如何检查我们可以访问哪些StorageVolume,我们不能访问哪些? [英] How to check which StorageVolume we have access to, and which we don't?

查看:510
本文介绍了如何检查我们可以访问哪些StorageVolume,我们不能访问哪些?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

Google(非常遗憾) 计划破坏存储权限 ,以便应用无法使用标准File API(和文件路径)访问文件系统.许多都是 反对 ,因为它改变了应用访问存储的方式而且在很多方面都是受限制的API.

Google (sadly) plans to ruin storage permission so that apps won't be able to access the file system using the standard File API (and file-paths). Many are against it as it changes the way apps can access the storage and in many ways it's a restricted and limited API.

因此,我们将需要在将来的某些Android版本上完全使用SAF(存储访问框架)(在Android Q上,我们至少可以暂时使用

As a result, we will need to use SAF (storage access framework) entirely on some future Android version (on Android Q we can, at least temporarily, use a flag to use the normal storage permission), if we wish to deal with various storage volumes and reach all files there.

例如,假设您要创建一个文件管理器并显示设备的所有存储卷,以显示用户可以授予访问权限的内容,并且如果您已经可以访问每个文件,则只需输入该文件即可.这样的事情看起来很合理,但是由于我找不到解决办法.

So, for example, suppose you want to make a file manager and show all the storage volumes of the device, to show what the user can grant access to, and if you already have access to each, you just enter it. Such a thing seems very legitimate, but as I can't find a way to do it.

从API 24开始( 此处 > ),我们终于可以列出所有存储卷,如下所示:

Starting from API 24 (here), we finally have the ability to list all of the storage volumes, as such:

    val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
    val storageVolumes = storageManager.storageVolumes

而且,这是有史以来第一次,我们可以请求访问storageVolume(

And, for the first time ever, we can have an Intent to request access to a storageVolume (here). So if we want, for example, to request the user to grant access to the primary one (which will just start from there, actually, and not really ask anything), we could use this:

startActivityForResult(storageManager.primaryStorageVolume.createOpenDocumentTreeIntent(), REQUEST_CODE__DIRECTORTY_PERMISSION)

代替startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), REQUEST_CODE__DIRECTORTY_PERMISSION),希望用户在那里选择正确的东西.

Instead of startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), REQUEST_CODE__DIRECTORTY_PERMISSION) , and hoping the user will choose the correct thing there.

要最终访问用户选择的内容,我们需要这样做:

And to finally get the access to what the user chose, we have this:

@TargetApi(Build.VERSION_CODES.KITKAT)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == REQUEST_CODE__DIRECTORTY_PERMISSION && resultCode == Activity.RESULT_OK && data != null) {
        val treeUri = data.data ?: return
        contentResolver.takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
        val pickedDir = DocumentFile.fromTreeUri(this, treeUri)
        ...

到目前为止,我们可以请求对各种存储卷的许可...

So far we can request for permission on the various storage volumes...

但是,如果您想知道自己获得了哪些权限,哪些还没有获得权限,就会出现问题.

However, the problem arises if you want to know which you got permission to and which you haven't.

  1. Google提供了有关范围目录访问"的视频( 此处 ),他们专门讨论了StorageVolume类.他们甚至提供有关侦听StorageVolume挂载事件的信息,但他们并没有提供任何有关识别我们可以访问的内容的信息.

  1. There is a video about "Scoped Directory Access" by Google (here), which they talk specifically about the StorageVolume class. They even give information about listening to mount-events of StorageVolume, but they don't tell anything about identifying those that we got access to.

StorageVolume类的唯一ID是 uuid ,但甚至不能保证返回任何内容.实际上,它在各种情况下都返回null.例如,主存储的情况.

The only ID of StorageVolume class is uuid , but it's not even guaranteed to return anything. And indeed it returns null in various cases. For example the case of the primary storage.

使用createOpenDocumentTreeIntent函数时,我注意到其中隐藏了一个Uri,可能会告诉您以哪个开头.它位于Extras内部,位于名为"android.provider.extra.INITIAL_URI"的键中.例如,当在主存储上检查其值时,我得到了:

When using the createOpenDocumentTreeIntent function, I've noticed there is a Uri hidden inside, probably telling which to start with. It's inside the extras, in a key called "android.provider.extra.INITIAL_URI". When checking its value on the primary storage, for example, I got this:

content://com.android.externalstorage.documents/root/primary

content://com.android.externalstorage.documents/root/primary

当我查看Uri时,我在onActivityResult中得到了回报,我得到的东西与#2有点相似,但是对于我显示的treeUri变量却有所不同:

When I look at the Uri I get in return in the onActivityResult, I get something a bit similar to #2, but different for the treeUri variable I've shown :

content://com.android.externalstorage.documents/tree/primary%3A

content://com.android.externalstorage.documents/tree/primary%3A

为了获得到目前为止您可以访问的内容的列表,您可以使用

In order to get the list of what you have access to so far, you can use this:

valsistenceedUriPermissions = contentResolver.persistedUriPermissions

val persistedUriPermissions = contentResolver.persistedUriPermissions

这将为您返回 UriPermission 的列表,每个都有一个Uri.可悲的是,当我使用它时,我得到的是与#3相同的东西,我无法真正与从StorageVolume获得的东西相比:

This returns you a list of UriPermission, each has a Uri. Sadly, when I use it, I get the same as on #3, which I can't really compare to what I get from StorageVolume :

content://com.android.externalstorage.documents/tree/primary%3A

因此,如您所见,在存储卷列表和用户授予的内容之间找不到任何类型的映射.

So as you can see, I can't find any kind of mapping between the list of storage volumes and what the user grants.

我什至不知道用户是否选择了一个存储卷,因为createOpenDocumentTreeIntent的功能仅将用户发送到StorageVolume,但是仍然可以选择一个文件夹.

I can't even know if the user has chosen a storage volume at all, because the function of createOpenDocumentTreeIntent only send the user to the StorageVolume, but it's still possible to select a folder instead.

我唯一拥有的就是我在其他问题上发现的许多解决方法功能,而且我认为它们并不可靠,尤其是由于我们实际上无法访问File API和文件路径.

The only thing that I do have, is a chunk of workaround functions I've found on other questions here, and I don't think they are reliable, especially now that we don't really have access to File API and file-path.

我把它们写在这里,以防万一您认为它们很有用:

I've written them here, in case you think they are useful:

@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;
}

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;
}

