Как проверить, к какому из StorageVolume у нас есть доступ, а какие нет?
Фон
Google (к сожалению) планирует испортить разрешение на хранение, чтобы приложения не могли получить доступ к файловой системе, используя стандартный File API (и пути к файлам). Многие против этого, поскольку это меняет способ, которым приложения могут получить доступ к хранилищу, и во многих отношениях это ограниченный и ограниченный API.
В результате нам потребуется полностью использовать SAF (инфраструктура доступа к хранилищу) в какой-то будущей версии Android (в Android Q мы можем, по крайней мере временно, использовать флаг, чтобы использовать обычное разрешение хранилища), если мы хотим иметь дело с различными объемы хранения и добраться до всех файлов там.
Так, например, предположим, что вы хотите создать файловый менеджер и показать все тома хранилища устройства, чтобы показать, к чему пользователь может предоставить доступ, и, если у вас уже есть доступ к каждому, вы просто вводите его. Такая вещь кажется очень законной, но, поскольку я не могу найти способ сделать это.
Эта проблема
Начиная с API 24 ( здесь), мы наконец-то получили возможность перечислять все тома хранения как таковые:
val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumes = storageManager.storageVolumes
И, впервые, у нас может быть Намерение запросить доступ к объему хранилища ( здесь). Так что, если мы хотим, например, запросить пользователя предоставить доступ к первичному (который на самом деле будет только с него начинать, а на самом деле ничего не спросить), мы можем использовать это:
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)
...
Пока что мы можем запросить разрешение на различные объемы хранения...
Однако проблема возникает, если вы хотите знать, на какое разрешение вы получили, а какое нет.
Что я нашел
-
Есть видео о "Доступе к Scoped Directory" от Google ( здесь), в котором они рассказывают конкретно о классе StorageVolume. Они даже дают информацию о прослушивании событий монтирования StorageVolume, но ничего не говорят об идентификации тех, к которым мы получили доступ.
-
Единственный идентификатор класса StorageVolume - это uuid, но он даже не гарантирует что-либо возвращать. И действительно, он возвращает ноль в различных случаях. Например, случай основного хранилища.
-
При использовании функции createOpenDocumentTreeIntent
я заметил, что внутри скрыт Uri, вероятно, createOpenDocumentTreeIntent
, с чего начать. Это внутри дополнений, в ключе под названием "android.provider.extra.INITIAL_URI". Например, при проверке его значения в основном хранилище я получил следующее:
Содержание://com.android.externalstorage.documents/root/primary
-
Когда я смотрю на Uri, который я получаю взамен в onActivityResult, я получаю нечто похожее на # 2, но другое для переменной treeUri
которую я показал:
Содержание://com.android.externalstorage.documents/tree/primary%3A
-
Чтобы получить список того, к чему у вас есть доступ, вы можете использовать это:
val persistedUriPermissions = contentResolver.persistedUriPermissions
Это возвращает вам список UriPermission, у каждого есть Uri. К сожалению, когда я использую его, я получаю то же самое, что и на # 3, который я не могу сравнить с тем, что я получаю от StorageVolume:
content://com.android.externalstorage.documents/tree/primary%3A
Итак, как вы можете видеть, я не могу найти какое-либо сопоставление между списком томов хранилища и тем, что предоставляет пользователь.
Я даже не могу знать, выбрал ли пользователь вообще объем хранилища, потому что функция createOpenDocumentTreeIntent
только отправляет пользователя в StorageVolume, но вместо этого все еще можно выбрать папку.
Единственное, что у меня есть, это набор обходных функций, которые я нашел по другим вопросам здесь, и я не думаю, что они надежны, особенно сейчас, когда у нас нет доступа к File API и file-path,
Я написал их здесь, если вы считаете, что они полезны:
@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, как я могу узнать, к чему у меня есть доступ, а какие нет, и если у меня есть доступ, открыть его и посмотреть, что внутри?
Ответы
Ответ 1
Вот альтернативный способ получить то, что вы хотите. Это обходной путь, который вы опубликовали без использования отражений или путей к файлам.
В эмуляторе я вижу следующие элементы, к которым у меня есть доступ.
Содержимое массива persistedUriPermissions (только значение URI):
0 uri = content://com.android.externalstorage.documents/tree/primary%3A
1 uri = content://com.android.externalstorage.documents/tree/1D03-2E0E%3ADownload
2 uri = content://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" - это двоеточие (":"). Итак, похоже, что URI строится следующим образом для тома, где "<том>" - это UUID тома.
uri = "content://com.android.externalstorage.documents/tree/<volume>:"
Если URI является каталогом непосредственно под томом, то структура:
uri = "content://com.android.externalstorage.documents/tree/<том>: <каталог>"
Для каталогов глубже в структуре, формат:
uri = "content://com.android.externalstorage.documents/tree/<том>: <каталог>/<каталог>/<каталог>..."
Таким образом, это просто вопрос извлечения томов из URI в этих форматах. Извлеченный том можно использовать в качестве ключа для StorageManager.storageVolumes
. Следующий код делает именно это.
Мне кажется, что должен быть более простой способ сделать это. В API должна отсутствовать связь между томами хранения и URI. Я не могу сказать, что эта техника охватывает все обстоятельства.
Я также подвергаю сомнению UUID, который возвращается storageVolume.uuid
который кажется 32-битным значением. Я думал, что UUIDs имеют длину 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}")
}
}
Ответ 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
}