Android 如何检查我们可以访问哪些存储卷,哪些不可以';T 背景

Android 如何检查我们可以访问哪些存储卷,哪些不可以';T 背景,android,storage-access-framework,Android,Storage Access Framework,谷歌(很遗憾)使应用程序无法使用标准的文件API(和文件路径)访问文件系统。很多都是因为它改变了应用程序访问存储的方式,并且在很多方面它是一个受限的API 因此,如果我们希望处理各种存储卷并访问其中的所有文件,我们将需要在未来的Android版本上完全使用SAF(存储访问框架)(在Android Q上,我们至少可以暂时使用正常的存储权限) 因此,例如,假设您要创建一个文件管理器并显示设备的所有存储卷,以显示用户可以授予访问权限的内容,如果您已经可以访问每个存储卷,则只需输入即可。这样的事情似乎很

谷歌(很遗憾)使应用程序无法使用标准的文件API(和文件路径)访问文件系统。很多都是因为它改变了应用程序访问存储的方式,并且在很多方面它是一个受限的API

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

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

问题 从API 24()开始,我们终于能够列出所有存储卷,例如:

    val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
    val storageVolumes = storageManager.storageVolumes
而且,我们有史以来第一次有意向请求访问storageVolume()。因此,例如,如果我们希望请求用户授予对主服务器的访问权(实际上,这将从那里开始,而不是真正要求任何东西),我们可以使用以下方法:

startActivityForResult(storageManager.primaryStorageVolume.createOpenDocumentTreeIntent(), REQUEST_CODE__DIRECTORTY_PERMISSION)
而不是
startActivityForResult(Intent(Intent.ACTION\u OPEN\u DOCUMENT\u TREE),请求\u code\u DIRECTORTY\u权限)
,并希望用户在那里选择正确的内容

为了最终访问用户选择的内容,我们有:

@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()的“作用域目录访问”的视频,他们专门讨论了StorageVolume类。它们甚至提供了有关侦听StorageVolume的装载事件的信息,但没有告诉我们如何识别我们可以访问的装载事件

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

  • 当使用
    createOpenDocumentTreeContent
    函数时,我注意到里面隐藏着一个Uri,可能告诉我们从哪个开始。它在extras中,在一个名为“android.provider.extra.INITIAL_URI”的键中。例如,在主存储器上检查其值时,我得到了以下结果:

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

  • 当我查看在onActivityResult中得到的Uri时,我得到了一些类似于#2的东西,但与我显示的
    treeUri
    变量不同:

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

  • 要获取到目前为止您可以访问的内容列表,您可以使用:

    val persistedUrpermissions=contentResolver.persistedUrmissions

  • 这将返回一个列表,每个列表都有一个Uri。遗憾的是,当我使用它时,我得到了与#3上相同的结果,这与我从StorageVolume获得的结果无法相比:

    content://com.android.externalstorage.documents/tree/primary%3A
    
    如您所见,我找不到存储卷列表与用户授权之间的任何映射

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

    我唯一拥有的是我在其他问题上找到的一大块变通函数,我认为它们不可靠,尤其是现在我们无法真正访问文件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;
        }
    }
    
    @TargetApi(版本号:棒棒糖)
    私有静态字符串getVolumeIdFromTreeUri(最终Uri treeUri){
    最终字符串docId=DocumentsContract.getTreeDocumentId(treeUri);
    final int end=docId.indexOf(“:”);
    字符串结果=end==-1?空:docId.substring(0,end);
    返回结果;
    }
    私有静态字符串getDocumentPathFromTreeUri(最终Uri treeUri){
    最终字符串docId=DocumentsContract.getTreeDocumentId(treeUri);
    //TODO避免使用字符串拆分(因为它使用额外的字符串创建)
    最终字符串[]split=docId.split(“:”);
    如果((split.length>=2)和&(split[1]!=null))
    收益分割[1];
    其他的
    返回File.separator;
    }
    公共静态字符串GetFullPathFDocumentFile(上下文上下文,文档文件){
    字符串volumePath=getVolumePath(上下文,getVolumeIdFromTreeUri(documentFile.getUri());
    if(volumePath==null)
    返回null;
    DocumentFile parent=DocumentFile.getParentFile();
    如果(父项==null)
    返回卷路径;
    final LinkedList fileHierarchy=新LinkedList();
    while(true){
    添加(0,documentFile.getName());
    documentFile=父级;
    parent=documentFile.getParentFile();
    如果(父项==null)
    打破
    }
    final StringBuilder sb=新StringBuilder(volumePath).append(File.separator);
    for(字符串文件名:fileHierarchy)
    sb.append(文件名).append(文件分隔符);
    使某人返回字符串();
    }
    /**
    *从文档的树URI获取文档的完整路径。
    *
    *@param treeUri树里。
    *@返回路径(不带尾随文件分隔符)。
    */
    公共静态字符串getFullPathFromTreeUri(上下文上下文,最终Uri treeUri){
    if(treeUri==null)
    返回null;
    字符串volumePath=getVolumePath(上下文,getVolumeIdFromTreeUri(treeUri));
    if(volumePath==null)
    返回File.separator;
    if(volumePath.endsWith(File.separator))
    volumePath=volumePath.substring(0,volumePath.length()-1);
    字符串documentPath=getDocumentPathFromTreeUri(treeUri);
    if(documentPath.endsWith(File.separator))
    文档路径=
    
    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")
        }
    }
    
    /**
     * 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;
    }
    
    <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>
    
    /** 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
    }
    
    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}")
        }
    }
    
    @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")
    }