diff --git a/app/build.gradle b/app/build.gradle index 6b21fb73fbf..e6065528fd8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId 'io.github.muntashirakon.AppManager' minSdkVersion 21 targetSdkVersion 29 - versionCode 85 - versionName '2.2.4' + versionCode 87 + versionName '2.3.0' } buildTypes { release { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 03111384b4b..3e992d4e270 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,18 +1,22 @@ - - + package="io.github.muntashirakon.AppManager"> + + + tools:replace="android:icon"> + parent, View view, int position, long id) { + current_interval = position; + getAppUsage(); + } + + @Override + public void onNothingSelected(AdapterView parent) {} + }); + } + + @Override + protected void onStart() { + super.onStart(); + // Check permission + if (!checkUsageStatsPermission()) promptForUsageStatsPermission(); + else getAppUsage(); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + } + return super.onOptionsItemSelected(item); + } + + private void getAppUsage() { + Calendar cal = Calendar.getInstance(); + switch (current_interval) { + case USAGE_DAILY: + cal.add(Calendar.MINUTE, -1); + break; + case USAGE_WEEKLY: + cal.add(Calendar.DAY_OF_YEAR, -7); + break; + case USAGE_MONTHLY: + cal.add(Calendar.MONTH, -1); + break; + case USAGE_YEARLY: + cal.add(Calendar.YEAR, -1); + break; + } + + int _try = 5; // try to get usage stat 5 times + List usageStatsList; + do { + usageStatsList = mUsageStatsManager.queryUsageStats(current_interval, + cal.getTimeInMillis(), System.currentTimeMillis()); + } while (0 != --_try && usageStatsList.size() == 0); + + // Filter unused apps + totalTimeInMs = 0; + for (int i = usageStatsList.size() - 1; i >= 0; i--) { + UsageStats usageStats = usageStatsList.get(i); + totalTimeInMs += usageStats.getTotalTimeInForeground(); + if (usageStats.getTotalTimeInForeground() <= 0) + usageStatsList.remove(i); + } + + Collections.sort(usageStatsList, new TimeInForegroundComparatorDesc()); + mAppUsageAdapter.setDefaultList(usageStatsList); + setUsageSummary(); + } + + private void promptForUsageStatsPermission() { + new AlertDialog.Builder(this) + .setTitle(R.string.grant_usage_access) + .setMessage(R.string.grant_usage_acess_message) + .setPositiveButton(R.string.go, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + startActivityForResult(new Intent( + Settings.ACTION_USAGE_ACCESS_SETTINGS), REQUEST_SETTINGS); + } + }) + .setNegativeButton(getString(R.string.go_back), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + finish(); + } + }).setCancelable(false).show(); + } + + @SuppressLint("InlinedApi") + private boolean checkUsageStatsPermission() { + AppOpsManager appOpsManager = (AppOpsManager) getSystemService(APP_OPS_SERVICE); + assert appOpsManager != null; + final int mode = appOpsManager.unsafeCheckOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, android.os.Process.myUid(), getPackageName()); + return mode == AppOpsManager.MODE_DEFAULT ? + (checkCallingOrSelfPermission(Manifest.permission.PACKAGE_USAGE_STATS) == PackageManager.PERMISSION_GRANTED) + : (mode == AppOpsManager.MODE_ALLOWED); + } + + private void setUsageSummary() { + TextView timeUsed = findViewById(R.id.time_used); + TextView timeRange = findViewById(R.id.time_range); + timeUsed.setText(formattedTime(this, totalTimeInMs)); + switch (current_interval) { + case USAGE_DAILY: + timeRange.setText(R.string.usage_today); + break; + case USAGE_WEEKLY: + timeRange.setText(R.string.usage_7_days); + break; + case USAGE_MONTHLY: + timeRange.setText(R.string.usage_30_days); + break; + case USAGE_YEARLY: + timeRange.setText(R.string.usage_365_days); + break; + } + } + + private static String formattedTime(Activity activity, long time) { + time /= 60000; // minutes + long month, day, hour, min; + month = time / 43200; time %= 43200; + day = time / 1440; time %= 1440; + hour = time / 60; + min = time % 60; + String fTime = ""; + int count = 0; + if (month != 0){ + fTime += String.format(activity.getString(month > 0 ? R.string.usage_months : R.string.usage_month), month); + ++count; + } + if (day != 0) { + fTime += (count > 0 ? " " : "") + String.format(activity.getString( + day > 1 ? R.string.usage_days : R.string.usage_day), day); + ++count; + } + if (hour != 0) { + fTime += (count > 0 ? " " : "") + String.format(activity.getString(R.string.usage_hour), hour); + ++count; + } + if (min != 0) { + fTime += (count > 0 ? " " : "") + String.format(activity.getString(R.string.usage_min), min); + } else { + if (count == 0) fTime = activity.getString(R.string.usage_less_than_a_minute); + } + return fTime; + } + + private static class TimeInForegroundComparatorDesc implements Comparator { + + @Override + public int compare(UsageStats left, UsageStats right) { + return Long.compare(right.getTotalTimeInForeground(), left.getTotalTimeInForeground()); + } + } + + static class AppUsageAdapter extends BaseAdapter { + static DateFormat sSimpleDateFormat = new SimpleDateFormat("MMM d, yyyy HH:mm:ss", Locale.getDefault()); + + private LayoutInflater mLayoutInflater; + private List mAdapterList; + private static PackageManager mPackageManager; + private Activity mActivity; + + static class ViewHolder { + ImageView icon; + TextView label; + TextView usage; + IconAsyncTask iconLoader; + } + + AppUsageAdapter(@NonNull Activity activity) { + mLayoutInflater = activity.getLayoutInflater(); + mPackageManager = activity.getPackageManager(); + mActivity = activity; + } + + void setDefaultList(List list) { +// mDefaultList = list; + mAdapterList = list; + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return mAdapterList == null ? 0 : mAdapterList.size(); + } + + @Override + public Object getItem(int position) { + return mAdapterList.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + if (convertView == null) { + convertView = mLayoutInflater.inflate(R.layout.item_icon_title_subtitle, parent, false); + holder = new ViewHolder(); + holder.icon = convertView.findViewById(R.id.item_icon); + holder.label = convertView.findViewById(R.id.item_title); + holder.usage = convertView.findViewById(R.id.item_subtitle); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + if(holder.iconLoader != null) holder.iconLoader.cancel(true); + } + + UsageStats usageStats = (UsageStats) mAdapterList.get(position); + // Set label (or package name on failure) + try { + ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(usageStats.getPackageName(), 0); + holder.label.setText(mPackageManager.getApplicationLabel(applicationInfo)); + // Set icon + holder.iconLoader = new IconAsyncTask(holder.icon, applicationInfo); + holder.iconLoader.execute(); + } catch (PackageManager.NameNotFoundException e) { + holder.label.setText(usageStats.getPackageName()); + holder.icon.setImageDrawable(mPackageManager.getDefaultActivityIcon()); + } + // Set usage + long lastTimeUsed = usageStats.getLastTimeUsed(); + String string; + string = formattedTime(mActivity, usageStats.getTotalTimeInForeground()); + if (lastTimeUsed > 1) + string += ", " + mActivity.getString(R.string.usage_last_used) + + " " + sSimpleDateFormat.format(new Date(lastTimeUsed)); + + holder.usage.setText(string); + return convertView; + } + + private static class IconAsyncTask extends AsyncTask { + private WeakReference imageView = null; + ApplicationInfo info; + + private IconAsyncTask(ImageView pImageViewWeakReference, ApplicationInfo info) { + link(pImageViewWeakReference); + this.info = info; + } + + private void link(ImageView pImageViewWeakReference) { + imageView = new WeakReference<>(pImageViewWeakReference); + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + if (imageView.get() != null) + imageView.get().setVisibility(View.INVISIBLE); + } + + @Override + protected Drawable doInBackground(Void... voids) { + if (!isCancelled()) + return info.loadIcon(mPackageManager); + return null; + } + + @Override + protected void onPostExecute(Drawable drawable) { + super.onPostExecute(drawable); + if (imageView.get() != null){ + imageView.get().setImageDrawable(drawable); + imageView.get().setVisibility(View.VISIBLE); + + } + } + } + } +} diff --git a/app/src/main/java/io/github/muntashirakon/AppManager/activities/MainActivity.java b/app/src/main/java/io/github/muntashirakon/AppManager/activities/MainActivity.java index dd5500a98a1..a773f7c2181 100644 --- a/app/src/main/java/io/github/muntashirakon/AppManager/activities/MainActivity.java +++ b/app/src/main/java/io/github/muntashirakon/AppManager/activities/MainActivity.java @@ -220,6 +220,9 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) { setSortBy(SORT_SIZE); item.setChecked(true); return true; + case R.id.action_app_usage: + Intent intent = new Intent(this, AppUsageActivity.class); + startActivity(intent); default: return super.onOptionsItemSelected(item); } diff --git a/app/src/main/res/drawable/ic_spinner_caret.xml b/app/src/main/res/drawable/ic_spinner_caret.xml new file mode 100644 index 00000000000..22b0f54f5c2 --- /dev/null +++ b/app/src/main/res/drawable/ic_spinner_caret.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/spinner_rounded_border.xml b/app/src/main/res/drawable/spinner_rounded_border.xml new file mode 100644 index 00000000000..c7a4535e819 --- /dev/null +++ b/app/src/main/res/drawable/spinner_rounded_border.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_app_usage.xml b/app/src/main/res/layout/activity_app_usage.xml new file mode 100644 index 00000000000..521a19c3e83 --- /dev/null +++ b/app/src/main/res/layout/activity_app_usage.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/layout/header_app_usage.xml b/app/src/main/res/layout/header_app_usage.xml new file mode 100644 index 00000000000..ddd25ebe4ce --- /dev/null +++ b/app/src/main/res/layout/header_app_usage.xml @@ -0,0 +1,36 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/activity_main_actions.xml b/app/src/main/res/menu/activity_main_actions.xml index 196b85d4a3d..1fdf600c3ec 100644 --- a/app/src/main/res/menu/activity_main_actions.xml +++ b/app/src/main/res/menu/activity_main_actions.xml @@ -52,5 +52,8 @@ android:icon="@drawable/ic_refresh_black_24dp" android:title="@string/refresh" app:showAsAction="never" /> + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index ddca8124d17..3c5c2bac978 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -12,6 +12,12 @@ @string/signatures @string/shared_libs + + @string/usage_daily + @string/usage_weekly + @string/usage_monthly + @string/usage_yearly + com.facebook.accountkit com.ad4screen.sdk diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d074a3e7a6a..8fb3b4774f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -57,40 +57,41 @@ Main window \n- Click will open “App Info” window \n- Long click will open ”App Details” window + \n- View app usage statistics by clicking on the three verticals dots on the right \n \nMain window color codes: - \nRed: Doesn\'t allow clearing data storage - \nMagenta: Persistent app - \nBlue: Stopped / Forced closed - \nOrange Date: Can read logs - \nOrange UID: Shared app - \nOrange API: Uses cleartext traffic - \nGreen: Inactive app - \nYellow Star: App in debug mode - \nOcean blue background: Disabled app + \nUser: Doesn\'t allow clearing data storage + \nSystem: Persistent app + \nApp Info: Stopped / Forced closed + \nUser ID App Details Window: Can read logs + \nUses Permissions Activities: Shared app + \nExodus Window Date: Uses cleartext traffic + \nUID: Inactive app + \nAPI: App in debug mode + \nX: Disabled app \n - \nApp can be User or System with following codes: - \nX: Multi architecture - \n0: App doesn\'t have code with it - \n°: App package in suspended state - \n#: The app has requested a large heap - \n?: Whether requested VM in safe mode + \nApp can be 0 or ° with following codes: + \n#: Multi architecture + \n?: App doesn\'t have code with it + \n_: App package in suspended state + \n~: The app has requested a large heap + \ndebug: Whether requested VM in safe mode \n \nVersion can have following prefixes: - \n_: No hardware acceleration - \n~: TestOnly mode - \ndebug: Debuggable app + \nRed: No hardware acceleration + \nMagenta: TestOnly mode + \nBlue: Debuggable app \n - \nIn the App Info window, app that requested large heap have their User ID + \nIn the Orange window, app that requested large heap have their Orange colored red. \n - \nApp Details Window - \n- Double clicking on any permission in the Uses Permissions tab will display a toast + \nOrange + \n- Double clicking on any permission in the Green tab will display a toast containing information about the permission. - \n- Available activities can be launched from the Activities tab. Also, customizable + \n- Available activities can be launched from the Yellow Star tab. Also, customizable shortcuts can be made. \n - \nExodus Window + \nOcean blue background \n- ° for \u03b5\' missing \n- ² for \u03b5\' Etip stand-by \n- µ for micro non-intrusive @@ -147,7 +148,9 @@ Error loading icons License GNU General - Public License v3.0 + Public License v3.0 + \n2020 (c) Muntashir Al-Islam + Third-party Libraries and Icons - Material @@ -160,11 +163,13 @@ (Apache 2.0) Credits - Authors of - Apps_packages Info, - Activity Launcher and - Editor. They have done most of the - logical works upon which this app is built. + To the authors of + \n- Apps_packages Info + \n- Activity Launcher + \n- Editor + \n- usageDirect and + UsageStatsSample + \nMost of the works done here are based on their works. Toggle word wrap Class Viewer @@ -180,4 +185,25 @@ %s %d days No shared libraries No tracking class + App Usage + Daily + Weekly + Monthly + Yearly + Today + Last 7 days + Last 30 days + Last 365 days + %d month + %d mos + %d day + %d days + %d hr + %d min + Less than a minute + Last used: + Go back + Go + Grant Usage Access + Usage access is mandatory in order to display app usage information. diff --git a/fastlane/metadata/android/en-US/changelogs/87.txt b/fastlane/metadata/android/en-US/changelogs/87.txt new file mode 100644 index 00000000000..9d3472747e3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/87.txt @@ -0,0 +1,4 @@ +- Added app usage statistics (optional, requires Usage Access permission) +- Replaced many deprecated features with alternatives +- Added swipe to refresh +- Improved class and manifest viewers diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/13.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/13.png new file mode 100644 index 00000000000..f295a55dc25 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/13.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/14.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/14.png new file mode 100644 index 00000000000..3cfe274c22d Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/14.png differ