Проблемы с прохождением через иерархию каталогов с 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.


person spring.ace    schedule 12.12.2016    source источник
comment
and I see my connected device recognized as the MTP data source.. Вы можете подробнее рассказать об этом? MTP? Вы подключили устройство к своему Android-устройству? Как? Что за устройство?   -  person greenapps    schedule 12.12.2016
comment
if(isDirectory(mime)) . Вы не разместили код для этой функции. И вы этого не объяснили.   -  person greenapps    schedule 12.12.2016
comment
У вас есть Uri rootUri и Uri uri. Но последнее не объясняется.   -  person greenapps    schedule 12.12.2016
comment
closeQuietly(childCursor); childCursor?   -  person greenapps    schedule 12.12.2016
comment
@greenapps привет, да, я подключил камеру к своему телефону Android с помощью кабеля USB-C - USB-C. Android успешно обнаружил устройство и определил, что я могу получить доступ к содержимому через MTP. методы isDirectory и closeQuietly - это просто вспомогательные методы. Я добавил код в свой отредактированный пост. rootUri - это Uri, возвращенный из onActivityResult. Я сделал ошибку копирования / вставки, которую исправил.   -  person spring.ace    schedule 12.12.2016
comment
and determined I can access the contents via MTP. Вы можете подробнее рассказать об этом? Что видит пользователь, благодаря которому он знает, что соединение является MTP? Или программист? Я не понимаю, что вы используете этот код провайдера mtp из своей ссылки, поскольку ваше приложение для Android не является поставщиком. Камера будет провайдером. Никогда не видел камеры, которая общается по мтп. Для меня это все в новинку.   -  person greenapps    schedule 12.12.2016
comment
and its trying to communicate via MTP. Откуда вы знаете? Я спрашивал об этом раньше! Какая марка / тип камеры? Я тоже спрашивал об этом раньше.   -  person greenapps    schedule 13.12.2016
comment
@greenapps, Мой телефон (Nexus 6P, Android 7.1.1) обнаружил, когда я подключил камеру напрямую к нему, что USB подключен (появилось уведомление). Я предполагаю, что он будет вести себя так же, как подключение SD-карты к моему телефону с помощью устройства чтения SD-карт, и рассматривать его как внешнее хранилище, которое, как я ЗНАЮ, работает. Поэтому в коде я просто использовал Android Storage Access Framework и запустил средство выбора, используя намерение ACTION_OPEN_DOCUMENT_TREE, и в результате я вижу устройство, подключенное к камере, в средстве выбора. Я не делаю ничего особенного.   -  person spring.ace    schedule 13.12.2016
comment
@greenapps, ДА! Я говорю вам, что мой телефон автоматически определяет, что устройство подключено !. И я использую экшн-камеру, точнее, профессиональную камеру.   -  person spring.ace    schedule 13.12.2016
comment
Пожалуйста, попробуйте свой код на устройстве Android 6 или более ранней версии.   -  person greenapps    schedule 14.12.2016
comment
Привет! Все еще здесь? Теперь я могу попробовать ваш код с камерой, подключенной к устройству Android. URI-подобный контент: //com.android.mtp.documents/tree/147, и ваш код выполнил свою работу.   -  person greenapps    schedule 31.03.2018
comment
Когда я пытаюсь рекурсивно использовать buildChildDocumentsUriUsingTree (uri, docId), он возвращает Uri с родительским деревом вместо дерева uri. Как мне это исправить?   -  person John Glen    schedule 18.10.2020


Ответы (3)


Я использую ваш код для входа в подпапки с добавлением строк:

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));
}
person Foobnix    schedule 15.03.2018

Флаги, добавленные перед запуском действия для результата, ничего не делают.

Я не понимаю, что вы используете DocumentsContract. Пользователь выбирает каталог. Из полученного вами uri вы можете создать DocumentFile для этого каталога.

После этого используйте DocumentFile::listFiles() на этом экземпляре, чтобы получить список подкаталогов и файлов.

