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")}