【发布时间】: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)
...
到目前为止,我们可以请求各种存储卷的权限...
但是,如果您想知道哪些您获得了许可,哪些您没有获得许可,就会出现问题。
我发现了什么
Google (here) 有一个关于“范围目录访问”的视频,他们专门讨论了 StorageVolume 类。它们甚至提供有关侦听 StorageVolume 的挂载事件的信息,但没有提供任何有关识别我们可以访问的内容的信息。
StorageVolume 类的唯一 ID 是 uuid ,但它甚至不能保证返回任何东西。事实上,它在各种情况下都返回 null。例如主存储的情况。
-
当使用
createOpenDocumentTreeIntent函数时,我注意到里面隐藏了一个Uri,可能会告诉从哪个开始。它位于附加组件中,位于名为“android.provider.extra.INITIAL_URI”的键中。例如,当检查它在主存储上的值时,我得到了:content://com.android.externalstorage.documents/root/primary
-
当我查看我在 onActivityResult 中返回的 Uri 时,我得到的东西有点类似于 #2,但对于我显示的
treeUri变量有所不同:content://com.android.externalstorage.documents/tree/primary%3A
-
为了得到你目前可以访问的列表,你可以使用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