Проблемы, проходящие через иерархию каталогов с помощью 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() в этом случае, чтобы получить список подкаталогов и файлов.

Другие вопросы по тегам