├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── trianguloy │ │ └── continuousDataUsage │ │ ├── activities │ │ ├── HistoryActivity.java │ │ ├── ListAdapter.java │ │ ├── Main.java │ │ └── SettingsActivity.java │ │ ├── common │ │ ├── Accumulated.java │ │ ├── DataUsage.java │ │ ├── NonTouchableView.java │ │ ├── NumericEditText.java │ │ ├── PeriodCalendar.java │ │ ├── Preferences.java │ │ ├── Tweaks.java │ │ └── Utils.java │ │ └── widgets │ │ ├── AppWidgetBase.java │ │ ├── AppWidgetProgress.java │ │ └── AppWidgetRate.java │ ├── play │ ├── contact-email.txt │ ├── contact-website.txt │ ├── listings │ │ └── en-US │ │ │ ├── full-description.txt │ │ │ ├── graphics │ │ │ ├── feature-graphic │ │ │ │ ├── featured.png │ │ │ │ └── featured.xcf │ │ │ ├── icon │ │ │ │ └── ic_launcher-web.png │ │ │ └── phone-screenshots │ │ │ │ ├── Screenshot_1601111957.png │ │ │ │ ├── Screenshot_1601111968.png │ │ │ │ ├── Screenshot_1601112040.png │ │ │ │ └── Screenshot_1601112106.png │ │ │ ├── short-description.txt │ │ │ └── title.txt │ └── release-notes │ │ └── en-US │ │ └── default.txt │ └── res │ ├── drawable-nodpi │ ├── preview_progress.png │ └── preview_rate.png │ ├── drawable │ ├── background_progress.xml │ ├── background_progress_white.xml │ ├── background_rate.xml │ ├── background_rate_white.xml │ ├── ic_data_usage_black_24dp.xml │ ├── ic_history.xml │ └── ic_history_black.xml │ ├── layout │ ├── activity_history.xml │ ├── activity_main.xml │ ├── activity_settings.xml │ ├── lv_item.xml │ ├── widget_progress.xml │ ├── widget_progress_short.xml │ └── widget_rate.xml │ ├── menu │ └── menu_history.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-ru │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── dimens.xml │ ├── integers.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ ├── widgetprovider_progress.xml │ └── widgetprovider_rate.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | mapping.txt 5 | output.json 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | /app/release/release/ 18 | 19 | # Gradle files 20 | .gradle/ 21 | build/ 22 | 23 | # Local configuration file (sdk path, etc) 24 | local.properties 25 | 26 | # Proguard folder generated by Eclipse 27 | proguard/ 28 | 29 | # Log Files 30 | *.log 31 | 32 | # Android Studio Navigation editor temp files 33 | .navigation/ 34 | 35 | # Android Studio captures folder 36 | captures/ 37 | 38 | # Intellij 39 | *.iml 40 | .idea/ 41 | 42 | # Keystore files 43 | *.jks 44 | 45 | # External native build folder generated in Android Studio 2.2 and later 46 | .externalNativeBuild 47 | 48 | # Google Services (e.g. APIs or Firebase) 49 | google-services.json 50 | 51 | # Freeline 52 | freeline.py 53 | freeline/ 54 | freeline_project_description.json 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 TrianguloY 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Average-data-usage-widget 2 | 3 | Android app made by TrianguloY 4 | 5 | Get it on Google Play:\ 6 | https://play.google.com/store/apps/details?id=com.trianguloy.continuousDataUsage \ 7 | [Get it on Google Play](https://play.google.com/store/apps/details?id=com.trianguloy.continuousDataUsage) 10 | 11 | Get it on F-Droid:\ 12 | https://f-droid.org/packages/com.trianguloy.continuousDataUsage/ \ 13 | [Get it on F-Droid](https://f-droid.org/packages/com.trianguloy.continuousDataUsage/) 16 | 17 | Check more details on the app page: \ 18 | http://triangularapps.blogspot.com/p/average-data-usage-widget.html 19 | 20 | You can use parts of this project in your own ones, create pull request, or upload modified versions of it AS LONG AS you credit me. 21 | 22 | How to credit: 23 | - You must add my nick (TrianguloY) in an 'about' or 'acknowledgments' section visible to the user. 24 | - You must add a link to this GitHub main page (https://github.com/TrianguloY/Average-data-usage-widget/) or subpage (if you used a part of the code or an asset) in an 'about' or 'acknowledgments' section visible to the user. 25 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'com.github.triplet.play' version '3.7.0' 4 | } 5 | 6 | android { 7 | namespace 'com.trianguloy.continuousDataUsage' 8 | compileSdk 34 9 | defaultConfig { 10 | applicationId "com.trianguloy.continuousDataUsage" 11 | minSdkVersion 29 12 | targetSdkVersion 34 13 | versionCode 28 14 | versionName "4.1" 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled true 19 | shrinkResources true 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_17 26 | targetCompatibility JavaVersion.VERSION_17 27 | } 28 | buildFeatures { 29 | buildConfig true 30 | } 31 | } 32 | 33 | dependencies { 34 | implementation fileTree(dir: 'libs', include: ['*.jar']) 35 | } 36 | 37 | 38 | play { 39 | track.set("beta") 40 | releaseName.set(android.defaultConfig.versionName + " (" + android.defaultConfig.versionCode + ")") 41 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 37 | 38 | 42 | 43 | 44 | 45 | 46 | 49 | 50 | 54 | 55 | 56 | 57 | 58 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/trianguloy/continuousDataUsage/activities/HistoryActivity.java: -------------------------------------------------------------------------------- 1 | package com.trianguloy.continuousDataUsage.activities; 2 | 3 | import android.app.Activity; 4 | import android.app.AlertDialog; 5 | import android.app.TaskStackBuilder; 6 | import android.content.ActivityNotFoundException; 7 | import android.content.ComponentName; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.os.Bundle; 11 | import android.view.Menu; 12 | import android.view.MenuItem; 13 | import android.view.View; 14 | import android.view.Window; 15 | import android.widget.Button; 16 | import android.widget.ListView; 17 | import android.widget.ProgressBar; 18 | import android.widget.TextView; 19 | import android.widget.Toast; 20 | 21 | import com.trianguloy.continuousDataUsage.R; 22 | import com.trianguloy.continuousDataUsage.common.Accumulated; 23 | import com.trianguloy.continuousDataUsage.common.DataUsage; 24 | import com.trianguloy.continuousDataUsage.common.PeriodCalendar; 25 | import com.trianguloy.continuousDataUsage.common.Preferences; 26 | 27 | import java.text.DateFormat; 28 | import java.text.SimpleDateFormat; 29 | import java.util.Calendar; 30 | import java.util.Locale; 31 | 32 | 33 | /** 34 | * Activity to show the usage each day from the period. 35 | */ 36 | public class HistoryActivity extends Activity { 37 | 38 | // objects 39 | private ListAdapter adapter; 40 | private PeriodCalendar periodCalendar; 41 | private DataUsage dataUsage; 42 | private Preferences pref; 43 | 44 | // variables 45 | private int period; 46 | private TextView view_title; 47 | private ProgressBar view_loading; 48 | private Button view_right; 49 | private final DateFormat dateFormat = SimpleDateFormat.getDateInstance(); 50 | private Thread thread_setPeriod = null; 51 | 52 | @Override 53 | protected void onCreate(Bundle savedInstanceState) { 54 | super.onCreate(savedInstanceState); 55 | requestWindowFeature(Window.FEATURE_ACTION_BAR); 56 | setContentView(R.layout.activity_history); 57 | 58 | // initialize objects 59 | pref = new Preferences(this); 60 | periodCalendar = new PeriodCalendar(pref); 61 | dataUsage = new DataUsage(this, pref); 62 | adapter = new ListAdapter(this); 63 | 64 | try { 65 | // check for permission explicitly 66 | dataUsage.init(); 67 | } catch (DataUsage.Error e) { 68 | // can't open, probably no permission 69 | Toast.makeText(this, e.errorId, Toast.LENGTH_LONG).show(); 70 | finish(); 71 | } 72 | 73 | // get views 74 | ListView view_list = findViewById(R.id.h_lv_list); 75 | view_title = findViewById(R.id.h_tv_title); 76 | view_loading = findViewById(R.id.h_pb_loading); 77 | view_right = findViewById(R.id.h_btn_right); 78 | 79 | // initialize elements 80 | view_list.setAdapter(adapter); 81 | adapter.setDummyView(findViewById(R.id.h_item_dummy)); 82 | 83 | // update 84 | new Accumulated(pref, dataUsage, periodCalendar).updatePeriod(); 85 | 86 | period = 0; 87 | } 88 | 89 | @Override 90 | protected void onResume() { 91 | super.onResume(); 92 | // When resumed, reload period 93 | setPeriod(); 94 | } 95 | 96 | @Override 97 | public boolean onCreateOptionsMenu(Menu menu) { 98 | getMenuInflater().inflate(R.menu.menu_history, menu); 99 | return true; 100 | } 101 | 102 | public void onClick(View view) { 103 | switch (view.getId()) { 104 | case R.id.h_btn_left: 105 | // decrement period 106 | period--; 107 | setPeriod(); 108 | break; 109 | case R.id.h_btn_right: 110 | // increment period (if possible) 111 | if (period < 0) { 112 | period++; 113 | setPeriod(); 114 | } 115 | break; 116 | } 117 | } 118 | 119 | @Override 120 | public boolean onOptionsItemSelected(MenuItem item) { 121 | switch (item.getItemId()) { 122 | case R.id.h_action_info: 123 | //show info 124 | new AlertDialog.Builder(this).setMessage(R.string.h_toast_info).setCancelable(true).show(); 125 | return true; 126 | case R.id.h_action_settings: 127 | //show settings 128 | startActivity(new Intent(this, SettingsActivity.class)); 129 | return true; 130 | case R.id.h_action_android: 131 | //open android usage screen 132 | openAndroidUsageScreen(this); 133 | return true; 134 | case android.R.id.home: 135 | //explicitly go back to main activity 136 | final Intent upIntent = getParentActivityIntent(); 137 | if (shouldUpRecreateTask(upIntent) || isTaskRoot()) { 138 | TaskStackBuilder.create(this).addNextIntentWithParentStack(upIntent).startActivities(); 139 | } else { 140 | navigateUpTo(upIntent); 141 | } 142 | return true; 143 | } 144 | return super.onOptionsItemSelected(item); 145 | } 146 | 147 | public static void openAndroidUsageScreen(Context cntx) { 148 | Intent settings = new Intent(Intent.ACTION_MAIN); 149 | settings.setComponent(new ComponentName("com.android.settings", "com.android.settings.Settings$DataUsageSummaryActivity")); 150 | settings.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 151 | try { 152 | cntx.startActivity(settings); 153 | } catch (ActivityNotFoundException e) { 154 | Toast.makeText(cntx, R.string.toast_activityNotFound, Toast.LENGTH_LONG).show(); 155 | } 156 | } 157 | 158 | // reload period 159 | public void setPeriod() { 160 | 161 | //init variables 162 | final Calendar to = periodCalendar.getStartOfPeriod(period); 163 | SimpleDateFormat title_format = new SimpleDateFormat("MMM yyyy", Locale.getDefault()); 164 | 165 | //get from 166 | String monthFrom = title_format.format(to.getTime()); 167 | final long from = to.getTimeInMillis(); 168 | 169 | 170 | //get to 171 | to.add(pref.getPeriodType(), pref.getPeriodLength()); 172 | to.add(Calendar.DAY_OF_MONTH, -1); 173 | String monthTo = title_format.format(to.getTime()); 174 | 175 | //set title 176 | final String month = monthTo.equals(monthFrom) ? monthFrom : monthFrom + " - " + monthTo; 177 | view_title.setText(month); 178 | to.add(Calendar.DAY_OF_MONTH, 1); 179 | 180 | 181 | //background update 182 | if (thread_setPeriod != null) { 183 | thread_setPeriod.interrupt(); 184 | try { 185 | thread_setPeriod.join(); 186 | } catch (InterruptedException e) { 187 | e.printStackTrace(); 188 | } 189 | } 190 | 191 | //set as loading 192 | view_loading.setVisibility(View.VISIBLE); 193 | adapter.clearItems(); 194 | adapter.notifyDataSetChanged(); 195 | view_right.setEnabled(period < 0); 196 | 197 | 198 | thread_setPeriod = new Thread(() -> { 199 | 200 | // set period&average 201 | adapter.setTotalData(pref.getTotalData()); 202 | adapter.setDataPerDay( 203 | pref.getTotalData() / Math.round((to.getTimeInMillis() - from) / 1000d / 60d / 60d / 24d) 204 | ); 205 | 206 | var periodData = 0D; 207 | var days = 0; 208 | 209 | try { 210 | // each day 211 | while (from < to.getTimeInMillis()) { 212 | if (Thread.currentThread().isInterrupted()) return; 213 | 214 | long end = to.getTimeInMillis(); 215 | to.add(Calendar.DAY_OF_MONTH, -1); 216 | if (to.getTimeInMillis() <= System.currentTimeMillis()) { 217 | var dayData = dataUsage.getDataFromPeriod(to.getTimeInMillis(), end); 218 | adapter.addAverageItem(dayData, dateFormat.format(to.getTime())); 219 | days++; 220 | periodData += dayData; 221 | } 222 | } 223 | 224 | adapter.addSeparator(); 225 | 226 | // average & total 227 | adapter.addAverageItem(periodData / days, getString(R.string.txt_average)); 228 | adapter.addTotalItem(periodData, getString(R.string.txt_total)); 229 | 230 | } catch (final DataUsage.Error e) { 231 | if (Thread.currentThread().isInterrupted()) return; 232 | adapter.clearItems(); 233 | runOnUiThread(() -> Toast.makeText(HistoryActivity.this, e.errorId, Toast.LENGTH_LONG).show()); 234 | } 235 | 236 | if (Thread.currentThread().isInterrupted()) return; 237 | 238 | //notify 239 | runOnUiThread(() -> { 240 | adapter.notifyDataSetChanged(); 241 | view_loading.setVisibility(View.GONE); 242 | }); 243 | }); 244 | thread_setPeriod.start(); 245 | 246 | } 247 | 248 | } 249 | -------------------------------------------------------------------------------- /app/src/main/java/com/trianguloy/continuousDataUsage/activities/ListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.trianguloy.continuousDataUsage.activities; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.BaseAdapter; 8 | import android.widget.ProgressBar; 9 | import android.widget.TextView; 10 | 11 | import com.trianguloy.continuousDataUsage.R; 12 | import com.trianguloy.continuousDataUsage.common.Preferences; 13 | import com.trianguloy.continuousDataUsage.common.Tweaks; 14 | import com.trianguloy.continuousDataUsage.common.Utils; 15 | 16 | import java.util.ArrayList; 17 | 18 | /** 19 | * ListAdapter for the {@link HistoryActivity} 20 | */ 21 | public class ListAdapter extends BaseAdapter { 22 | 23 | /** 24 | * total data of the full period 25 | */ 26 | private float totalData; 27 | 28 | /** 29 | * Context used 30 | */ 31 | private final Context cntx; 32 | 33 | /** 34 | * Preferences 35 | */ 36 | private final Preferences pref; 37 | 38 | /** 39 | * Items in the list 40 | */ 41 | private ArrayList items = new ArrayList<>(); 42 | private ArrayList itemsTemp = new ArrayList<>(); 43 | 44 | /** 45 | * Data per day 46 | */ 47 | private float dataPerDay; 48 | 49 | /** 50 | * Adds an average item, doesn't refresh 51 | */ 52 | void addAverageItem(double usage, String date) { 53 | final Item item = new Item(usage, date, Item.Type.AVERAGE); 54 | itemsTemp.add(0, item); 55 | 56 | // update max width 57 | String text = item.usageString(); 58 | if (text.length() > dummy_usage_text.length()) { 59 | dummy_usage_text = text; 60 | } 61 | } 62 | 63 | /** 64 | * Dummy view to extract sizes 65 | */ 66 | private View dummy; 67 | private TextView dummy_usage; 68 | private String dummy_usage_text; 69 | 70 | //---------- Public ----------- 71 | 72 | /** 73 | * Default constructor 74 | * 75 | * @param cntx context 76 | */ 77 | ListAdapter(Context cntx) { 78 | this.cntx = cntx; 79 | this.pref = new Preferences(cntx); 80 | } 81 | 82 | /** 83 | * Adds a total item, doesn't refresh 84 | */ 85 | void addTotalItem(double usage, String label) { 86 | var item = new Item(usage, label, Item.Type.TOTAL); 87 | itemsTemp.add(0, item); 88 | 89 | 90 | // update max width 91 | String text = item.usageString(); 92 | if (text.length() > dummy_usage_text.length()) { 93 | dummy_usage_text = text; 94 | } 95 | } 96 | 97 | /** 98 | * Adds a separator, doesn't refresh 99 | */ 100 | void addSeparator() { 101 | itemsTemp.add(0, new Item(0, "", Item.Type.SEPARATOR)); 102 | } 103 | 104 | /** 105 | * sets the total data 106 | */ 107 | void setTotalData(float totalData) { 108 | this.totalData = totalData; 109 | } 110 | 111 | /** 112 | * Removes all the items, doesn't refresh 113 | */ 114 | void clearItems() { 115 | itemsTemp.clear(); 116 | dummy_usage_text = ""; 117 | } 118 | 119 | /** 120 | * sets the data per day 121 | */ 122 | void setDataPerDay(float dataPerDay) { 123 | this.dataPerDay = dataPerDay; 124 | } 125 | 126 | /** 127 | * Sets the dummy view 128 | */ 129 | public void setDummyView(View dummy) { 130 | this.dummy = dummy; 131 | dummy_usage = dummy.findViewById(R.id.lv_txt_usage); 132 | } 133 | 134 | //------------ Adapter overrides --------------- 135 | 136 | 137 | @Override 138 | public void notifyDataSetChanged() { 139 | items = itemsTemp; 140 | itemsTemp = new ArrayList<>(); 141 | dummy_usage.setText(dummy_usage_text); 142 | super.notifyDataSetChanged(); 143 | } 144 | 145 | @Override 146 | public int getCount() { 147 | return items.size(); 148 | } 149 | 150 | @Override 151 | public long getItemId(int i) { 152 | return items.get(i).label.hashCode(); 153 | } 154 | 155 | @Override 156 | public Object getItem(int i) { 157 | return items.get(i); 158 | } 159 | 160 | @Override 161 | public View getView(int position, View convertView, ViewGroup parent) { 162 | 163 | // inflate the layout for each list row 164 | if (convertView == null) { 165 | convertView = LayoutInflater.from(cntx). 166 | inflate(R.layout.lv_item, parent, false); 167 | 168 | for (int id : new int[]{R.id.lv_txt_date, R.id.lv_txt_usage, R.id.lv_pgb_negative, R.id.lv_pgb_positive}) { 169 | View view = convertView.findViewById(id); 170 | ViewGroup.LayoutParams lp = view.getLayoutParams(); 171 | lp.width = dummy.findViewById(id).getMeasuredWidth(); 172 | view.setLayoutParams(lp); 173 | } 174 | } 175 | 176 | // get current item to be displayed 177 | Item currentItem = (Item) getItem(position); 178 | 179 | // get the Views 180 | TextView txt_date = convertView.findViewById(R.id.lv_txt_date); 181 | TextView txt_usage = convertView.findViewById(R.id.lv_txt_usage); 182 | ProgressBar pgb_positive = convertView.findViewById(R.id.lv_pgb_positive); 183 | ProgressBar pgb_negative = convertView.findViewById(R.id.lv_pgb_negative); 184 | 185 | if (currentItem.type == Item.Type.SEPARATOR) { 186 | // separator, just hide and return 187 | txt_date.setVisibility(View.GONE); 188 | txt_usage.setVisibility(View.GONE); 189 | pgb_positive.setVisibility(View.GONE); 190 | pgb_negative.setVisibility(View.GONE); 191 | return convertView; 192 | } else { 193 | // non-separator, unhide 194 | txt_date.setVisibility(View.VISIBLE); 195 | txt_usage.setVisibility(View.VISIBLE); 196 | pgb_positive.setVisibility(View.VISIBLE); 197 | pgb_negative.setVisibility(View.VISIBLE); 198 | } 199 | 200 | // sets the properties 201 | txt_date.setText(currentItem.label); 202 | txt_usage.setText(currentItem.usageString()); 203 | 204 | double rate = currentItem.usage / (currentItem.type == Item.Type.TOTAL ? totalData : dataPerDay); 205 | if (rate > 1) { 206 | // more than average/total 207 | pgb_negative.setProgress(0); 208 | pgb_positive.setProgress(Utils.dbl2int((rate % 1) * pgb_positive.getMax())); 209 | pgb_positive.setSecondaryProgress(rate > 2 ? pgb_positive.getMax() : 0); 210 | } else { 211 | // less than average 212 | pgb_negative.setProgress(Utils.dbl2int((1 - rate) * pgb_negative.getMax())); 213 | pgb_positive.setProgress(0); 214 | pgb_positive.setSecondaryProgress(0); 215 | } 216 | 217 | // tweaks 218 | if (pref.getTweak(Tweaks.Tweak.capNoWarp) && pgb_positive.getSecondaryProgress() > 0) { 219 | // cap 220 | pgb_positive.setProgress(pgb_positive.getMax()); 221 | pgb_positive.setSecondaryProgress(0); 222 | } 223 | 224 | // returns the view for the current row 225 | return convertView; 226 | } 227 | 228 | /** 229 | * Each element in the list 230 | */ 231 | class Item { 232 | /** 233 | * The day 234 | */ 235 | String label; 236 | 237 | /** 238 | * The usage 239 | */ 240 | double usage; 241 | /** 242 | * Type of the row 243 | */ 244 | Type type; 245 | Item(double usage, String label, Type type) { 246 | this.usage = usage; 247 | this.label = label; 248 | this.type = type; 249 | } 250 | 251 | /** 252 | * @return the usage string 253 | */ 254 | String usageString() { 255 | return Utils.formatData(pref, "{0} / {M}", usage, (double) (type == Type.TOTAL ? totalData : dataPerDay)); 256 | } 257 | 258 | /** 259 | * Type of the row 260 | */ 261 | enum Type { 262 | /** 263 | * An average value 264 | */ 265 | AVERAGE, 266 | /** 267 | * A total value 268 | */ 269 | TOTAL, 270 | /** 271 | * A separator 272 | */ 273 | SEPARATOR 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /app/src/main/java/com/trianguloy/continuousDataUsage/activities/Main.java: -------------------------------------------------------------------------------- 1 | package com.trianguloy.continuousDataUsage.activities; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.text.method.LinkMovementMethod; 7 | import android.view.View; 8 | import android.widget.FrameLayout; 9 | import android.widget.RemoteViews; 10 | import android.widget.TextView; 11 | import android.widget.Toast; 12 | 13 | import com.trianguloy.continuousDataUsage.R; 14 | import com.trianguloy.continuousDataUsage.common.Preferences; 15 | import com.trianguloy.continuousDataUsage.common.Tweaks; 16 | import com.trianguloy.continuousDataUsage.widgets.AppWidgetProgress; 17 | 18 | /** 19 | * Main activity. Info 20 | */ 21 | public class Main extends Activity { 22 | 23 | private FrameLayout preview; 24 | 25 | /** 26 | * That function 27 | * 28 | * @param savedInstanceState no idea what this is 29 | */ 30 | @Override 31 | protected void onCreate(Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | setContentView(R.layout.activity_main); 34 | 35 | //clickable links 36 | ((TextView) findViewById(R.id.stt_txt_info)).setMovementMethod(LinkMovementMethod.getInstance()); 37 | 38 | preview = findViewById(R.id.m_f_widget); 39 | 40 | } 41 | 42 | /** 43 | * When the activity is resumed, update the widget 44 | */ 45 | @Override 46 | protected void onResume() { 47 | 48 | updatePreview(); 49 | 50 | super.onResume(); 51 | } 52 | 53 | /** 54 | * A button is clicked 55 | * 56 | * @param view which one 57 | */ 58 | public void onClick(View view) { 59 | switch (view.getId()) { 60 | case R.id.m_btn_history: 61 | //open history 62 | startActivity(new Intent(this, HistoryActivity.class)); 63 | break; 64 | case R.id.m_btn_settings: 65 | //open settings 66 | startActivity(new Intent(this, SettingsActivity.class)); 67 | break; 68 | case R.id.m_f_widget: 69 | //show info 70 | Toast.makeText(this, R.string.toast_preview, Toast.LENGTH_LONG).show(); 71 | break; 72 | } 73 | 74 | } 75 | 76 | /** 77 | * Updates the preview 78 | */ 79 | private void updatePreview() { 80 | // Construct the RemoteViews object 81 | RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.widget_progress); 82 | AppWidgetProgress.updateViews(this, remoteViews); 83 | 84 | try { 85 | 86 | View views = remoteViews.apply(this, preview); 87 | preview.removeAllViews(); 88 | preview.addView(views); 89 | 90 | } catch (Throwable ignore) { 91 | 92 | // if exception, disable 93 | Toast.makeText(this, "Exception detected, disabling tweaks", Toast.LENGTH_LONG).show(); 94 | Preferences prefs = new Preferences(this); 95 | for (Tweaks.Tweak item : Tweaks.Tweak.values()) { 96 | prefs.setTweak(item, false); 97 | } 98 | updatePreview(); 99 | 100 | } 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/trianguloy/continuousDataUsage/activities/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package com.trianguloy.continuousDataUsage.activities; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.app.AlertDialog; 6 | import android.app.AppOpsManager; 7 | import android.app.DatePickerDialog; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.content.pm.PackageManager; 11 | import android.graphics.Color; 12 | import android.os.Build; 13 | import android.os.Bundle; 14 | import android.os.Process; 15 | import android.provider.Settings; 16 | import android.text.InputType; 17 | import android.text.method.LinkMovementMethod; 18 | import android.view.View; 19 | import android.view.inputmethod.EditorInfo; 20 | import android.widget.AdapterView; 21 | import android.widget.ArrayAdapter; 22 | import android.widget.CheckBox; 23 | import android.widget.EditText; 24 | import android.widget.LinearLayout; 25 | import android.widget.SeekBar; 26 | import android.widget.Spinner; 27 | import android.widget.TextView; 28 | import android.widget.Toast; 29 | 30 | import com.trianguloy.continuousDataUsage.R; 31 | import com.trianguloy.continuousDataUsage.common.Accumulated; 32 | import com.trianguloy.continuousDataUsage.common.DataUsage; 33 | import com.trianguloy.continuousDataUsage.common.NumericEditText; 34 | import com.trianguloy.continuousDataUsage.common.PeriodCalendar; 35 | import com.trianguloy.continuousDataUsage.common.Preferences; 36 | import com.trianguloy.continuousDataUsage.common.Tweaks; 37 | import com.trianguloy.continuousDataUsage.common.Utils; 38 | 39 | import java.text.SimpleDateFormat; 40 | import java.util.Arrays; 41 | import java.util.Calendar; 42 | import java.util.List; 43 | 44 | /** 45 | * Main activity: settings 46 | */ 47 | public class SettingsActivity extends Activity { 48 | 49 | // used classes 50 | private Preferences pref = null; 51 | private Accumulated accumulated; 52 | 53 | // views 54 | private NumericEditText view_accumulated; 55 | private TextView view_txt_decimals; 56 | private EditText txt_periodStart; 57 | private TextView txt_notif; 58 | private LinearLayout ll_notif; 59 | private TextView txt_usageStats; 60 | 61 | 62 | /** 63 | * When the activity is created, like a constructor 64 | * 65 | * @param savedInstanceState previous saved state, used on super only 66 | */ 67 | @Override 68 | protected void onCreate(Bundle savedInstanceState) { 69 | super.onCreate(savedInstanceState); 70 | setContentView(R.layout.activity_settings); 71 | 72 | // get objects 73 | pref = new Preferences(this); 74 | accumulated = new Accumulated(pref, new DataUsage(this, pref), new PeriodCalendar(pref)); 75 | 76 | // get views 77 | txt_notif = findViewById(R.id.stt_txt_notif); 78 | ll_notif = findViewById(R.id.ll_notif); 79 | txt_usageStats = findViewById(R.id.stt_txt_usageStats); 80 | 81 | //initializes 82 | initialize(); 83 | 84 | // updates 85 | accumulated.updatePeriod(); 86 | updateViews(); 87 | 88 | } 89 | 90 | 91 | /** 92 | * Initialize all the views 93 | */ 94 | private void initialize() { 95 | 96 | // totaldata 97 | final NumericEditText view_totalData = findViewById(R.id.stt_edTxt_totalData); 98 | view_totalData.initFloat(false, false, pref.getTotalData(), number -> pref.setTotalData(number)); 99 | 100 | // period length, amount 101 | final NumericEditText txt_periodLength = findViewById(R.id.stt_edTxt_periodLength); 102 | txt_periodLength.initInt(false, false, pref.getPeriodLength(), number -> pref.setPeriodLength(number)); 103 | 104 | // period length, type 105 | final Spinner spn_periodType = findViewById(R.id.stt_spn_periodType); 106 | final List periodTypes = Arrays.asList(Calendar.DAY_OF_MONTH, Calendar.MONTH); 107 | spn_periodType.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, new String[]{getString(R.string.days), getString(R.string.months)})); 108 | spn_periodType.setSelection(periodTypes.indexOf(pref.getPeriodType())); 109 | spn_periodType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 110 | @Override 111 | public void onItemSelected(AdapterView adapterView, View view, int i, long l) { 112 | pref.setPeriodType(periodTypes.get(i)); 113 | } 114 | 115 | @Override 116 | public void onNothingSelected(AdapterView adapterView) { 117 | pref.setPeriodType(periodTypes.get(0)); 118 | } 119 | }); 120 | 121 | // periodStart 122 | txt_periodStart = findViewById(R.id.stt_edTxt_periodStart); 123 | 124 | // decimals 125 | view_txt_decimals = findViewById(R.id.stt_txt_decimals); 126 | SeekBar view_sb_decimals = findViewById(R.id.stt_sb_decimals); 127 | view_sb_decimals.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 128 | @Override 129 | public void onProgressChanged(SeekBar seekBar, int i, boolean b) { 130 | view_txt_decimals.setText(Integer.toString(i)); 131 | pref.setDecimals(i); 132 | } 133 | 134 | @Override 135 | public void onStartTrackingTouch(SeekBar seekBar) { 136 | } 137 | 138 | @Override 139 | public void onStopTrackingTouch(SeekBar seekBar) { 140 | } 141 | }); 142 | view_sb_decimals.setProgress(pref.getDecimals(), false); // setProgress may not update if the value is the same 143 | view_txt_decimals.setText(Integer.toString(pref.getDecimals())); 144 | 145 | // GB 146 | CheckBox view_gb = findViewById(R.id.stt_chk_gb); 147 | view_gb.setChecked(pref.getGB()); 148 | view_gb.setOnCheckedChangeListener((compoundButton, b) -> pref.setGB(b)); 149 | 150 | // inv 151 | var view_inv = this.findViewById(R.id.stt_chk_inv); 152 | view_inv.setChecked(pref.getInv()); 153 | view_inv.setOnCheckedChangeListener((compoundButton, b) -> pref.setInv(b)); 154 | 155 | // alternate conversion 156 | final CheckBox view_alternateConversion = findViewById(R.id.stt_chkBx_alternateConversion); 157 | view_alternateConversion.setChecked(pref.getAltConversion()); 158 | view_alternateConversion.setOnClickListener(view -> pref.setAltConversion(view_alternateConversion.isChecked())); 159 | 160 | // accumulated periods 161 | final NumericEditText view_sb_savedPeriods = findViewById(R.id.stt_edTxt_savedPeriods); 162 | view_sb_savedPeriods.initInt(true, false, pref.getSavedPeriods(), number -> pref.setSavedPeriods(number)); 163 | 164 | // accumulated megas 165 | view_accumulated = findViewById(R.id.stt_edTxt_accum); 166 | view_accumulated.initFloat(true, true, pref.getAccumulated(), number -> pref.setAccumulated(number)); 167 | 168 | // clickable links 169 | ((TextView) findViewById(R.id.stt_txt_perm_us)).setMovementMethod(LinkMovementMethod.getInstance()); 170 | 171 | } 172 | 173 | private void updateViews() { 174 | // period start 175 | final Calendar periodStart = pref.getPeriodStart(); 176 | txt_periodStart.setText(SimpleDateFormat.getDateInstance().format(periodStart.getTime())); 177 | 178 | // accumulated megas 179 | view_accumulated.setValue(pref.getAccumulated()); 180 | 181 | // getUsageStats permission 182 | int mode = AppOpsManager.MODE_DEFAULT; 183 | AppOpsManager appOps = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE); 184 | if (appOps != null) { 185 | //check permission 186 | mode = appOps.unsafeCheckOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, Process.myUid(), getPackageName()); 187 | } 188 | setPermissionState(txt_usageStats, mode == AppOpsManager.MODE_ALLOWED, true); 189 | 190 | // notifications permission 191 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 192 | setPermissionState(txt_notif, checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED, false); 193 | ll_notif.setVisibility(View.VISIBLE); 194 | } else { 195 | ll_notif.setVisibility(View.GONE); 196 | } 197 | } 198 | 199 | 200 | /** 201 | * Updates the specified id with the specified state. 202 | * state=true -> green and 'permission granted' 203 | * state=false && !required -> orange and 'permission not granted' 204 | * state=false && required -> red and 'permission needed' 205 | */ 206 | private void setPermissionState(TextView txt, boolean granted, boolean required) { 207 | txt.setText(granted ? getString(R.string.txt_permissionGranted) 208 | : required ? getString(R.string.txt_permissionNeeded) 209 | : getString(R.string.txt_permissionNotGranted)); 210 | txt.setBackgroundColor(granted ? Color.argb(128, 0, 255, 0) // green 211 | : required ? Color.argb(128, 255, 0, 0) // orange 212 | : Color.argb(128, 255, 127, 0)); // red 213 | } 214 | 215 | 216 | /** 217 | * Button clicked 218 | * 219 | * @param view which button 220 | */ 221 | public void onClick(View view) { 222 | switch (view.getId()) { 223 | //open usage settings to give permission 224 | case R.id.stt_btn_usageStats -> startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)); 225 | //request 'notifications permission' button, request permission 226 | case R.id.stt_btn_notif -> { 227 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) 228 | requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 0); 229 | } 230 | // start date picker 231 | case R.id.stt_edTxt_periodStart -> pickPeriodStart(); 232 | case R.id.stt_btn_accum -> { 233 | //auto-calculate accumulated 234 | calculate(); 235 | } 236 | // show tweaks 237 | case R.id.stt_btn_tweaks -> new Tweaks(pref, this).showDialog(); 238 | } 239 | } 240 | 241 | /** 242 | * Allows the user to choose how to calculate the accumulated data 243 | */ 244 | private void calculate() { 245 | new AlertDialog.Builder(this) 246 | .setTitle(R.string.btn_settings_accum) 247 | .setItems(R.array.itms_settings_accum, (dialog, which) -> { 248 | try { 249 | switch (which) { 250 | case 0 -> { 251 | view_accumulated.setValue((float) accumulated.autoCalculateAccumulated()); 252 | 253 | // update if necessary 254 | txt_periodStart.setText(SimpleDateFormat.getDateInstance().format(pref.getPeriodStart().getTime())); 255 | } 256 | case 1 -> setVisibleData(); 257 | } 258 | } catch (DataUsage.Error e) { 259 | Toast.makeText(this, getString(e.errorId), Toast.LENGTH_LONG).show(); 260 | } 261 | }) 262 | .show(); 263 | 264 | 265 | } 266 | 267 | /** 268 | * Calculates the accumulated data based on the visible amount 269 | */ 270 | private void setVisibleData() throws DataUsage.Error { 271 | 272 | double data = accumulated.getUsedDataFromCurrentPeriod(); 273 | 274 | // invert data 275 | if (pref.getInv()) { 276 | data = pref.getTotalData() - data; 277 | } 278 | 279 | final float[] input = {(float) data}; 280 | NumericEditText txt = new NumericEditText(this); 281 | txt.setInputType(EditorInfo.TYPE_NUMBER_FLAG_DECIMAL | InputType.TYPE_NUMBER_FLAG_SIGNED); 282 | txt.initFloat(true, true, input[0], number -> input[0] = number); 283 | new AlertDialog.Builder(this) 284 | .setTitle(R.string.txt_visible_title) 285 | .setMessage(R.string.txt_visible_message) 286 | .setView(txt) 287 | .setPositiveButton(android.R.string.ok, (dialog, which) -> { 288 | 289 | // re-invert data 290 | if (pref.getInv()) { 291 | input[0] = pref.getTotalData() - input[0]; 292 | } 293 | 294 | try { 295 | view_accumulated.setValue((float) accumulated.setUsedDataFromCurrentPeriod(input[0])); 296 | } catch (DataUsage.Error e) { 297 | Toast.makeText(this, getString(e.errorId), Toast.LENGTH_LONG).show(); 298 | } 299 | 300 | // update if necessary 301 | txt_periodStart.setText(SimpleDateFormat.getDateInstance().format(pref.getPeriodStart().getTime())); 302 | }) 303 | .setNegativeButton(android.R.string.cancel, null) 304 | .show(); 305 | } 306 | 307 | /** 308 | * Shows a date dialog to change the start date 309 | */ 310 | private void pickPeriodStart() { 311 | final DatePickerDialog dialog = new DatePickerDialog(this); 312 | dialog.setOnDateSetListener((datePicker, year, month, day) -> { 313 | // new date 314 | final Calendar cal = PeriodCalendar.from(year, month, day); 315 | txt_periodStart.setText(SimpleDateFormat.getDateInstance().format(cal.getTime())); 316 | pref.setPeriodStart(cal); 317 | }); 318 | // set 319 | final Calendar cal = pref.getPeriodStart(); 320 | dialog.updateDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)); 321 | // show 322 | dialog.show(); 323 | } 324 | 325 | 326 | /** 327 | * Activity resumed, probably after opening usage settings, updates views accordingly. 328 | */ 329 | @Override 330 | protected void onResume() { 331 | super.onResume(); 332 | accumulated.updatePeriod(); 333 | updateViews(); 334 | } 335 | 336 | @Override 337 | protected void onPause() { 338 | super.onPause(); 339 | Utils.updateAllWidgets(this); 340 | } 341 | 342 | /** 343 | * After requesting permissions, updates views accordingly. 344 | */ 345 | @Override 346 | public void onRequestPermissionsResult(int i, String[] s, int[] j) { 347 | updateViews(); 348 | } 349 | 350 | } 351 | -------------------------------------------------------------------------------- /app/src/main/java/com/trianguloy/continuousDataUsage/common/Accumulated.java: -------------------------------------------------------------------------------- 1 | package com.trianguloy.continuousDataUsage.common; 2 | 3 | import android.util.Log; 4 | 5 | import com.trianguloy.continuousDataUsage.BuildConfig; 6 | 7 | /** 8 | * Functions related to the accumulated data. 9 | */ 10 | public class Accumulated { 11 | 12 | // objects 13 | private final Preferences pref; 14 | private final DataUsage dataUsage; 15 | private final PeriodCalendar periodCalendar; 16 | 17 | public Accumulated(Preferences pref, DataUsage dataUsage, PeriodCalendar periodCalendar) { 18 | this.pref = pref; 19 | this.dataUsage = dataUsage; 20 | this.periodCalendar = periodCalendar; 21 | } 22 | 23 | /** 24 | * Returns the used data from the current period (used - accumulated) 25 | */ 26 | public double getUsedDataFromCurrentPeriod() throws DataUsage.Error { 27 | double usedData = dataUsage.getDataFromPeriod(periodCalendar.getLimitsOfPeriod(0).first, Long.MAX_VALUE); 28 | 29 | // subtract accumulated/offset from previous period 30 | usedData -= pref.getAccumulated(); 31 | 32 | return usedData; 33 | } 34 | 35 | /** 36 | * Sets the used data from the current period (by changing the accumulated value, which then returns) 37 | */ 38 | public double setUsedDataFromCurrentPeriod(double usedData) throws DataUsage.Error { 39 | double accumulated = dataUsage.getDataFromPeriod(periodCalendar.getLimitsOfPeriod(0).first, Long.MAX_VALUE) - usedData; 40 | pref.setAccumulated((float) accumulated); 41 | return accumulated; 42 | } 43 | 44 | /** 45 | * Checks if the currently saved period and accumulated data needs update, and does so. 46 | */ 47 | public void updatePeriod() { 48 | int current = periodCalendar.getCurrentPeriod(); 49 | 50 | if (current != 0) { 51 | // new period 52 | 53 | // update start of period 54 | pref.setPeriodStart(periodCalendar.getStartOfPeriod(current)); 55 | if (BuildConfig.DEBUG && periodCalendar.getCurrentPeriod() != 0) { 56 | throw new AssertionError("Current period is " + periodCalendar.getCurrentPeriod() + ", not 0!"); 57 | } 58 | 59 | // calculate new accumulated data 60 | try { 61 | pref.setAccumulated((float) calculateAccumulated(pref.getAccumulated(), -current, false, periodCalendar)); 62 | } catch (DataUsage.Error ignore) { 63 | // can't get, just ignore 64 | } 65 | Log.d("UPDATE", "Updated"); 66 | } 67 | } 68 | 69 | // ----------------------------------- 70 | 71 | /** 72 | * Recursive function to get the accumulated data starting from any previous period (ignores empty) 73 | * 74 | * @param accum accumulated data in latest period 75 | * @param period which latest period 76 | * @param skipEmpty if true, unspent months will be skipped 77 | * @param perCal PeriodCalendar object 78 | * @return accumulated data in previous current period 79 | * @throws DataUsage.Error if can't calculate it 80 | */ 81 | public double calculateAccumulated(double accum, int period, boolean skipEmpty, PeriodCalendar perCal) throws DataUsage.Error { 82 | if (period >= 0) { 83 | //end of recursion, final period 84 | return accum; 85 | } 86 | 87 | // onto next period 88 | double dataInPeriod = dataUsage.getDataFromPeriod(perCal.getLimitsOfPeriod(period)); 89 | 90 | // skip if required and nothing was spent (device not configured yet) 91 | if (!(skipEmpty && dataInPeriod == 0)) { 92 | 93 | // calculate accumulated 94 | accum += pref.getTotalData() - dataInPeriod; 95 | if (accum < 0) accum = 0; // if used more, accumulated=0 (used all, nothing saved) 96 | float periodsData = pref.getTotalData() * pref.getSavedPeriods(); 97 | if (accum > periodsData) accum = periodsData; // if accumulated more than possible, cut 98 | skipEmpty = false; 99 | 100 | } 101 | 102 | return calculateAccumulated(accum, period + 1, skipEmpty, perCal); 103 | } 104 | 105 | /** 106 | * Tries to calculate the accumulated data of the previous period from the latest 100 periods (ignoring empty) 107 | * 108 | * @return calculated accumulated data 109 | * @throws DataUsage.Error if can't get data 110 | */ 111 | public double autoCalculateAccumulated() throws DataUsage.Error { 112 | return calculateAccumulated(0, -100, true, periodCalendar); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/src/main/java/com/trianguloy/continuousDataUsage/common/DataUsage.java: -------------------------------------------------------------------------------- 1 | package com.trianguloy.continuousDataUsage.common; 2 | 3 | import android.Manifest; 4 | import android.app.usage.NetworkStats; 5 | import android.app.usage.NetworkStatsManager; 6 | import android.content.Context; 7 | import android.content.pm.PackageManager; 8 | import android.net.ConnectivityManager; 9 | import android.os.RemoteException; 10 | import android.telephony.TelephonyManager; 11 | import android.util.Log; 12 | import android.util.Pair; 13 | 14 | import com.trianguloy.continuousDataUsage.R; 15 | 16 | /** 17 | * Data-related functions. 18 | */ 19 | public class DataUsage { 20 | 21 | /** 22 | * Returned when calling a function 23 | */ 24 | public static class Error extends Exception { 25 | /** 26 | * The id of the string resource with the error 27 | */ 28 | public int errorId; 29 | 30 | Error(int errorId) { 31 | this.errorId = errorId; 32 | } 33 | } 34 | 35 | /** 36 | * Preferences 37 | */ 38 | private final Preferences pref; 39 | 40 | /** 41 | * Context 42 | */ 43 | private final Context cntx; 44 | 45 | 46 | //------------ Public -------------- 47 | 48 | /** 49 | * Constructor to avoid duplicated Preferences 50 | * 51 | * @param cntx context 52 | * @param pref preferences class 53 | */ 54 | public DataUsage(Context cntx, Preferences pref) { 55 | this.pref = pref; 56 | this.cntx = cntx; 57 | } 58 | 59 | 60 | /** 61 | * Initializes internal data 62 | */ 63 | public void init() throws Error { 64 | //get service 65 | nsm = cntx.getSystemService(NetworkStatsManager.class); 66 | if (nsm == null) { 67 | //can't get NetworkStatsManager 68 | Log.d("widget", "error on NetworkStatsManager"); 69 | throw new Error(R.string.txt_widget_errorService); 70 | } 71 | } 72 | 73 | /** 74 | * Returns the data usage on the given period 75 | * 76 | * @param from_to pair start&end of period 77 | * @return data usage in period 78 | * @throws Error if can't get the data 79 | */ 80 | public double getDataFromPeriod(Pair from_to) throws Error { 81 | return getDataFromPeriod(from_to.first, from_to.second); 82 | } 83 | 84 | /** 85 | * Returns the data usage on the given period 86 | * 87 | * @param from start of period 88 | * @param to end of period 89 | * @return data usage in period 90 | * @throws Error if can't get the data 91 | */ 92 | public double getDataFromPeriod(long from, long to) throws Error { 93 | 94 | //get data 95 | NetworkStats.Bucket bucket; 96 | try { 97 | bucket = getNsm().querySummaryForDevice(ConnectivityManager.TYPE_MOBILE, null, from, to); 98 | } catch (RemoteException e) { 99 | Log.d("widget", "error on querySummaryForDevice-RemoteException"); 100 | throw new Error(R.string.txt_widget_errorQuering); 101 | } catch (SecurityException se) { 102 | Log.d("widget", "error on querySummaryForDevice-SecurityException"); 103 | throw new Error(R.string.txt_widget_noPermission); 104 | } 105 | 106 | double bytesConversion = pref.getAltConversion() ? 107 | 1f / 1000f / 1000f 108 | : 109 | 1f / 1024f / 1024f; 110 | return (bucket.getRxBytes() + bucket.getTxBytes()) * bytesConversion; 111 | } 112 | 113 | // ------------------- internal data ------------------- 114 | 115 | 116 | /** 117 | * Class for the calculations 118 | */ 119 | private NetworkStatsManager nsm = null; 120 | 121 | private NetworkStatsManager getNsm() throws Error { 122 | if (nsm == null) 123 | init(); 124 | return nsm; 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /app/src/main/java/com/trianguloy/continuousDataUsage/common/NonTouchableView.java: -------------------------------------------------------------------------------- 1 | package com.trianguloy.continuousDataUsage.common; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.MotionEvent; 6 | import android.widget.FrameLayout; 7 | 8 | /** 9 | * A view that removes all touches 10 | */ 11 | public class NonTouchableView extends FrameLayout { 12 | 13 | public NonTouchableView(Context context) { 14 | super(context); 15 | } 16 | 17 | public NonTouchableView(Context context, AttributeSet attrs) { 18 | super(context, attrs); 19 | } 20 | 21 | public NonTouchableView(Context context, AttributeSet attrs, int defStyleAttr) { 22 | super(context, attrs, defStyleAttr); 23 | } 24 | 25 | public NonTouchableView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 26 | super(context, attrs, defStyleAttr, defStyleRes); 27 | } 28 | 29 | @Override 30 | public boolean onInterceptTouchEvent (MotionEvent ev){ 31 | return true; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/trianguloy/continuousDataUsage/common/NumericEditText.java: -------------------------------------------------------------------------------- 1 | package com.trianguloy.continuousDataUsage.common; 2 | 3 | import android.content.Context; 4 | import android.text.Editable; 5 | import android.text.TextWatcher; 6 | import android.util.AttributeSet; 7 | import android.util.Log; 8 | import android.widget.EditText; 9 | 10 | import java.text.NumberFormat; 11 | import java.text.ParseException; 12 | import java.util.Locale; 13 | 14 | public class NumericEditText extends EditText { 15 | private boolean mAllowZero = false; 16 | private boolean mAllowNegative = false; 17 | 18 | public NumericEditText(Context context) { 19 | super(context); 20 | } 21 | 22 | public NumericEditText(Context context, AttributeSet attrs) { 23 | super(context, attrs); 24 | } 25 | 26 | // ------------------- Integer ------------------- 27 | private OnNewIntListener mIntListener = null; 28 | 29 | public void initInt(boolean allowZero, boolean allowNegative, int value, OnNewIntListener listener) { 30 | mAllowZero = allowZero; 31 | mAllowNegative = allowNegative; 32 | addTextChangedListener(new TextWatcher() { 33 | @Override 34 | public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { 35 | } 36 | 37 | @Override 38 | public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { 39 | } 40 | 41 | @Override 42 | public void afterTextChanged(Editable editable) { 43 | // empty = nothing 44 | if (editable.toString().isEmpty()) return; 45 | 46 | try { 47 | int value = NumberFormat.getInstance(Locale.US).parse(editable.toString()).intValue(); 48 | if ((value > 0) || (mAllowZero && value == 0)) { 49 | // valid number 50 | setHint(String.format(Locale.US, "%s", value)); 51 | if (mIntListener != null) mIntListener.newNumber(value); 52 | } else { 53 | // invalid number 54 | setText(""); 55 | } 56 | } catch (ParseException | NullPointerException e) { 57 | Log.d("settings", "numberformatexception"); 58 | e.printStackTrace(); 59 | } 60 | } 61 | }); 62 | setValue(value); 63 | mIntListener = listener; 64 | } 65 | 66 | public void setValue(int value) { 67 | setText(String.format(Locale.US, "%s", value)); 68 | } 69 | 70 | public interface OnNewIntListener { 71 | void newNumber(int number); 72 | } 73 | 74 | // ------------------- Float ------------------- 75 | 76 | private OnNewFloatListener mFloatListener = null; 77 | 78 | public void initFloat(boolean allowZero, boolean allowNegative, float initial, OnNewFloatListener listener) { 79 | mAllowZero = allowZero; 80 | mAllowNegative = allowNegative; 81 | addTextChangedListener(new TextWatcher() { 82 | @Override 83 | public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { 84 | } 85 | 86 | @Override 87 | public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { 88 | } 89 | 90 | @Override 91 | public void afterTextChanged(Editable editable) { 92 | // empty = nothing 93 | if (editable.toString().isEmpty()) return; 94 | 95 | try { 96 | float value = NumberFormat.getInstance(Locale.US).parse(editable.toString()).floatValue(); 97 | if ((mAllowNegative && value < 0) || value > 0 || (mAllowZero && value == 0)) { 98 | // valid number 99 | setHint(String.format(Locale.US, "%s", value)); 100 | if (mFloatListener != null) mFloatListener.newNumber(value); 101 | } else { 102 | // invalid number 103 | setText(""); 104 | } 105 | } catch (ParseException | NullPointerException e) { 106 | Log.d("settings", "numberformatexception"); 107 | e.printStackTrace(); 108 | } 109 | } 110 | }); 111 | setValue(initial); 112 | mFloatListener = listener; 113 | } 114 | 115 | public void setValue(float value) { 116 | setText(String.format(Locale.US, "%s", value)); 117 | } 118 | 119 | public interface OnNewFloatListener { 120 | void newNumber(float number); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/com/trianguloy/continuousDataUsage/common/PeriodCalendar.java: -------------------------------------------------------------------------------- 1 | package com.trianguloy.continuousDataUsage.common; 2 | 3 | import android.util.Pair; 4 | 5 | import java.util.Calendar; 6 | 7 | /** 8 | * Calendar-related functions, manages a period 9 | */ 10 | public class PeriodCalendar { 11 | 12 | private final Preferences pref; 13 | 14 | /** 15 | * Constructor 16 | * 17 | * @param pref preferences to get info about saved period 18 | */ 19 | public PeriodCalendar(Preferences pref) { 20 | this.pref = pref; 21 | } 22 | 23 | /** 24 | * Returns the start and end millis of the specified period 25 | * 26 | * @param period 0 for current, -1 for previous, -2 for two previous... 27 | * @return start&end millis pair 28 | */ 29 | public Pair getLimitsOfPeriod(int period) { 30 | 31 | Calendar cal = getStartOfPeriod(period); 32 | 33 | // get start of the period 34 | long startOfPeriod = cal.getTimeInMillis(); 35 | 36 | // get end of period 37 | cal.add(pref.getPeriodType(), pref.getPeriodLength()); 38 | long endOfPeriod = cal.getTimeInMillis(); 39 | 40 | return new Pair<>(startOfPeriod, endOfPeriod); 41 | } 42 | 43 | /** 44 | * @return the current period based on the saved prefs. 45 | * Almost always 0 46 | * 1 (or more) when period changes 47 | * -1 or less when time travel 48 | */ 49 | public int getCurrentPeriod() { 50 | int period = 0; 51 | 52 | long now = System.currentTimeMillis(); 53 | 54 | while (true) { 55 | final Pair limits = getLimitsOfPeriod(period); 56 | if (limits.first <= now && now < limits.second) return period; 57 | if (now >= limits.second) period++; 58 | if (now < limits.first) period--; 59 | } 60 | } 61 | 62 | 63 | // ---------------- 64 | 65 | /** 66 | * @param period 0 for current, -1 for previous... 67 | * @return a calendar at the start of the specified period 68 | */ 69 | public Calendar getStartOfPeriod(int period) { 70 | Calendar cal = pref.getPeriodStart(); 71 | 72 | // goto period wanted 73 | while (period > 0) { 74 | // add length for future period 75 | cal.add(pref.getPeriodType(), pref.getPeriodLength()); 76 | period--; 77 | } 78 | while (period < 0) { 79 | // substract length for past (or current) period 80 | cal.add(pref.getPeriodType(), -pref.getPeriodLength()); 81 | period++; 82 | } 83 | 84 | return cal; 85 | } 86 | 87 | // ------------------- static ------------------- 88 | 89 | /** 90 | * @return today as a calendar 91 | */ 92 | public static Calendar today() { 93 | Calendar cal = Calendar.getInstance(); 94 | cal.set(Calendar.HOUR_OF_DAY, 0); 95 | cal.set(Calendar.MINUTE, 0); 96 | cal.set(Calendar.SECOND, 0); 97 | cal.set(Calendar.MILLISECOND, 0); 98 | return cal; 99 | } 100 | 101 | /** 102 | * @return A calendar from a @param year, @param month and @param day 103 | */ 104 | public static Calendar from(int year, int month, int day) { 105 | Calendar cal = today(); 106 | cal.set(Calendar.YEAR, year); 107 | cal.set(Calendar.MONTH, month); 108 | cal.set(Calendar.DAY_OF_MONTH, day); 109 | return cal; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/com/trianguloy/continuousDataUsage/common/Preferences.java: -------------------------------------------------------------------------------- 1 | package com.trianguloy.continuousDataUsage.common; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import java.util.Calendar; 7 | import java.util.Collections; 8 | import java.util.HashSet; 9 | import java.util.Set; 10 | 11 | /** 12 | * Utility class to manage the shared preferences 13 | */ 14 | 15 | public class Preferences { 16 | 17 | /** 18 | * SharedPrefs 19 | */ 20 | private static final String PREF_NAME = "pref"; 21 | private final SharedPreferences sharedPreferences; 22 | 23 | private static final String KEY_INV = "inverse"; 24 | 25 | 26 | /** 27 | * Total data (float > 0) 28 | */ 29 | private static final String KEY_TOTALDATA = "totalData"; 30 | private static final float DEFAULT_TOTALDATA = 1024; 31 | 32 | public float getTotalData() { 33 | return sharedPreferences.getFloat(KEY_TOTALDATA, DEFAULT_TOTALDATA); 34 | } 35 | 36 | public void setTotalData(float totalData) { 37 | sharedPreferences.edit().putFloat(KEY_TOTALDATA, totalData).apply(); 38 | } 39 | 40 | 41 | /** 42 | * Alternative conversion (boolean) 43 | */ 44 | private static final String KEY_ALTCONVERSION = "altConversion"; 45 | private static final boolean DEFAULT_ALTCONVERSION = false; 46 | 47 | public boolean getAltConversion() { 48 | return sharedPreferences.getBoolean(KEY_ALTCONVERSION, DEFAULT_ALTCONVERSION); 49 | } 50 | 51 | public void setAltConversion(boolean altConversion) { 52 | sharedPreferences.edit().putBoolean(KEY_ALTCONVERSION, altConversion).apply(); 53 | } 54 | 55 | 56 | /** 57 | * Start date for current period (inclusive) 58 | */ 59 | private static final String KEY_PERIODSTART = "periodStart"; 60 | 61 | public Calendar getPeriodStart() { 62 | Calendar cal; 63 | final long millis = sharedPreferences.getLong(KEY_PERIODSTART, -1); 64 | if (millis != -1) { 65 | cal = Calendar.getInstance(); 66 | cal.setTimeInMillis(millis); 67 | } else { 68 | // DEFAULT: first day of current month 69 | cal = PeriodCalendar.today(); 70 | cal.set(Calendar.DAY_OF_MONTH, 1); 71 | setPeriodStart(cal); // save, otherwise it will never be updated 72 | } 73 | return cal; 74 | } 75 | 76 | public void setPeriodStart(Calendar periodStart) { 77 | sharedPreferences.edit().putLong(KEY_PERIODSTART, periodStart.getTimeInMillis()).apply(); 78 | } 79 | 80 | 81 | /** 82 | * Length of current period (int) 83 | * See Period 84 | */ 85 | private static final String KEY_PERIODLENGTH = "periodLength"; 86 | private static final int DEFAULT_PERIODLENGTH = 1; 87 | 88 | public int getPeriodLength() { 89 | return sharedPreferences.getInt(KEY_PERIODLENGTH, DEFAULT_PERIODLENGTH); 90 | } 91 | 92 | public void setPeriodLength(int periodLength) { 93 | sharedPreferences.edit().putInt(KEY_PERIODLENGTH, periodLength).apply(); 94 | } 95 | 96 | /** 97 | * Type of current period (month or days) 98 | * See Length 99 | */ 100 | private static final String KEY_PERIODTYPE = "periodType"; 101 | private static final int DEFAULT_PERIODTYPE = Calendar.MONTH; 102 | 103 | public int getPeriodType() { 104 | return sharedPreferences.getInt(KEY_PERIODTYPE, DEFAULT_PERIODTYPE); 105 | } 106 | 107 | public void setPeriodType(int periodType) { 108 | sharedPreferences.edit().putInt(KEY_PERIODTYPE, periodType).apply(); 109 | } 110 | 111 | /** 112 | * Information requested flag. Cleared when read. 113 | */ 114 | private static final String KEY_INFOREQUESTED = "infoRequested"; 115 | 116 | public void infoRequested() { 117 | sharedPreferences.edit().putBoolean(KEY_INFOREQUESTED, true).apply(); 118 | } 119 | 120 | public boolean isInfoRequested() { 121 | boolean b = sharedPreferences.getBoolean(KEY_INFOREQUESTED, false); 122 | sharedPreferences.edit().remove(KEY_INFOREQUESTED).apply(); 123 | return b; 124 | } 125 | 126 | 127 | /** 128 | * Saved Periods (int) 129 | */ 130 | private static final String KEY_SAVEDPERIODS = "savedPeriods"; 131 | private static final int DEFAULT_SAVEDPERIODS = 0; 132 | 133 | public int getSavedPeriods() { 134 | return sharedPreferences.getInt(KEY_SAVEDPERIODS, DEFAULT_SAVEDPERIODS); 135 | } 136 | 137 | public void setSavedPeriods(int savedPeriods) { 138 | sharedPreferences.edit().putInt(KEY_SAVEDPERIODS, savedPeriods).apply(); 139 | } 140 | 141 | /** 142 | * Accumulated data of current period (float) 143 | */ 144 | private static final String KEY_ACCUMULATED = "accumulated"; 145 | private static final float DEFAULT_ACCUMULATED = 0; 146 | 147 | public Float getAccumulated() { 148 | return sharedPreferences.getFloat(KEY_ACCUMULATED, DEFAULT_ACCUMULATED); 149 | } 150 | 151 | public void setAccumulated(float accumulated) { 152 | sharedPreferences.edit().putFloat(KEY_ACCUMULATED, accumulated).apply(); 153 | } 154 | 155 | /** 156 | * Decimals (int) 157 | */ 158 | private static final String KEY_DECIMALS = "decimals"; 159 | private static final int DEFAULT_DECIMALS = 2; 160 | 161 | public int getDecimals() { 162 | return sharedPreferences.getInt(KEY_DECIMALS, DEFAULT_DECIMALS); 163 | } 164 | 165 | public void setDecimals(int decimals) { 166 | sharedPreferences.edit().putInt(KEY_DECIMALS, decimals).apply(); 167 | } 168 | 169 | /** 170 | * Gigabytes (boolean) 171 | */ 172 | private static final String KEY_GB = "gb"; 173 | private static final boolean DEFAULT_GB = false; 174 | 175 | public boolean getGB() { 176 | return sharedPreferences.getBoolean(KEY_GB, DEFAULT_GB); 177 | } 178 | 179 | public void setGB(boolean gb) { 180 | sharedPreferences.edit().putBoolean(KEY_GB, gb).apply(); 181 | } 182 | private static final boolean DEFAULT_INV = false; 183 | public Preferences(Context context) { 184 | this.sharedPreferences = context.getSharedPreferences(Preferences.PREF_NAME, Context.MODE_PRIVATE); 185 | 186 | // updates 187 | final String KEY_ACCUMULATE = "accumulate"; 188 | if (sharedPreferences.contains(KEY_ACCUMULATE)) { 189 | // accumulate=true/false -> savedPeriods=1/0 190 | setSavedPeriods(sharedPreferences.getBoolean(KEY_ACCUMULATE, false) ? 1 : 0); 191 | sharedPreferences.edit().remove(KEY_ACCUMULATE).apply(); 192 | } 193 | 194 | final String KEY_FIRSTDAY = "firstDay"; 195 | if (sharedPreferences.contains(KEY_FIRSTDAY)) { 196 | // firstDay=n -> periodStart=today(day=n) 197 | Calendar cal = PeriodCalendar.today(); 198 | cal.set(Calendar.DAY_OF_MONTH, sharedPreferences.getInt(KEY_FIRSTDAY, 0)); 199 | if (System.currentTimeMillis() < cal.getTimeInMillis()) { 200 | cal.add(Calendar.MONTH, -1); 201 | } 202 | setPeriodStart(cal); 203 | sharedPreferences.edit().remove(KEY_FIRSTDAY).apply(); 204 | } 205 | 206 | final String KEY_ACCUMULATEDM = "accumulatedm"; 207 | if (sharedPreferences.contains(KEY_ACCUMULATEDM)) { 208 | // accumulated month -> shift current month 209 | int diff = (getPeriodStart().get(Calendar.MONTH) - sharedPreferences.getInt(KEY_ACCUMULATEDM, 0) + 12) % 12; 210 | if (diff != 0) { 211 | Calendar cal = getPeriodStart(); 212 | cal.add(Calendar.MONTH, -diff); 213 | setPeriodStart(cal); 214 | } 215 | sharedPreferences.edit().remove(KEY_ACCUMULATEDM).apply(); 216 | } 217 | 218 | var KEY_REMAINING = "showRemaining"; 219 | if (sharedPreferences.contains(KEY_REMAINING)) { 220 | // show remaining tweak -> show inv 221 | setInv(sharedPreferences.getBoolean(KEY_REMAINING, false)); 222 | sharedPreferences.edit().remove(KEY_REMAINING).apply(); 223 | } 224 | 225 | // sharedPreferences.edit().clear().apply(); 226 | } 227 | 228 | public boolean getInv() { 229 | return sharedPreferences.getBoolean(KEY_INV, DEFAULT_INV); 230 | } 231 | 232 | public void setInv(boolean inv) { 233 | sharedPreferences.edit().putBoolean(KEY_INV, inv).apply(); 234 | } 235 | 236 | // ------------------- tweaks ------------------- 237 | 238 | /** 239 | * Tweaks 240 | */ 241 | private static final String KEY_TWEAKS = "tweaks"; 242 | private final Set DEFAULT_TWEAKS = Collections.emptySet(); 243 | 244 | public boolean getTweak(Tweaks.Tweak tweak) { 245 | return sharedPreferences.getBoolean(tweak.name(), false); 246 | } 247 | 248 | public void setTweak(Tweaks.Tweak tweak, boolean enabled) { 249 | sharedPreferences.edit().putBoolean(tweak.name(), enabled).apply(); 250 | } 251 | 252 | public void cleanupTweaks() { 253 | SharedPreferences.Editor edit = sharedPreferences.edit(); 254 | 255 | // remove old tweaks: 256 | for (String name : sharedPreferences.getStringSet(KEY_TWEAKS, DEFAULT_TWEAKS)) { 257 | // check every saved tweak 258 | try { 259 | // check if still exists 260 | Tweaks.Tweak.valueOf(name); 261 | } catch (IllegalArgumentException e) { 262 | // if not, remove 263 | edit.remove(name); 264 | } 265 | } 266 | 267 | // save current tweaks list: 268 | Tweaks.Tweak[] items = Tweaks.Tweak.values(); 269 | Set names = new HashSet<>(items.length); 270 | for (Tweaks.Tweak tweak : items) { 271 | // add current tweak name to list 272 | names.add(tweak.name()); 273 | } 274 | edit.putStringSet(KEY_TWEAKS, names); 275 | 276 | // save 277 | edit.apply(); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /app/src/main/java/com/trianguloy/continuousDataUsage/common/Tweaks.java: -------------------------------------------------------------------------------- 1 | package com.trianguloy.continuousDataUsage.common; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Context; 5 | import android.content.DialogInterface; 6 | import android.content.res.Resources; 7 | 8 | import com.trianguloy.continuousDataUsage.BuildConfig; 9 | import com.trianguloy.continuousDataUsage.R; 10 | 11 | /** 12 | * Manager of the tweaks 13 | */ 14 | public class Tweaks implements DialogInterface.OnMultiChoiceClickListener { 15 | 16 | /** 17 | * Prefix for the tweaks 18 | */ 19 | private static final String ID_PREFIX = "tweak_"; 20 | 21 | private final Preferences prefs; 22 | private final Context cntx; 23 | 24 | /** 25 | * Tweak items. 26 | * Note: names are used as keys in preferences and as ids in strings (with prefix) 27 | */ 28 | public enum Tweak { 29 | hideDate, 30 | hideData, 31 | hideBars, 32 | hideTexts, 33 | whiteWidgets, 34 | showConsumed, 35 | showAverage, 36 | advancedSecondary, 37 | capNoWarp, 38 | androidUsageButton, 39 | } 40 | 41 | // ------------------- Tweaks ------------------- 42 | 43 | /** 44 | * Main constructor 45 | * @param prefs base prefs 46 | * @param cntx base context 47 | */ 48 | public Tweaks(Preferences prefs, Context cntx) { 49 | this.prefs = prefs; 50 | this.cntx = cntx; 51 | } 52 | 53 | /** 54 | * Displays the dialog with the list of tweaks to toggle 55 | */ 56 | public void showDialog() { 57 | 58 | // get tweaks state 59 | Tweak[] tweaks = Tweak.values(); 60 | 61 | CharSequence[] items = new CharSequence[tweaks.length]; 62 | boolean[] checkItems = new boolean[tweaks.length]; 63 | 64 | for (int i = 0; i < tweaks.length; i++) { 65 | Tweak tweak = tweaks[i]; 66 | // initialize item and append 67 | items[i] = getItemDescr(tweak); 68 | checkItems[i] = prefs.getTweak(tweak); 69 | } 70 | 71 | // do cleanup 72 | prefs.cleanupTweaks(); 73 | 74 | // show 75 | new AlertDialog.Builder(cntx) 76 | .setMultiChoiceItems(items, checkItems, this) 77 | .setTitle(R.string.btn_tweaks) 78 | .setPositiveButton(cntx.getString(R.string.btn_close), null) 79 | .show(); 80 | 81 | } 82 | 83 | /** 84 | * Returns the description of a tweak 85 | * 86 | * @param item which tweak 87 | * @return the description as string 88 | */ 89 | private String getItemDescr(Tweak item) { 90 | try { 91 | return cntx.getString(cntx.getResources().getIdentifier(ID_PREFIX + item.name(), "string", cntx.getPackageName())); 92 | } catch (Resources.NotFoundException e) { 93 | if (BuildConfig.DEBUG) { 94 | throw e; // rethrow in debug mode 95 | } else { 96 | return "---" + item.name() + "---"; // placeholder in release mode 97 | } 98 | } 99 | } 100 | 101 | // ------------------- DialogInterface.OnMultiChoiceClickListener ------------------- 102 | 103 | @Override 104 | public void onClick(DialogInterface dialogInterface, int i, boolean b) { 105 | prefs.setTweak(Tweak.values()[i], b); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/com/trianguloy/continuousDataUsage/common/Utils.java: -------------------------------------------------------------------------------- 1 | package com.trianguloy.continuousDataUsage.common; 2 | 3 | import android.appwidget.AppWidgetManager; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | 8 | import com.trianguloy.continuousDataUsage.widgets.AppWidgetProgress; 9 | import com.trianguloy.continuousDataUsage.widgets.AppWidgetRate; 10 | 11 | import java.text.NumberFormat; 12 | import java.util.Locale; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | 16 | public class Utils { 17 | 18 | /** 19 | * Rounds double to int 20 | * 21 | * @param d double value 22 | * @return round(d) as int 23 | */ 24 | public static int dbl2int(double d) { 25 | return Math.round(Math.round(d)); 26 | } 27 | 28 | // ------------------- Format ------------------- 29 | 30 | /** 31 | * for {@link #formatData(Preferences, String, Double...)} 32 | */ 33 | private static final Pattern pattern = Pattern.compile("\\{(.)\\}"); 34 | 35 | /** 36 | * Custom formatter based on the preferences. 37 | *

