【问题标题】:How to check which StorageVolume we have access to, and which we don't?如何检查我们可以访问哪些 StorageVolume,哪些不能访问?
【发布时间】:2019-11-01 14:26:07
【问题描述】:

背景

Google(遗憾地)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)
        ...

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

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

我发现了什么

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

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

  3. 当使用createOpenDocumentTreeIntent 函数时,我注意到里面隐藏了一个Uri,可能会告诉从哪个开始。它位于附加组件中,位于名为“android.provider.extra.INITIAL_URI”的键中。例如,当检查它在主存储上的值时,我得到了:

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

  4. 当我查看我在 onActivityResult 中返回的 Uri 时,我得到的东西有点类似于 #2,但对于我显示的 treeUri 变量有所不同:

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

  5. 为了得到你目前可以访问的列表,你可以使用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 列表,我如何知道哪些我可以访问哪些我不能访问,如果我可以访问,打开它看看里面有什么?

【问题讨论】:

  • 伙计,我只是想问点什么......我慢慢地颤抖......它没有这种问题吗??

标签: android storage-access-framework


【解决方案1】:

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

在模拟器上,我看到以下我有权访问的项目。

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

0 uri = content://com.android.externalstorage.documents/tree/primary%3A
1 uri = 内容://com.android.externalstorage.documents/tree/1D03-2E0E%3ADownload
2 uri = 内容://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”是一个冒号(“:”)。因此,对于“”是该卷的 UUID 的卷,URI 的构造如下所示。

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

如果uri是一个卷下的目录,那么结构是:

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

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

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

因此,只需从这些格式的 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}")
    }
}

【讨论】:

  • 感谢您的帮助和解释,但您的解决方案似乎与我展示的 getDocumentPathFromTreeUrigetVolumeIdFromTreeUri 函数中的类似。但是,您使用更正式的方式来处理 Uri,而不是我发现的。很遗憾,这只是一个更好的解决方法:(
  • 新代码已发布。基本上是一样的,但是我从存储 UUID 构建 URI。
  • 看来你已经做了一个更好的解决方案。我会接受它并给+1,但对于赏金我想先问一些事情:你认为这是一个更有效的解决方法,还是一个真正的解决方案?你认为谷歌应该提供更正式的方式来做到这一点吗?你认为什么时候会找不到 UUID?您是否在真实设备上测试过它,即使是 Android 9.0+、SD 卡和 USBOTG?
  • @androiddeveloper 真正的解决方案或解决方法?我认为 API 中缺少一些功能,因此是“解决方法”,但它是当前可用的真正解决方案(恕我直言)。 UUID 应该始终可用,但我读过一些 SD 卡没有它们的地方(可能已损坏。)没有它们,我不知道 Android 将如何在设备被移除后保持访问。 StorageManager 报告的 UUID 无论如何都是假的:它们太短了。我还没有在真实设备上测试过。遗憾的是,我无法使用 Android 9+ 设备。 :(
  • 好答案。太糟糕了,检查这些东西太难了,我们只依赖几种可能有效也可能无效的方法。我认为直到最近 Google 才终于在其模拟器中添加了 SD 卡分区,但我认为仅在 Android Q 上。好的,我将授予赏金,但如果您找到其他可能的解决方法或解决方案,请也写在这里并告诉我它。另外,请把它放在一个函数中,而不是一个 onClickListener ...
【解决方案2】:

编辑:找到了一种解决方法,但它可能有一天不起作用。

它使用反射来获取 StorageVolume 实例的真实路径,并使用我之前拥有的获取 persistedUriPermissions 的路径。如果它们之间有交集,则意味着我可以访问 storageVolume。

似乎可以在模拟器上运行,它最终具有内部存储和 SD 卡。

希望我们能获得合适的 API,而不需要使用反射。

如果有没有这些技巧的更好的方法,请告诉我。

所以,这里有一个例子:

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>

把它放在一个简单的函数中,这里:

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

【讨论】:

    【解决方案3】:

    适用于 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")
    }
    

    【讨论】:

    • 什么是“reAndroidDataFolder”?
    • private val reAndroidDataFolder = """/Android/data/.*""".toRegex()
    【解决方案4】:

    这些解决方案都不适用于运行 Android 11 (+?) 的三星设备,因为似乎无法在这些设备上获取卷名。

    MediaStore.getExternalVolumeNames() 不起作用。 StorageManager.getStorageVolumes() 不起作用。

    https://forum.developer.samsung.com/t/usb-massive-storage-not-recognized-in-galaxy-s20-android-11/9758/6

    然而,不知何故,某些文件浏览器应用程序(如 X-Plore)能够获取这些卷名。

    【讨论】:

    • 询问他们的开发人员他们是如何做到的。我想我和一个人谈过别的事情,他们实际上回答了。
    • 解决方案:实际上有一种类似的方法适用于这些 Android 11 三星手机:getRecentExternalVolumeNames()。只需使用它即可。
    • 我不明白你会怎么做。
    • 这至少适用于我的用例,它特定于 USB 驱动器:gist.github.com/gavingt/c43bfb876ad78571b72e8bc6fd8ffb82
    • 很抱歉,我不知道如何提供帮助。另外为什么不把代码放在这里?时间不长……
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-16
    • 2012-02-20
    相关资源
    最近更新 更多