Проблемы, проходящие через иерархию каталогов с помощью Android Storage Access Framework / DocumentProvider с использованием MTP
ОБНОВЛЕНИЕ: Мой первоначальный вопрос может вводить в заблуждение, поэтому я хочу перефразировать его: я хочу пройти через иерархическое дерево с подключенного к MTP устройства через Android Storage Access Framework. Я не могу достичь этого, потому что я получаю SecurityException
указав, что подузел не является потомком своего родительского узла. Есть ли способ обойти эту проблему? Или это известная проблема? Благодарю.
Я пишу приложение для Android, которое пытается обойти документы и получить доступ к ним через иерархическое дерево с помощью Android Storage Access Framework (SAF) через MtpDocumentsProvider
, Я более или менее следую примеру кода, описанному в https://github.com/googlesamples/android-DirectorySelection о том, как запустить средство выбора SAF из моего приложения, выбрать источник данных MTP, а затем, в onActivityResult
, используйте возвращенный Uri
пройти через иерархию. К сожалению, это, похоже, не работает, потому что, как только я получаю доступ к подпапке и пытаюсь пройти через это, я всегда получаю SecurityException
основываясь на этом document xx is not a descendant of yy
Итак, мой вопрос, используя MtpDocumentProvider
Как я могу успешно пройти через дерево иерархии из моего приложения и избежать этого исключения?
Чтобы быть точным, в моем приложении сначала я вызываю следующий метод для запуска средства выбора SAF:
private void launchStoragePicker() {
Intent browseIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
browseIntent.addFlags(
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
| Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
);
startActivityForResult(browseIntent, REQUEST_CODE_OPEN_DIRECTORY);
}
Затем запускается сборщик Android SAF, и я вижу, что мое подключенное устройство распознается как источник данных MTP. Я выбираю указанный источник данных и получаю Uri
от моего onActivityResult
:
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_OPEN_DIRECTORY && resultCode == Activity.RESULT_OK) {
traverseDirectoryEntries(data.getData()); // getData() returns the root uri node
}
}
Затем, используя возвращенный Uri
, Я звоню DocumentsContract.buildChildDocumentsUriUsingTree
чтобы получить Uri
который я затем могу использовать для запроса и доступа к древовидной иерархии:
void traverseDirectoryEntries(Uri rootUri) {
ContentResolver contentResolver = getActivity().getContentResolver();
Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, DocumentsContract.getTreeDocumentId(rootUri));
// Keep track of our directory hierarchy
List<Uri> dirNodes = new LinkedList<>();
dirNodes.add(childrenUri);
while(!dirNodes.isEmpty()) {
childrenUri = dirNodes.remove(0); // get the item from top
Log.d(TAG, "node uri: ", childrenUri);
Cursor c = contentResolver.query(childrenUri, new String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_MIME_TYPE}, null, null, null);
try {
while (c.moveToNext()) {
final String docId = c.getString(0);
final String name = c.getString(1);
final String mime = c.getString(2);
Log.d(TAG, "docId: " + id + ", name: " + name + ", mime: " + mime);
if(isDirectory(mime)) {
final Uri newNode = DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, docId);
dirNodes.add(newNode);
}
}
} finally {
closeQuietly(c);
}
}
}
// Util method to check if the mime type is a directory
private static boolean isDirectory(String mimeType) {
return DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType);
}
// Util method to close a closeable
private static void closeQuietly(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (RuntimeException re) {
throw re;
} catch (Exception ignore) {
// ignore exception
}
}
}
Первая итерация во внешнем цикле while успешна: вызов query
вернул действительный Cursor
для меня, чтобы пройти. Проблема заключается во второй итерации: когда я пытаюсь запросить Uri
, который оказывается подузлом rootUri
Я получаю SecurityException
указание документа xx не является потомком yy.
D / MyApp (19241): узел uri: content://com.android.mtp.documents/tree/2/document/2/children D/MyApp(19241): docId: 4, имя: DCIM, mime: vnd.android.document/directory D/MyApp(19241): uri узла: content://com.android.mtp.documents/tree/2/document/4/children E/DatabaseUtils(20944): запись исключения в пакет E/DatabaseUtils(20944): java.lang.SecurityException: документ 4 не является потомком 2
Кто-нибудь может дать некоторое представление о том, что я делаю неправильно? Если я использую другого поставщика источника данных, например, из внешнего хранилища (то есть SD-карты, подключенной через стандартный USB OTG-ридер), все работает нормально.
Дополнительная информация: я запускаю это на Nexus 6P, Android 7.1.1 и моем приложении minSdkVersion
19
3 ответа
После разных попыток я не уверен, что есть способ обойти это SecurityException
для источника данных MTP (если кто-то не может опровергнуть меня по этому вопросу). Глядя на DocumentProvider.java
Исходный код и трассировка стека, кажется, что вызов DocumentProvider#isChildDocument
внутри DocumentProvider#enforceTree
возможно, не были должным образом переопределены в MtpDocumentProvider
реализация в рамках Android. Реализация по умолчанию всегда возвращает false
, #грустное лицо
Я использую ваш код для перехода в подпапки с добавлением строк:
Uri childrenUri;
try {
//for childs and sub child dirs
childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri));
} catch (Exception e) {
// for parent dir
childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri));
}
Флаги, добавленные до начала действия для результата, ничего не делают.
Я не понимаю, что вы используете DocumentsContract. Пользователь выбирает каталог. Из URI вы можете построить DocumentFile
для этого каталога.
После этого использования DocumentFile::listFiles()
в этом случае, чтобы получить список подкаталогов и файлов.