person greenapps    schedule 12.12.2016
comment
Я хочу пройти по древовидной иерархии, начиная с rootUri. Я вызываю метод DocumentsContract для создания дочерних uri, чтобы я мог рекурсивно перемещаться по каталогу под ним. Итак, представьте, что у меня есть корневой узел A и дочерние каталоги B и C. А у B есть файлы с именами B1, B2, B3. В конечном итоге я хочу получить доступ к B1, B2, B3 (они могут быть файлами изображений), но проблема в том, что я не могу перейти к дочерним узлам B, потому что, как только я прошу дочерние узлы B, он дает мне SecurityException . - person spring.ace; 12.12.2016
comment
Вам не нужно снова объяснять, что вы хотите и что делаете. Я уже понял это и попробовал ваш код, который работал для SD-карты. Весь ваш код был для меня новым и очень интересным. Но я хотел бы знать, почему вы не используете предложенный мной код. Вы даже не реагируете на мое предложение. С моим кодом вы тоже можете перемещаться. В противном случае я бы его не разместил. - person greenapps; 12.12.2016
comment
Поскольку мне нужен прямой доступ к Uri, я не хочу просто перечислять их. Если я хочу открыть содержимое указанных каталогов, например файлы изображений, и открыть его через поток, я должен это сделать. Но да, позвольте мне попробовать ваш подход и рассказать, что происходит. Я просто отвечаю на комментарии, чтобы убедиться, что мои намерения ясны. - person spring.ace; 12.12.2016
comment
Как уже говорилось, вы можете просматривать и перечислять все с помощью DocumentFile. И используйте getContentResolver (). openInputStream (docfile.getUri ()) для чтения содержимого файлов. Это нормальный способ для потребителя. Между тем вы не объяснили, почему вы должны использовать код поставщика для потребителя. - person greenapps; 12.12.2016
comment
Я пробовал использовать DocumentFile#listFiles(), но это тоже не сработало. У меня такое же поведение. Каждый раз, когда я пытаюсь получить доступ к дочернему Uri, например, открывая его в потоке с помощью getContentResolver().openInputStream или даже вызывая DocumentFile#isDirectory(), он выдает то же самое SecurityException. Генерируемый Uri одинаков, использую ли я DocumentFile или через провайдера. Опять же, это только в том случае, если источником данных является источник данных MTP. Работает нормально, если это из внешнего хранилища (например, SD-карты) - person spring.ace; 13.12.2016
comment
Опять же: откуда вы знаете, что это источник данных mtp? Почему я должен спрашивать об этом снова и снова? Поймите, я никогда не видел ничего с mtp, поэтому объясните, что видит пользователь. - person greenapps; 13.12.2016
comment
Не знаю, как еще вам это объяснить. Я говорю вам, когда я запускаю средство выбора SAF, оно показывает его как источник данных MTP. Ничего особенного не делаю! Я посмотрел на код фреймворка Android и увидел внутренний MtpDocumentProvider, поэтому я предполагаю, что фреймворк знает, как запустить этот элемент, когда я запускаю ACTION_OPEN_DOCUMENT_TREE намерение. Я не знаю, как еще объяснить вам, если я не покажу вам его фотографию. - person spring.ace; 13.12.2016
comment
Я не уверен, где это отключение. Суть Android Storage Access Framework состоит в том, чтобы абстрагировать источник данных от пользователя независимо от того, откуда он. В коде приложения нет кода MTP, потому что он мне не нужен; Я полагаюсь на фреймворк, чтобы дать мне Uri указание на источник, и меня не волнует, исходит ли он из источника MTP, внешнего источника хранения, облачного источника и т. Д. Я взаимодействую с ContentProvider. В этом суть SAF. Единственное, что у меня есть, что я использую источник данных MTP, - это то, что сборщик сказал это в названии, MTP! - person spring.ace; 13.12.2016
comment
the SAF picker it shows it as an MTP Data Source. Ok. Теперь я знаю. Это то, что видит пользователь. - person greenapps; 13.12.2016

После разных попыток я не уверен, что есть способ обойти это SecurityException для источника данных MTP (если только кто-то не может опровергнуть меня по этому поводу). Глядя на DocumentProvider.java исходный код и трассировку стека, кажется, что вызов DocumentProvider#isChildDocument внутри DocumentProvider#enforceTree, возможно, не был должным образом переопределен в реализации MtpDocumentProvider в платформе Android. Реализация по умолчанию всегда возвращает false. #грустное лицо

person spring.ace    schedule 13.12.2016
comment
Может быть. Но эти функции не используются, если вы используете только DocumentFile, как я. Так откуда же тогда появиться исключение? - person greenapps; 13.12.2016
comment
Проблема заключается в том, что вы запрашиваете Uri для рассматриваемого элемента. Чтобы получить доступ к содержимому DocumentFile, вам все равно нужно получить от Uri до DocumentFile#getUri, иначе DocumentFile использует эту Uri ссылку для выполнения своих задач (например, вызов DocumentFile#isDirectory вызывает то же исключение). Итак, я предполагаю, что DocumentFile запрашивает этот Uri через преобразователь содержимого, который, в свою очередь, вызывает конкретную реализацию queryChildren внутри, которая в конечном итоге вызывает enforceTree, который затем выдает исключение. Это мое предположение. - person spring.ace; 13.12.2016
comment
К сожалению, у меня нет возможности пройти тестирование. Более того, реализована ли обработка mtp для других версий, кроме Android 7? - person greenapps; 13.12.2016
comment
may not have been properly overriden in the MtpDocumentProvider implementation in the Android framework. Извините, но я не могу представить, что используется этот код. У вашего провайдера есть профессиональная камера, и как вы узнаете, как она там реализована? - person greenapps; 14.12.2016