android – 如何检查我们有权访问哪个StorageVolume,哪个不访问?

背景

谷歌(遗憾地)plans to ruin storage permission使应用程序无法使用标准文件API(和文件路径)访问文件系统.许多都是against it,因为它改变了应用程序访问存储的方式,并且在很多方面它是受限且有限的API.

因此,如果我们希望处理各种存储卷,我们将需要完全在未来的某些Android版本上使用SAF(存储访问框架)(在Android Q上我们可以,至少是暂时的,use a flag使用正常的存储权限)并到达那里的所有文件.

因此,例如,假设您要创建文件管理器并显示设备的所有存储卷,以显示用户可以授予访问权限的内容,并且如果您已经有权访问每个存储卷,则只需输入它即可.这样的事情似乎很合理,但我找不到办法去做.

问题

从API 24(here)开始,我们最终能够列出所有存储卷,如下所示:

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

而且,有史以来第一次,我们可以有一个Intent来请求访问storageVolume(here).因此,例如,如果我们想要请求用户授予对主要用户的访问权限(实际上只是从那里开始,而不是真正提出任何问题),我们可以使用:

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

而不是startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE),REQUEST_CODE__DIRECTORTY_PERMISSION),并希望用户在那里选择正确的东西.

最后,为了获得用户选择的内容,我们有:

@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)
        ...

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

但是,如果您想知道哪些人获得了许可,哪些人没有获得许可,则会出现问题.

我发现了什么

> Google(here)有一个关于“Scoped Directory Access”的视频,他们特别谈到了StorageVolume类.他们甚至提供有关收听StorageVolume的挂载事件的信息,但是他们没有说明识别我们可以访问的那些事件.
> StorageVolume类的唯一ID是uuid,但它甚至不能保证返回任何内容.实际上它在各种情况下都返回null.例如,主存储的情况.
>当使用createopendocumentTreeIntent函数时,我注意到里面隐藏着一个Uri,可能会告诉你从哪个开始.它位于额外内容中,名为“android.provider.extra.INITIAL_URI”.例如,当检查它在主存储上的值时,我得到了这个:

内容://com.android.externalstorage.documents/root/primary
>当我看到Uri时,我在onActivityResult中获得了回报,我得到的东西有点类似于#2,但是我所显示的treeUri变量有所不同:

内容://com.android.externalstorage.documents/tree/primary:
>为了获得到目前为止您可以访问的列表,您可以使用this

val persistedUriPermissions = contentResolver.persistedUriPermissions

这将返回一个UriPermission的列表,每个列表都有一个Uri.可悲的是,当我使用它时,我得到的就像#3一样,我无法与从StorageVolume得到的东西相提并论:

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

正如您所看到的,我无法在存储卷列表和用户授予的内容之间找到任何类型的映射.

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

我唯一拥有的是我在其他问题上找到的大量解决方法函数,我认为它们不可靠,特别是现在我们无法访问File API和文件路径.

在这里写了它们,以防你认为它们有用:

@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列表之间进行映射?

换句话说,给定一个StorageVolume列表,我怎么知道我有哪些访问权限,哪些我没有访问权限,如果我有访问权限,可以打开它,看看里面有什么内容

解决方法:

这是获得你想要的另一种方式.这是一种解决方法,就像您在不使用反射或文件路径的情况下发布的那样.

在模拟器上,我看到以下允许访问的项目.

persistedUriPermissions数组内容(仅限URI的值):

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

“:”是冒号(“:”).因此,对于“< volume>”的卷来说,URI的构造如下所示.是卷的UUID.

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

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

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

对于结构中较深的目录,格式为:

uri = “content://com.android.externalstorage.documents/tree/<volume>:<directory>/<directory>/<directory>…”

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

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

我还质疑storageVolume.uuid返回的UUID,它似乎是一个32位的值.我认为UUID的长度是128位.这是UUID的替代格式还是以某种方式从UUID派生的?有意思,它即将降临!

相关文章

Android性能优化——之控件的优化 前面讲了图像的优化,接下...
前言 上一篇已经讲了如何实现textView中粗字体效果,里面主要...
最近项目重构,涉及到了数据库和文件下载,发现GreenDao这个...
WebView加载页面的两种方式 一、加载网络页面 加载网络页面,...
给APP全局设置字体主要分为两个方面来介绍 一、给原生界面设...
前言 最近UI大牛出了一版新的效果图,按照IOS的效果做的,页...