How to check which StorageVolume we have access to, and which we don't? How to check which StorageVolume we have access to, and which we don't? android android

How to check which StorageVolume we have access to, and which we don't?


Here is an alternate way to get what you want. It is a work-around like you have posted without using reflection or file paths.

On an emulator, I see the following items for which I have permitted access.

persistedUriPermissions array contents (value of URI only):

0 uri = content://com.android.externalstorage.documents/tree/primary%3A
1 uri = content://com.android.externalstorage.documents/tree/1D03-2E0E%3ADownload
2 uri = content://com.android.externalstorage.documents/tree/1D03-2E0E%3A
3 uri = content://com.android.externalstorage.documents/tree/primary%3ADCIM
4 uri = content://com.android.externalstorage.documents/tree/primary%3AAlarms

"%3A" is a colon (":"). So, it appears that the URI is constructed as follows for a volume where "<volume>" is the UUID of the volume.

uri = "content://com.android.externalstorage.documents/tree/<volume>:"

If the uri is a directory directly under a volume, then the structure is:

uri = "content://com.android.externalstorage.documents/tree/<volume>:<directory>"

For directories deeper in the structure, the format is:

uri = "content://com.android.externalstorage.documents/tree/<volume>:<directory>/<directory>/<directory>..."

So, it is just a matter of extracting volumes from URIs in these formats. The volume extracted can be used as a key for StorageManager.storageVolumes. The following code does just this.

It seems to me that there should be an easier way to go about this. There must be a missing linkage in the API between storage volumes and URIs. I can't say that this technique covers all circumstances.

I also question the UUID that is returned by storageVolume.uuid which seems to be a 32-bit value. I thought that UUIDs are 128 bits in length. Is this an alternative format for a UUID or somehow derived from the UUID? Interesting, and it is all about to drop! :(

MainActivity.kt

class MainActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)        val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager        var storageVolumes = storageManager.storageVolumes        val storageVolumePathsWeHaveAccessTo = HashSet<String>()        checkAccessButton.setOnClickListener {            checkAccessToStorageVolumes()        }        requestAccessButton.setOnClickListener {            storageVolumes = storageManager.storageVolumes            val primaryVolume = storageManager.primaryStorageVolume            val intent = primaryVolume.createOpenDocumentTreeIntent()            startActivityForResult(intent, 1)        }    }    private fun checkAccessToStorageVolumes() {        val storageVolumePathsWeHaveAccessTo = HashSet<String>()        val persistedUriPermissions = contentResolver.persistedUriPermissions        persistedUriPermissions.forEach {            storageVolumePathsWeHaveAccessTo.add(it.uri.toString())        }        val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager        val storageVolumes = storageManager.storageVolumes        for (storageVolume in storageVolumes) {            val uuid = if (storageVolume.isPrimary) {                // Primary storage doesn't get a UUID here.                "primary"            } else {                storageVolume.uuid            }            val volumeUri = uuid?.let { buildVolumeUriFromUuid(it) }            when {                uuid == null ->                     Log.d("AppLog", "UUID is null for ${storageVolume.getDescription(this)}!")                storageVolumePathsWeHaveAccessTo.contains(volumeUri) ->                     Log.d("AppLog", "Have access to $uuid")                else -> Log.d("AppLog", "Don't have access to $uuid")            }        }    }    private fun buildVolumeUriFromUuid(uuid: String): String {        return DocumentsContract.buildTreeDocumentUri(            "com.android.externalstorage.documents",            "$uuid:"        ).toString()    }    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {        super.onActivityResult(requestCode, resultCode, data)        Log.d("AppLog", "resultCode:$resultCode")        val uri = data?.data ?: return        val takeFlags =            Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION        contentResolver.takePersistableUriPermission(uri, takeFlags)        Log.d("AppLog", "granted uri: ${uri.path}")    }}


EDIT: Found a workaround, but it might not work some day.

It uses reflection to get the real path of the StorageVolume instance, and it uses what I had before to get the path of persistedUriPermissions . If there are intersections between them, it means I have access to the storageVolume.

Seems to work on emulator, which finally has both internal storage and SD-card.

Hopefully we will get proper API and not need to use reflections.

If there is a better way to do it, without those kinds of tricks, please let me know.

So, here's an example:

MainActivity.kt

class MainActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)        val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager        val storageVolumes = storageManager.storageVolumes        val primaryVolume = storageManager.primaryStorageVolume        checkAccessButton.setOnClickListener {            val persistedUriPermissions = contentResolver.persistedUriPermissions            val storageVolumePathsWeHaveAccessTo = HashSet<String>()            Log.d("AppLog", "got access to paths:")            for (persistedUriPermission in persistedUriPermissions) {                val path = FileUtilEx.getFullPathFromTreeUri(this, persistedUriPermission.uri)                        ?: continue                Log.d("AppLog", "path: $path")                storageVolumePathsWeHaveAccessTo.add(path)            }            Log.d("AppLog", "storage volumes:")            for (storageVolume in storageVolumes) {                val volumePath = FileUtilEx.getVolumePath(storageVolume)                if (volumePath == null) {                    Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - failed to get volumePath")                } else {                    val hasAccess = storageVolumePathsWeHaveAccessTo.contains(volumePath)                    Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - volumePath:$volumePath - gotAccess? $hasAccess")                }            }        }        requestAccessButton.setOnClickListener {            val intent = primaryVolume.createOpenDocumentTreeIntent()            startActivityForResult(intent, 1)        }    }    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {        super.onActivityResult(requestCode, resultCode, data)        Log.d("AppLog", "resultCode:$resultCode")        val uri = data?.data ?: return        val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION        contentResolver.takePersistableUriPermission(uri, takeFlags)        val fullPathFromTreeUri = FileUtilEx.getFullPathFromTreeUri(this, uri)        Log.d("AppLog", "granted uri:$uri $fullPathFromTreeUri")    }}

