背景

Google(不好)plans to ruin storage permission,这样应用程序将无法使用标准File 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),他们专门谈论StorageVolume类。他们甚至提供有关监听StorageVolume挂载事件的信息,但对于识别我们可以访问的事件并没有任何帮助。
  • StorageVolume类的唯一ID是uuid,但甚至不能保证它返回任何东西。实际上,它在各种情况下都返回null。例如主存储的情况。
  • 使用createOpenDocumentTreeIntent函数时,我注意到其中隐藏了一个Uri,可能告诉了它以哪个开头。它位于Extras内部,位于名为“android.provider.extra.INITIAL_URI”的键中。例如,在主存储上检查其值时,得到了以下信息:

    内容://com.android.externalstorage.documents/root/primary
  • 当我查看Uri时,在onActivityResult中得到了返回,我得到的东西与#2有点相似,但是对于我所显示的treeUri变量却有所不同:

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

    valsistenceedUriPermissions = contentResolver.persistedUriPermissions

  • 这将返回UriPermission列表,每个列表都有一个Uri。可悲的是,当我使用它时,我得到了与#3相同的东西,我无法真正将其与从StorageVolume中得到的东西进行比较:
    content://com.android.externalstorage.documents/tree/primary%3A
    

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

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

    我唯一拥有的是在其他问题上发现的许多解决方法功能,而且我认为它们并不可靠,尤其是由于我们实际上没有访问文件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的值):



    “%3A”是冒号(“:”)。因此,对于一个卷而言,似乎URI的构造如下,其中“”是该卷的UUID。



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



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



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

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

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

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

    10-07 19:16
    查看更多