public static String getFullPathOfDocumentFile(Context context, DocumentFile documentFile) {
    String volumePath = getVolumePath(context, getVolumeIdFromTreeUri(documentFile.getUri()));
    if (volumePath == null)
        return null;
    DocumentFile parent = documentFile.getParentFile();
    if (parent == null)
        return volumePath;
    final LinkedList<String> fileHierarchy = new LinkedList<>();
    while (true) {
        fileHierarchy.add(0, documentFile.getName());
        documentFile = parent;
        parent = documentFile.getParentFile();
        if (parent == null)
            break;
    }
    final StringBuilder sb = new StringBuilder(volumePath).append(File.separator);
    for (String fileName : fileHierarchy)
        sb.append(fileName).append(File.separator);
    return sb.toString();
}

/**
 * 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;
}

/**
 * Get the path of a certain volume.
 *
 * @param volumeId The volume id.
 * @return The path.
 */
private static String getVolumePath(Context context, final String volumeId) {
    if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP)
        return null;
    try {
        final StorageManager storageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
        if (VERSION.SDK_INT >= VERSION_CODES.N) {
            final Class<?> storageVolumeClazz = StorageVolume.class;
            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;
    }
}

问题

如何在StorageVolume列表和已授予UriPermission列表之间进行映射?

The question

How can I map between the list of StorageVolume and the list of granted UriPermission ?

换句话说,给定一个StorageVolume列表,我怎么知道我可以访问哪些内容,而我没有访问权限,如果我可以访问的话,如何打开它并查看其中的内容呢?

In other words, given a list of StorageVolume, how can I know to which I have access to and which I don't , and if I do have access, to open it and see what's inside?

推荐答案

这里是获取所需内容的另一种方法.这是一种变通方法,就像您在不使用反射或文件路径的情况下发布的那样.

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数组内容(仅URI的值):

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

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"是冒号(:").因此,对于一个卷而言,似乎URI的构造如下,其中< volume>"是该卷的UUID.

"%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>:"

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

如果uri是直接在卷下的目录,则结构为:

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

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

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> ..."

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

因此,只需从这些格式的URI中提取卷即可.提取的音量可用作StorageManager.storageVolumes的键.下面的代码就是这样做的.

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.

在我看来,应该有一种更简单的方法来解决此问题. API中存储卷和URI之间必须缺少链接.我不能说这种技术可以涵盖所有情况.

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.

我还质疑storageVolume.uuid返回的UUID,它似乎是32位值.我以为UUID的长度为128位.这是UUID的替代格式还是从UUID导出的格式?有趣,一切都将掉落! :(

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

这篇关于如何检查我们可以访问哪些StorageVolume,我们不能访问哪些?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