FileUtilEx.java

/** * Get the full path of a document from its tree URI. * * @param treeUri The tree RI. * @return The path (without trailing file separator). */public static String getFullPathFromTreeUri(Context context, final Uri treeUri) {    if (treeUri == null)        return null;    String volumePath = getVolumePath(context, getVolumeIdFromTreeUri(treeUri));    if (volumePath == null)        return File.separator;    if (volumePath.endsWith(File.separator))        volumePath = volumePath.substring(0, volumePath.length() - 1);    String documentPath = getDocumentPathFromTreeUri(treeUri);    if (documentPath.endsWith(File.separator))        documentPath = documentPath.substring(0, documentPath.length() - 1);    if (documentPath.length() > 0)        if (documentPath.startsWith(File.separator))            return volumePath + documentPath;        else return volumePath + File.separator + documentPath;    return volumePath;}public static String getVolumePath(StorageVolume storageVolume){    if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP)        return null;    try{        final Class<?> storageVolumeClazz = StorageVolume.class;        final Method getPath = storageVolumeClazz.getMethod("getPath");        return (String) getPath.invoke(storageVolume);    } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {        e.printStackTrace();    }    return null;}/** * Get the path of a certain volume. * * @param volumeId The volume id. * @return The path. */@SuppressLint("ObsoleteSdkInt")private static String getVolumePath(Context context, final String volumeId) {    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)        return null;    try {        final StorageManager storageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {            final Class<?> storageVolumeClazz = StorageVolume.class;            //noinspection JavaReflectionMemberAccess            final Method getPath = storageVolumeClazz.getMethod("getPath");            final List<StorageVolume> storageVolumes = storageManager.getStorageVolumes();            for (final StorageVolume storageVolume : storageVolumes) {                final String uuid = storageVolume.getUuid();                final boolean primary = storageVolume.isPrimary();                // primary volume?                if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) {                    return (String) getPath.invoke(storageVolume);                }                // other volumes?                if (uuid != null && uuid.equals(volumeId))                    return (String) getPath.invoke(storageVolume);            }            return null;        }        final Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");        final Method getVolumeList = storageManager.getClass().getMethod("getVolumeList");        final Method getUuid = storageVolumeClazz.getMethod("getUuid");        //noinspection JavaReflectionMemberAccess        final Method getPath = storageVolumeClazz.getMethod("getPath");        final Method isPrimary = storageVolumeClazz.getMethod("isPrimary");        final Object result = getVolumeList.invoke(storageManager);        final int length = Array.getLength(result);        for (int i = 0; i < length; i++) {            final Object storageVolumeElement = Array.get(result, i);            final String uuid = (String) getUuid.invoke(storageVolumeElement);            final Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);            // primary volume?            if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) {                return (String) getPath.invoke(storageVolumeElement);            }            // other volumes?            if (uuid != null && uuid.equals(volumeId))                return (String) getPath.invoke(storageVolumeElement);        }        // not found.        return null;    } catch (Exception ex) {        return null;    }}/** * Get the document path (relative to volume name) for a tree URI (LOLLIPOP). * * @param treeUri The tree URI. * @return the document path. */@TargetApi(VERSION_CODES.LOLLIPOP)private static String getDocumentPathFromTreeUri(final Uri treeUri) {    final String docId = DocumentsContract.getTreeDocumentId(treeUri);    //TODO avoid using spliting of a string (because it uses extra strings creation)    final String[] split = docId.split(":");    if ((split.length >= 2) && (split[1] != null))        return split[1];    else        return File.separator;}/** * Get the volume ID from the tree URI. * * @param treeUri The tree URI. * @return The volume ID. */@TargetApi(VERSION_CODES.LOLLIPOP)private static String getVolumeIdFromTreeUri(final Uri treeUri) {    final String docId = DocumentsContract.getTreeDocumentId(treeUri);    final int end = docId.indexOf(':');    String result = end == -1 ? null : docId.substring(0, end);    return result;}

activity_main.xml

<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"  android:gravity="center" android:orientation="vertical" tools:context=".MainActivity">  <Button    android:id="@+id/checkAccessButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="checkAccess"/>  <Button    android:id="@+id/requestAccessButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="requestAccess"/></LinearLayout>

To put it in a simple function, here:

/** for each storageVolume, tells if we have access or not, via a HashMap (true for each iff we identified it has access*/fun getStorageVolumesAccessState(context: Context): HashMap<StorageVolume, Boolean> {    val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager    val storageVolumes = storageManager.storageVolumes    val persistedUriPermissions = context.contentResolver.persistedUriPermissions    val storageVolumePathsWeHaveAccessTo = HashSet<String>()    //            Log.d("AppLog", "got access to paths:")    for (persistedUriPermission in persistedUriPermissions) {        val path = FileUtilEx.getFullPathFromTreeUri(context, persistedUriPermission.uri)                ?: continue        //                Log.d("AppLog", "path: $path")        storageVolumePathsWeHaveAccessTo.add(path)    }    //            Log.d("AppLog", "storage volumes:")    val result = HashMap<StorageVolume, Boolean>(storageVolumes.size)    for (storageVolume in storageVolumes) {        val volumePath = FileUtilEx.getVolumePath(storageVolume)        val hasAccess = volumePath != null && storageVolumePathsWeHaveAccessTo.contains(volumePath)        result[storageVolume] = hasAccess    }    return result}


for API 30 (Android 11)

@TargetApi(30)private fun getVolumePathApi30(context:Context, uuid: String): String{    // /storage/emulated/0/Android/data/{packageName}/files    // /storage/0222-9FE1/Android/data/{packageName}/files    val list = ContextCompat.getExternalFilesDirs(context, null).map{ it.canonicalPath.replace(reAndroidDataFolder, "") }    // /storage/emulated/0    // /storage/0222-9FE1    val path = if( uuid == "primary") {        list.firstOrNull()    }else {        list.find { it.contains(uuid, ignoreCase = true) }    }    return path ?: error("can't find volume for uuid $uuid")}