38 | * Uses getDecimals to set the number of decimals for data numbers. 39 | * Uses getGB to set the suffix and convert the argument. 40 | * Uses getAltConversion for the GB conversion. 41 | *

42 | *

  • '{0}' data without suffix -> '1.123'
  • 43 | *
  • '{M}' data with suffix -> '1.123 MB'
  • 44 | *
  • '{%}' percentage -> '1.23%'
  • 45 | *
  • '{/}' ratio -> '1.23'
  • 46 | * 47 | * @param pref preferences 48 | * @param format format string, see above for formatting options 49 | * @param args values 50 | * @return formatted string 51 | */ 52 | public static String formatData(Preferences pref, String format, Double... args) { 53 | // init elements 54 | NumberFormat formatter = NumberFormat.getNumberInstance(Locale.US); 55 | StringBuffer formatted = new StringBuffer(format.length()); 56 | final Matcher matcher = pattern.matcher(format); 57 | int i = 0; 58 | 59 | // replace each formatting element 60 | while (matcher.find()) { 61 | 62 | // set replacing properties 63 | String suffix = ""; 64 | switch (matcher.group(1)) { 65 | case "M": 66 | // data with suffix 67 | suffix = (pref.getGB() ? " GB" : " MB"); 68 | case "0": 69 | // data without suffix 70 | formatter.setMinimumFractionDigits(pref.getDecimals()); 71 | formatter.setMaximumFractionDigits(pref.getDecimals()); 72 | if (pref.getGB()) args[i] /= pref.getAltConversion() ? 1000 : 1024; 73 | break; 74 | case "%": 75 | // percentage 76 | suffix = "%"; 77 | case "/": 78 | // ratio 79 | formatter.setMinimumFractionDigits(2); 80 | formatter.setMaximumFractionDigits(2); 81 | break; 82 | default: 83 | throw new IllegalArgumentException(matcher.group()); 84 | } 85 | 86 | // replace 87 | matcher.appendReplacement(formatted, 88 | Matcher.quoteReplacement(formatter.format(args[i]) + suffix) 89 | ); 90 | i++; 91 | } 92 | matcher.appendTail(formatted); 93 | 94 | // return 95 | return formatted.toString(); 96 | } 97 | 98 | /** 99 | * Update all widgets now 100 | */ 101 | public static void updateAllWidgets(Context context) { 102 | AppWidgetManager man = AppWidgetManager.getInstance(context); 103 | for (Class cls : new Class[]{AppWidgetProgress.class, AppWidgetRate.class}) { 104 | Intent updateIntent = new Intent(context, cls); 105 | updateIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); 106 | updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, man.getAppWidgetIds(new ComponentName(context, cls))); 107 | context.sendBroadcast(updateIntent); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/trianguloy/continuousDataUsage/widgets/AppWidgetBase.java: -------------------------------------------------------------------------------- 1 | package com.trianguloy.continuousDataUsage.widgets; 2 | 3 | import android.app.PendingIntent; 4 | import android.appwidget.AppWidgetManager; 5 | import android.appwidget.AppWidgetProvider; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.util.Log; 9 | import android.util.Pair; 10 | import android.widget.RemoteViews; 11 | import android.widget.Toast; 12 | 13 | import com.trianguloy.continuousDataUsage.R; 14 | import com.trianguloy.continuousDataUsage.activities.HistoryActivity; 15 | import com.trianguloy.continuousDataUsage.common.Accumulated; 16 | import com.trianguloy.continuousDataUsage.common.DataUsage; 17 | import com.trianguloy.continuousDataUsage.common.PeriodCalendar; 18 | import com.trianguloy.continuousDataUsage.common.Preferences; 19 | import com.trianguloy.continuousDataUsage.common.Tweaks; 20 | 21 | import java.text.SimpleDateFormat; 22 | import java.util.Calendar; 23 | 24 | /** 25 | * Implementation of App Widget functionality. Base class. 26 | */ 27 | abstract class AppWidgetBase extends AppWidgetProvider { 28 | 29 | //abstract 30 | abstract void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId); 31 | 32 | private static final String EXTRA_ACTION = "com.trianguloy.continuousDataUsage.action"; 33 | static final int ACTION_REFRESH = 0; 34 | static final int ACTION_USAGE = 1; 35 | static final int ACTION_INFO = 2; 36 | 37 | 38 | /** 39 | * When a widget is created for the first time. Nothing done. 40 | * 41 | * @param context base context 42 | */ 43 | @Override 44 | public void onEnabled(Context context) { 45 | } 46 | 47 | 48 | /** 49 | * When the last widget is removed. Nothing done 50 | * 51 | * @param context base context 52 | */ 53 | @Override 54 | public void onDisabled(Context context) { 55 | } 56 | 57 | 58 | /** 59 | * Called when one or more widgets needs to be updated. Updates all of them 60 | * 61 | * @param context the base context 62 | * @param appWidgetManager the widget manager 63 | * @param appWidgetIds the ids of the widgets that needs to be updated 64 | */ 65 | @Override 66 | public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { 67 | for (int appWidgetId : appWidgetIds) { 68 | updateAppWidget(context, appWidgetManager, appWidgetId); 69 | } 70 | } 71 | 72 | 73 | /** 74 | * Intent received, act accordingly. 75 | * 76 | * @param context that magical class 77 | * @param intent the intent 78 | */ 79 | @Override 80 | public void onReceive(Context context, Intent intent) { 81 | var prefs = new Preferences(context); 82 | 83 | switch (intent.getIntExtra(EXTRA_ACTION, -1)) { 84 | case ACTION_USAGE: 85 | if (prefs.getTweak(Tweaks.Tweak.androidUsageButton)) { 86 | HistoryActivity.openAndroidUsageScreen(context); 87 | } else { 88 | //open the history activity 89 | Intent usage = new Intent(context, HistoryActivity.class); 90 | usage.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 91 | context.startActivity(usage); 92 | } 93 | break; 94 | case ACTION_INFO: 95 | //info requested, set flag 96 | prefs.infoRequested(); 97 | default: 98 | super.onReceive(context, intent); 99 | break; 100 | } 101 | } 102 | 103 | /** 104 | * To return a bundle with multiple values in the #getCommonInfo function 105 | */ 106 | static class ReturnedInfo { 107 | double percentDate = 0; 108 | double totalData = 0; 109 | 110 | int error = -1; 111 | 112 | double percentData = 0; 113 | double megabytes = 0; 114 | } 115 | 116 | /** 117 | * The core of the widgets, calculates the necessary data 118 | * 119 | * @param context not to be confused with contest 120 | * @return calculated data in a packed class 121 | */ 122 | static ReturnedInfo getCommonInfo(Context context) { 123 | 124 | ReturnedInfo returnedInfo = new ReturnedInfo(); 125 | 126 | // get objects 127 | Preferences pref = new Preferences(context); 128 | PeriodCalendar periodCalendar = new PeriodCalendar(pref); 129 | DataUsage dataUsage = new DataUsage(context, pref); 130 | Accumulated accumulated = new Accumulated(pref, dataUsage, periodCalendar); 131 | 132 | // update 133 | accumulated.updatePeriod(); 134 | 135 | boolean infoRequested = pref.isInfoRequested(); 136 | 137 | //current time 138 | long currentMillis = System.currentTimeMillis(); 139 | 140 | //current period 141 | Pair val = periodCalendar.getLimitsOfPeriod(0); 142 | long startOfPeriod = val.first; 143 | long endOfPeriod = val.second; 144 | 145 | 146 | //upper bar 147 | returnedInfo.percentDate = (currentMillis - startOfPeriod) / (double) (endOfPeriod - startOfPeriod); 148 | double totalData = pref.getTotalData(); 149 | returnedInfo.totalData = totalData; 150 | 151 | //bottom bar 152 | double percentData; 153 | double megabytes; 154 | 155 | try { 156 | megabytes = accumulated.getUsedDataFromCurrentPeriod(); 157 | 158 | } catch (DataUsage.Error e) { 159 | returnedInfo.error = e.errorId; 160 | return returnedInfo; 161 | } 162 | 163 | returnedInfo.megabytes = megabytes; 164 | percentData = megabytes / totalData; 165 | returnedInfo.percentData = percentData; 166 | 167 | 168 | //current usage as date 169 | if (infoRequested) { 170 | double millisEquivalent = (endOfPeriod - startOfPeriod) * percentData + startOfPeriod; 171 | Calendar cal = Calendar.getInstance(); 172 | cal.clear(); 173 | cal.setTimeInMillis(Math.round(millisEquivalent)); 174 | Toast.makeText(context, context.getString(R.string.toast_currentUsage, 175 | SimpleDateFormat.getDateTimeInstance().format(cal.getTime()), 176 | millisToInterval(cal.getTimeInMillis() - currentMillis)), Toast.LENGTH_LONG).show(); 177 | } 178 | 179 | // invert data 180 | if (pref.getInv()) { 181 | returnedInfo.percentData = 1 - returnedInfo.percentData; 182 | returnedInfo.megabytes = returnedInfo.totalData - returnedInfo.megabytes; 183 | returnedInfo.percentDate = 1 - returnedInfo.percentDate; 184 | } 185 | 186 | Log.d("Widget", "updated"); 187 | 188 | return returnedInfo; 189 | } 190 | 191 | 192 | //--------------utils------------------- 193 | 194 | 195 | /** 196 | * Puts the updateWidget click event on the specific views 197 | * 198 | * @param context base context 199 | * @param appWidgetIds ids to update 200 | * @param remoteViews where to find the views 201 | * @param views views to set 202 | * @param action intent action 203 | * @param classForIntent class for the intent 204 | */ 205 | static void setOnClick(Context context, int[] appWidgetIds, RemoteViews remoteViews, int[] views, int action, Class classForIntent) { 206 | Intent intent = new Intent(context, classForIntent); 207 | intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); 208 | intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds); 209 | intent.putExtra(EXTRA_ACTION, action); 210 | PendingIntent pendingIntent = PendingIntent.getBroadcast(context, action, intent, PendingIntent.FLAG_IMMUTABLE); 211 | for (int view : views) { 212 | remoteViews.setOnClickPendingIntent(view, pendingIntent); 213 | } 214 | } 215 | 216 | /** 217 | * Converts millis to a string display 218 | * 219 | * @param millis mililiseconds of a specific time 220 | * @return the time as string 221 | */ 222 | static String millisToInterval(long millis) { 223 | StringBuilder sb = new StringBuilder(); 224 | String prefix = millis >= 0 ? "+ " : "- "; 225 | millis = millis > 0 ? millis : -millis; 226 | 227 | millis /= 1000; 228 | 229 | sb.insert(0, " seconds"); 230 | sb.insert(0, millis % 60); 231 | millis /= 60; 232 | 233 | if (millis > 0) { 234 | sb.insert(0, " minutes, "); 235 | sb.insert(0, millis % 60); 236 | millis /= 60; 237 | } 238 | 239 | if (millis > 0) { 240 | sb.insert(0, " hours, "); 241 | sb.insert(0, millis % 24); 242 | millis /= 24; 243 | } 244 | 245 | if (millis > 0) { 246 | sb.insert(0, " days, "); 247 | sb.insert(0, millis); 248 | } 249 | 250 | sb.insert(0, prefix); 251 | 252 | return sb.toString(); 253 | } 254 | 255 | 256 | } 257 | -------------------------------------------------------------------------------- /app/src/main/java/com/trianguloy/continuousDataUsage/widgets/AppWidgetProgress.java: -------------------------------------------------------------------------------- 1 | package com.trianguloy.continuousDataUsage.widgets; 2 | 3 | import android.appwidget.AppWidgetManager; 4 | import android.content.Context; 5 | import android.graphics.Color; 6 | import android.graphics.Point; 7 | import android.os.Bundle; 8 | import android.view.View; 9 | import android.view.WindowManager; 10 | import android.widget.RemoteViews; 11 | 12 | import com.trianguloy.continuousDataUsage.R; 13 | import com.trianguloy.continuousDataUsage.common.Preferences; 14 | import com.trianguloy.continuousDataUsage.common.Tweaks; 15 | import com.trianguloy.continuousDataUsage.common.Utils; 16 | 17 | /** 18 | * Implementation of the Progress Widget functionality. 19 | * Displays the values and two progress bar corresponding to the 'average' and 'current' usage. 20 | */ 21 | public class AppWidgetProgress extends AppWidgetBase { 22 | 23 | /** 24 | * When the size changes, update the widget 25 | * 26 | * @param context base context 27 | * @param appWidgetManager widget manager, base class 28 | * @param appWidgetId id of the widget to update 29 | * @param newOptions unused 30 | */ 31 | @Override 32 | public void onAppWidgetOptionsChanged(Context context, 33 | AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { 34 | 35 | updateAppWidget(context, appWidgetManager, appWidgetId); 36 | super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); 37 | } 38 | 39 | /** 40 | * Updates a widget adding its views and configuring them. 41 | * 42 | * @param context base context 43 | * @param appWidgetManager widget manager, base class 44 | * @param appWidgetId id of the widget to update 45 | */ 46 | void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { 47 | 48 | boolean small = appWidgetManager.getAppWidgetOptions(appWidgetId).getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) < 180; 49 | 50 | // Construct the RemoteViews object 51 | RemoteViews views = new RemoteViews(context.getPackageName(), small ? R.layout.widget_progress_short : R.layout.widget_progress); 52 | updateViews(context, views); 53 | 54 | //update when clicking 55 | setOnClick(context, new int[]{appWidgetId}, views, new int[]{R.id.wdg_prgBar_data, R.id.wdg_prgBar_date}, ACTION_REFRESH, AppWidgetProgress.class); 56 | setOnClick(context, new int[]{appWidgetId}, views, new int[]{R.id.btn_showData}, ACTION_USAGE, AppWidgetProgress.class); 57 | setOnClick(context, new int[]{appWidgetId}, views, new int[]{R.id.wdg_txt_data, R.id.wdg_txt_date}, ACTION_INFO, AppWidgetProgress.class); 58 | 59 | 60 | // Instruct the widget manager to update the widget 61 | appWidgetManager.updateAppWidget(appWidgetId, views); 62 | } 63 | 64 | /** 65 | * Updates the views of a widget 66 | * 67 | * @param context base context 68 | * @param views views to update 69 | */ 70 | public static void updateViews(Context context, RemoteViews views) { 71 | ReturnedInfo commonInfo = getCommonInfo(context); 72 | Preferences pref = new Preferences(context); 73 | 74 | //variables 75 | WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 76 | Point p = new Point(context.getResources().getInteger(R.integer.DEFAULT_PROGRESS_PRECISION), 0); 77 | if (wm != null) { 78 | wm.getDefaultDisplay().getSize(p); 79 | } 80 | int progressPrecision = p.x; 81 | 82 | //top bar 83 | views.setProgressBar(R.id.wdg_prgBar_date, progressPrecision, Utils.dbl2int(commonInfo.percentDate * progressPrecision), false); 84 | views.setTextViewText(R.id.wdg_txt_date, 85 | Utils.formatData(pref, "{M} ({%})", commonInfo.percentDate * commonInfo.totalData, commonInfo.percentDate * 100) 86 | ); 87 | 88 | //error 89 | if (commonInfo.error != -1) { 90 | views.setTextViewText(R.id.wdg_txt_data, context.getString(commonInfo.error)); 91 | return; 92 | } 93 | 94 | // bottom bar 95 | views.setProgressBar(R.id.wdg_prgBar_data, progressPrecision, Utils.dbl2int((commonInfo.percentData % 1) * progressPrecision), false); 96 | views.setInt(R.id.wdg_prgBar_data, "setSecondaryProgress", 97 | commonInfo.percentData > 1 ? progressPrecision 98 | : commonInfo.percentData < 0 ? Utils.dbl2int((1 + (commonInfo.percentData % 1)) * progressPrecision) 99 | : 0); 100 | views.setTextViewText(R.id.wdg_txt_data, 101 | Utils.formatData(pref, "{M} ({%})", commonInfo.megabytes, commonInfo.percentData * 100) 102 | ); 103 | 104 | // tweaks 105 | if (pref.getTweak(Tweaks.Tweak.hideDate)) { 106 | views.setViewVisibility(R.id.wdg_txt_date, View.GONE); 107 | views.setViewVisibility(R.id.wdg_prgBar_date, View.GONE); 108 | } 109 | if (pref.getTweak(Tweaks.Tweak.hideData)) { 110 | views.setViewVisibility(R.id.wdg_txt_data, View.GONE); 111 | views.setViewVisibility(R.id.wdg_prgBar_data, View.GONE); 112 | } 113 | if (pref.getTweak(Tweaks.Tweak.hideBars)) { 114 | views.setViewVisibility(R.id.wdg_prgBar_data, View.GONE); 115 | views.setViewVisibility(R.id.wdg_prgBar_date, View.GONE); 116 | } 117 | if (pref.getTweak(Tweaks.Tweak.hideTexts)) { 118 | views.setViewVisibility(R.id.wdg_txt_data, View.GONE); 119 | views.setViewVisibility(R.id.wdg_txt_date, View.GONE); 120 | } 121 | if (pref.getTweak(Tweaks.Tweak.advancedSecondary)) { 122 | final int sp = pref.getSavedPeriods(); 123 | double percent; // this is easier with Kotlin :( 124 | if (commonInfo.percentData >= 1) 125 | // [1,oo) = more than the current usage -> show full 126 | percent = 1; 127 | else if (commonInfo.percentData >= 0) 128 | // [0,1) = normal usage -> don't show 129 | percent = 0; 130 | else if (commonInfo.percentData >= -sp + 1) 131 | // [-sp+1,0) = saved data -> % in range 132 | percent = (commonInfo.percentData + sp - 1) / (sp - 1); 133 | else if (commonInfo.percentData >= -sp) 134 | // [-sp,-sp+1) = will be lost -> % in range 135 | percent = commonInfo.percentData + sp; 136 | else 137 | // (-oo,-sp) = more than one period will be lost -> show nothing 138 | percent = 0; 139 | views.setInt(R.id.wdg_prgBar_data, "setSecondaryProgress", Utils.dbl2int(percent * progressPrecision)); 140 | } 141 | if (pref.getTweak(Tweaks.Tweak.capNoWarp)) { 142 | views.setProgressBar(R.id.wdg_prgBar_data, progressPrecision, Utils.dbl2int((commonInfo.percentData) * progressPrecision), false); 143 | views.setInt(R.id.wdg_prgBar_data, "setSecondaryProgress", 0); 144 | } 145 | if (pref.getTweak(Tweaks.Tweak.whiteWidgets)) { 146 | views.setInt(R.id.wdg_parent, "setBackgroundResource", R.drawable.background_progress_white); 147 | views.setTextColor(R.id.wdg_txt_date, Color.BLACK); 148 | views.setTextColor(R.id.wdg_txt_data, Color.BLACK); 149 | views.setInt(R.id.btn_showData, "setImageResource", R.drawable.ic_history_black); 150 | } 151 | 152 | } 153 | } 154 | 155 | -------------------------------------------------------------------------------- /app/src/main/java/com/trianguloy/continuousDataUsage/widgets/AppWidgetRate.java: -------------------------------------------------------------------------------- 1 | package com.trianguloy.continuousDataUsage.widgets; 2 | 3 | import android.appwidget.AppWidgetManager; 4 | import android.content.Context; 5 | import android.graphics.Color; 6 | import android.widget.RemoteViews; 7 | 8 | import com.trianguloy.continuousDataUsage.R; 9 | import com.trianguloy.continuousDataUsage.common.Preferences; 10 | import com.trianguloy.continuousDataUsage.common.Tweaks; 11 | import com.trianguloy.continuousDataUsage.common.Utils; 12 | 13 | /** 14 | * Implementation of the Rate widget functionality. 15 | * Displays a number with the rate between used_data / average_data 16 | */ 17 | public class AppWidgetRate extends AppWidgetBase { 18 | 19 | 20 | /** 21 | * Updates a widget adding its views and configuring them. 22 | * 23 | * @param context base context 24 | * @param appWidgetManager widget manager, base class 25 | * @param appWidgetId id of the widget to update 26 | */ 27 | void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { 28 | 29 | // Construct the RemoteViews object 30 | RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_rate); 31 | updateViews(context, views); 32 | 33 | //update when clicking 34 | setOnClick(context, new int[]{appWidgetId}, views, new int[]{R.id.wdg_prgBar_data, R.id.wdg_txt_rate}, ACTION_INFO, AppWidgetRate.class); 35 | 36 | 37 | // Instruct the widget manager to update the widget 38 | appWidgetManager.updateAppWidget(appWidgetId, views); 39 | } 40 | 41 | /** 42 | * Updates the views of a widget 43 | * 44 | * @param context base context 45 | * @param views views to update 46 | */ 47 | void updateViews(Context context, RemoteViews views) { 48 | ReturnedInfo commonInfo = getCommonInfo(context); 49 | Preferences pref = new Preferences(context); 50 | 51 | if (commonInfo.error != -1) { 52 | views.setTextViewText(R.id.wdg_txt_rate, context.getString(commonInfo.error)); 53 | return; 54 | } 55 | 56 | //number 57 | double rate = commonInfo.percentData / commonInfo.percentDate; 58 | views.setTextViewText(R.id.wdg_txt_rate, Utils.formatData(pref, "{/}", rate)); 59 | 60 | // tweaks 61 | if (pref.getTweak(Tweaks.Tweak.showConsumed)) { 62 | rate = commonInfo.megabytes; 63 | views.setTextViewText(R.id.wdg_txt_rate, Utils.formatData(pref, "{0}", rate)); 64 | } 65 | if (pref.getTweak(Tweaks.Tweak.showAverage)) { 66 | rate = commonInfo.totalData; 67 | views.setTextViewText(R.id.wdg_txt_rate, Utils.formatData(pref, "{0}", rate)); 68 | } 69 | if (pref.getTweak(Tweaks.Tweak.whiteWidgets)) { 70 | views.setInt(R.id.wdg_parent, "setBackgroundResource", R.drawable.background_rate_white); 71 | views.setTextColor(R.id.wdg_txt_rate, Color.BLACK); 72 | } 73 | 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /app/src/main/play/contact-email.txt: -------------------------------------------------------------------------------- 1 | correo--correo+appCDUW@hotmail.com -------------------------------------------------------------------------------- /app/src/main/play/contact-website.txt: -------------------------------------------------------------------------------- 1 | https://triangularapps.blogspot.com/ -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/full-description.txt: -------------------------------------------------------------------------------- 1 | New: You can now configure the start and duration of the period, for example one week, 28 days, or even one year. 2 | 3 | 4 | 5 | Do you have an almost unlimited data plan and you never consume all your data? Lucky you! Unfortunately this app will be useless in this situation. 6 | 7 | On the other hand: do you have a limited data plan and it has happened to you: 8 | a) You always spend too much data on the first days of the period, and you have few left at the end? 9 | or 10 | b) You try not to spend too much data at the beginning of the period, and then you end with unused data? 11 | or 12 | c) You always wanted to know 'Did I spent too much already?' 'Am I above an average usage?'. 13 | 14 | Then this app will (I hope) help you! 15 | It shows your data usage (bottom bar, how much you already used) with an ideal 'average data usage' (top bar, how much you would have used by downloading the same amount of bytes every second in the period). This way with just one look you can check if you are above or below the 'average data usage'. 16 | - If the top bar is longer than the bottom: Good! You can download a bit more and still have at the end of the period. 17 | - If the top bar is shorter than the bottom: Not good! You need to stop using too much data, otherwise you'll end with no more left. 18 | 19 | Isn't this useful? I think it is, and that's why I (TrianguloY) published it. It doesn't contains ads, and it is absurdly lightweight, so give it a try. 20 | If you have any suggestion or comment leave one or send an email. 21 | 22 | DISCLAIMER!!!! 23 | Please note that the current consumption is measured by your device and may differ with your company measurement. I can't take responsibility if the displayed data usage is wrong. 24 | 25 | 26 | Permissions: 27 | - PACKAGE_USAGE_STATS - Permission needed to get the current usage from the usage service. No other data is retrieved nor used. 28 | More info here: https://developer.android.com/reference/android/app/usage/NetworkStatsManager.html#querySummaryForDevice(int,%20java.lang.String,%20long,%20long) 29 | - POST_NOTIFICATIONS - Permission needed to show a small message at the bottom of the screen (toast) when pressing the widget. The app does not send any standard notification. 30 | 31 | NOTE: there is no internet permission, there are no ads so it is not necessary. 32 | 33 | --------------------------------- 34 | The source code is available here: https://github.com/TrianguloY/Average-data-usage-widget -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/graphics/feature-graphic/featured.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrianguloY/Average-data-usage-widget/2d3a7657887687cc13ed43dfd7c42e3a7eb55670/app/src/main/play/listings/en-US/graphics/feature-graphic/featured.png -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/graphics/feature-graphic/featured.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrianguloY/Average-data-usage-widget/2d3a7657887687cc13ed43dfd7c42e3a7eb55670/app/src/main/play/listings/en-US/graphics/feature-graphic/featured.xcf -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/graphics/icon/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrianguloY/Average-data-usage-widget/2d3a7657887687cc13ed43dfd7c42e3a7eb55670/app/src/main/play/listings/en-US/graphics/icon/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/graphics/phone-screenshots/Screenshot_1601111957.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrianguloY/Average-data-usage-widget/2d3a7657887687cc13ed43dfd7c42e3a7eb55670/app/src/main/play/listings/en-US/graphics/phone-screenshots/Screenshot_1601111957.png -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/graphics/phone-screenshots/Screenshot_1601111968.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrianguloY/Average-data-usage-widget/2d3a7657887687cc13ed43dfd7c42e3a7eb55670/app/src/main/play/listings/en-US/graphics/phone-screenshots/Screenshot_1601111968.png -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/graphics/phone-screenshots/Screenshot_1601112040.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrianguloY/Average-data-usage-widget/2d3a7657887687cc13ed43dfd7c42e3a7eb55670/app/src/main/play/listings/en-US/graphics/phone-screenshots/Screenshot_1601112040.png -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/graphics/phone-screenshots/Screenshot_1601112106.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrianguloY/Average-data-usage-widget/2d3a7657887687cc13ed43dfd7c42e3a7eb55670/app/src/main/play/listings/en-US/graphics/phone-screenshots/Screenshot_1601112106.png -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/short-description.txt: -------------------------------------------------------------------------------- 1 | Check your data usage and compare it with an indicator of the average usage. -------------------------------------------------------------------------------- /app/src/main/play/listings/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Average data usage widget -------------------------------------------------------------------------------- /app/src/main/play/release-notes/en-US/default.txt: -------------------------------------------------------------------------------- 1 | V 4.1 2 | - Added Russian translation. Thank you kojjii! 3 | 4 | V 4.0 5 | - Updated to Android 10+ 6 | - New: Average and total data on the history screen 7 | - Tweak: Remaining tweak promoted to full setting (Pending/Used) 8 | - New: Option to calculate accumulated data by setting the desired visible amount 9 | - Improve: Accumulated data can be negative 10 | - Improve: Accumulated data can be set while accumulated period is 0 (as offset data) 11 | - New: Tweak: open android settings when clicking the widget button 12 | 13 | V 3.0 14 | - Configurable period length (any number of days or months). 15 | - New tweak: advanced secondary bars 16 | - Improved settings screen -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/preview_progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrianguloY/Average-data-usage-widget/2d3a7657887687cc13ed43dfd7c42e3a7eb55670/app/src/main/res/drawable-nodpi/preview_progress.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/preview_rate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrianguloY/Average-data-usage-widget/2d3a7657887687cc13ed43dfd7c42e3a7eb55670/app/src/main/res/drawable-nodpi/preview_rate.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_progress.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_progress_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_rate.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/background_rate_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_data_usage_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_history.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_history_black.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_history.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 |