and*_*per 12 android storage-access-framework
Google不幸地计划破坏存储权限,以使应用程序无法使用标准File API(和文件路径)访问文件系统。许多人反对它,因为它改变了应用程序访问存储的方式,并且在许多方面都是受限制的有限API。
因此,如果我们想处理各种问题,我们将需要在将来的某些Android版本上完全使用SAF(存储访问框架)(在Android Q上,我们至少可以暂时使用一个标志来使用常规存储权限)。存储卷并到达那里的所有文件。
因此,例如,假设您要创建一个文件管理器并显示设备的所有存储卷,以显示用户可以授予访问权限的内容,并且如果您已经可以访问每个文件,则只需输入该文件即可。这样的事情看起来很合理,但是由于我找不到解决方法。
从API 24(此处)开始,我们终于可以列出所有存储卷,如下所示:
val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumes = storageManager.storageVolumes
Run Code Online (Sandbox Code Playgroud)
而且,这是有史以来第一次,我们可以有一个Intent请求访问storageVolume(在此)。因此,例如,如果我们要请求用户授予对主数据库的访问权限(实际上是从那里开始,实际上并没有真正询问任何内容),则可以使用以下方法:
startActivityForResult(storageManager.primaryStorageVolume.createOpenDocumentTreeIntent(), REQUEST_CODE__DIRECTORTY_PERMISSION)
Run Code Online (Sandbox Code Playgroud)
代替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)
...
Run Code Online (Sandbox Code Playgroud)
到目前为止,我们可以请求对各种存储卷的许可...
但是,如果您想知道自己获得了哪些权限,而没有得到哪些权限,就会出现问题。
有一个有关Google的“范围目录访问”的视频(在此处),他们专门谈论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
为了获得到目前为止您可以访问的内容的列表,可以使用以下命令:
valsistenceedUriPermissions = contentResolver.persistedUriPermissions
这将返回UriPermission列表,每个列表都有一个Uri。可悲的是,当我使用它时,我得到了与#3相同的东西,我无法真正与从StorageVolume获得的东西进行比较:
content://com.android.externalstorage.documents/tree/primary%3A
Run Code Online (Sandbox Code Playgroud)
如您所见,在存储卷列表和用户授予的内容之间找不到任何类型的映射。
我什至不知道用户是否选择了一个存储卷,因为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;
}
}
Run Code Online (Sandbox Code Playgroud)
如何在StorageVolume列表和已授予UriPermission列表之间进行映射?
换句话说,给定一个StorageVolume列表,我怎么知道我可以访问哪些内容,而我没有访问权限,如果我确实具有访问权限,则可以打开它并查看其中的内容?
这是获得您想要的东西的另一种方法。这是一种解决方法,就像您发布的那样,不使用反射或文件路径。
在模拟器上,我看到我已允许访问的以下项目。
persistedUriPermissions 数组内容(仅限 URI 的值):
0 uri = 内容://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 = 内容://com.android.externalstorage.documents/tree/primary%3ADCIM
4 uri = 内容://com.android.externalstorage.documents/tree /primary%3AAlarms
“%3A”是冒号(“:”)。因此,对于卷来说,URI 的构造如下,其中“<volume>”是该卷的 UUID。
uri =“内容://com.android.externalstorage.documents/tree/<卷>:”
如果uri是卷正下方的目录,则结构为:
uri =“content://com.android.externalstorage.documents/tree/<卷>:<目录>”
对于结构较深的目录,格式为:
uri =“content://com.android.externalstorage.documents/tree/<卷>:<目录>/<目录>/<目录>...”
因此,只需从这些格式的 URI 中提取卷即可。提取的卷可以用作 的密钥StorageManager.storageVolumes。下面的代码就是这样做的。
在我看来,应该有一种更简单的方法来解决这个问题。存储卷和 URI 之间的 API 中肯定缺少链接。我不能说这种技术适用于所有情况。
我还质疑返回的 UUID,storageVolume.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}")
}
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
710 次 |
| 最近记录: |