Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use StorageStatsManager.queryStatsForPackage on Android O?

Background

Before Android O, in order to get an app size, you had to have a special permission for it, and use a hidden API.

The problem

Such a solution won't work anymore, but instead, Google provides an official API that's less granular: queryStatsForPackage , which returns StorageStats object.

However, I can't find any information of how to use it.

As opposed to the hidden API, which only required the package name of the app, this one also requires "String volumeUuid" and "UserHandle user".

The questions

  1. How do I provide those parameters?
  2. What do they mean? Is each of the volumeUuid refer to a different storage (build in storage and real sd-card, for example), and UserHandle is for each user?
  3. If #2 is correct, should I really call each of them for each possible value, to get the real total storage the app takes? If so, how?
like image 977
android developer Avatar asked Dec 18 '22 08:12

android developer


2 Answers

OK, I've found out how to use the new API. I've prepared a small sample of this, fetching some information about the volume storage and of a specific app (this time of the Play Store, but you can change it if needed) :

public class MainActivity extends AppCompatActivity {

    public static final String PACKAGE_NAME = "com.android.vending";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (!hasUsageStatsPermission(this))
            startActivityForResult(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY), 1);
        else {
            final Context context = this;
            AsyncTask.execute(new Runnable() {
                @TargetApi(VERSION_CODES.O)
                @Override
                public void run() {
                    @SuppressLint("WrongConstant") final StorageStatsManager storageStatsManager = (StorageStatsManager) getSystemService(Context.STORAGE_STATS_SERVICE);
                    final StorageManager storageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
                    final List<StorageVolume> storageVolumes = storageManager.getStorageVolumes();
                    final UserHandle user = android.os.Process.myUserHandle();
                    for (StorageVolume storageVolume : storageVolumes) {
                        final String uuidStr = storageVolume.getUuid();
                        final UUID uuid = uuidStr == null ? StorageManager.UUID_DEFAULT : UUID.fromString(uuidStr);
                        try {
                            Log.d("AppLog", "storage:" + uuid + " : " + storageVolume.getDescription(context) + " : " + storageVolume.getState());
                            Log.d("AppLog", "getFreeBytes:" + Formatter.formatShortFileSize(context, storageStatsManager.getFreeBytes(uuid)));
                            Log.d("AppLog", "getTotalBytes:" + Formatter.formatShortFileSize(context, storageStatsManager.getTotalBytes(uuid)));
                            Log.d("AppLog", "storage stats for app of package name:" + PACKAGE_NAME + " : ");

                            final StorageStats storageStats = storageStatsManager.queryStatsForPackage(uuid, PACKAGE_NAME, user);
                            Log.d("AppLog", "getAppBytes:" + Formatter.formatShortFileSize(context, storageStats.getAppBytes()) +
                                    " getCacheBytes:" + Formatter.formatShortFileSize(context, storageStats.getCacheBytes()) +
                                    " getDataBytes:" + Formatter.formatShortFileSize(context, storageStats.getDataBytes()));
                        } catch (NameNotFoundException e) {
                            e.printStackTrace();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });

        }
    }

    @TargetApi(VERSION_CODES.M)
    public static boolean hasUsageStatsPermission(Context context) {
        //http://stackoverflow.com/a/42390614/878126
        if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP)
            return false;
        AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
        final int mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, android.os.Process.myUid(), context.getPackageName());
        boolean granted = false;
        if (mode == AppOpsManager.MODE_DEFAULT)
            granted = (context.checkCallingOrSelfPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) == PackageManager.PERMISSION_GRANTED);
        else
            granted = (mode == AppOpsManager.MODE_ALLOWED);
        return granted;
    }
}

manifest file should have this:

<uses-permission
    android:name="android.permission.PACKAGE_USAGE_STATS"
    tools:ignore="ProtectedPermissions"/>

EDIT: note that the part of UUID.fromString(uuidStr) could fail for SD-cards, and the reason by Google is this:

Android can't maintain quota-style statistics for public volumes (SD card etc.). Apps will need to build size calculation logic themselves.

Sadly, on Android 11, due to the new restrictions of reaching "Android" folder, even this is impossible. Requested here to change it:

@t0m But even with what they wrote, it's not officially impossible to do what they said to do, as storage permission got very restricted on Android 11, not allowing you to reach Android folder properly. Requested to solve this here:

  • https://issuetracker.google.com/issues/187069982
  • https://issuetracker.google.com/issues/187082347

Please consider starring.

like image 136
android developer Avatar answered Dec 28 '22 06:12

android developer


An alternative approach for getting UUID.

To get UUIDs, you can also use StorageManager.getUuidForPath(File path) function. As to the path, use Context.getFilesDir() and Context.getExternalFilesDirs() to get all possibles directories that an app data will be saved to.

Please note that different path may return the same UUID, so you have to use a hash set to collect unique UUIDs

like image 36
Sam Lu Avatar answered Dec 28 '22 06:12

Sam Lu