Intereting Posts
Внедрение рекомендаций по дизайну Google для слайдеров Нужно ли пример кода о том, как запустить службу Android навсегда в фоновом режиме, даже когда устройство спит, например Whatsapp? Телефонная кнопка PushNotification для открытия отдельной страницы приложения Ориентация Android Compass на ненадежную (фильтр нижних частот) Преобразование в формат Dalvik не удалось с ошибкой 1 в Android на экспорт (adt 21) Android 6.0 (Marshmallow): Как играть в миди? Обновить текст уведомления, а не целое уведомление Общие элементы, анимация между фрагментами Список нескольких вариантов с пользовательским представлением? Как центрировать значок и текст в андроидной кнопке с шириной, заданной как «заполнить родительский элемент», Какова продолжительность анимации actionbar.show () и hide () В версии 2 Map не отображается карта ProgressDialog – как удалить номера Android JavascriptInterface Security? Как получить смещение часовой пояс в GMT (как GMT + 7: 00) с устройства Android?

Как использовать новый API доступа к SD-карте, представленный для Android 5.0 (Lollipop)?

Задний план

На Android 4.4 (KitKat) Google сделал доступ к SD-карте довольно ограниченным.

Начиная с Android Lollipop (5.0), разработчики могут использовать новый API, который просит пользователя подтвердить, чтобы разрешить доступ к определенным папкам, как написано в этой записи Google-Groups .

Проблема

Сообщение направляет вас посетить два веб-сайта:

  • https://android.googlesource.com/platform/development/+/android-5.0.0_r2/samples/Vault/src/com/example/android/vault/VaultProvider.java#258

Это похоже на внутренний пример (возможно, позже будет показан на демонстрационных примерах API), но понять, что происходит, довольно сложно.

  • http://developer.android.com/reference/android/support/v4/provider/DocumentFile.html

Это официальная документация по новому API, но в нем недостаточно информации о том, как ее использовать.

Вот что он вам говорит:

Если вам действительно нужен полный доступ ко всему поддереву документов, начните с запуска ACTION_OPEN_DOCUMENT_TREE, чтобы пользователь мог выбрать каталог. Затем передайте полученный getData () в fromTreeUri (Context, Uri), чтобы начать работу с выбранным пользователем деревом.

Когда вы перемещаете дерево экземпляров DocumentFile, вы всегда можете использовать getUri () для получения Uri, представляющего базовый документ для этого объекта, для использования с openInputStream (Uri) и т. Д.

Чтобы упростить код на устройствах, работающих под управлением KITKAT или ранее, вы можете использовать fromFile (File), который эмулирует поведение DocumentProvider.

Вопросы

У меня есть несколько вопросов о новом API:

  1. Как вы его используете?
  2. Согласно сообщению, ОС будет помнить, что приложению было предоставлено разрешение на доступ к файлам / папкам. Как вы проверяете, можете ли вы получить доступ к файлам / папкам? Есть ли функция, которая возвращает мне список файлов / папок, к которым я могу получить доступ?
  3. Как вы справляетесь с этой проблемой в Kitkat? Является ли это частью библиотеки поддержки?
  4. Есть ли экран настроек в ОС, который показывает, какие приложения имеют доступ к файлам / папкам?
  5. Что произойдет, если приложение установлено для нескольких пользователей на одном устройстве?
  6. Есть ли другая документация / учебник об этом новом API?
  7. Можно ли отозвать разрешения? Если да, есть ли намерение, которое отправляется в приложение?
  8. Будет ли запрос разрешать работу рекурсивно в выбранной папке?
  9. Будет ли использование разрешения также давать пользователю возможность множественного выбора по выбору пользователя? Или приложение должно конкретно указать намерение, какие файлы / папки разрешить?
  10. Есть ли способ эмулятора попробовать новый API? Я имею в виду, что у него есть раздел SD-карты, но он работает как основное внешнее хранилище, поэтому весь доступ к нему уже предоставлен (с использованием простого разрешения).
  11. Что происходит, когда пользователь заменяет SD-карту другим?

Solutions Collecting From Web of "Как использовать новый API доступа к SD-карте, представленный для Android 5.0 (Lollipop)?"

Много хороших вопросов, давайте копаем. 🙂

Как ты это используешь?

Вот отличный учебник для взаимодействия с платформой доступа к хранилищу в KitKat:

https://developer.android.com/guide/topics/providers/document-provider.html#client

Взаимодействие с новыми API в Lollipop очень похоже. Чтобы предложить пользователю выбрать дерево каталогов, вы можете запустить такое намерение следующим образом:

Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); startActivityForResult(intent, 42); 

Затем в вашем onActivityResult () вы можете передать выбранный пользователем Uri в новый класс помощника DocumentFile. Ниже приведен краткий пример, в котором перечислены файлы в выбранном каталоге, а затем создается новый файл:

 public void onActivityResult(int requestCode, int resultCode, Intent resultData) { if (resultCode == RESULT_OK) { Uri treeUri = resultData.getData(); DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri); // List all existing files inside picked directory for (DocumentFile file : pickedDir.listFiles()) { Log.d(TAG, "Found file " + file.getName() + " with size " + file.length()); } // Create a new file and write into it DocumentFile newFile = pickedDir.createFile("text/plain", "My Novel"); OutputStream out = getContentResolver().openOutputStream(newFile.getUri()); out.write("A long time ago...".getBytes()); out.close(); } } 

Uri, возвращенный DocumentFile.getUri() достаточно гибкий, чтобы использовать с различными API-интерфейсами платформы. Например, вы можете поделиться им с помощью Intent.setData() с Intent.FLAG_GRANT_READ_URI_PERMISSION .

Если вы хотите получить доступ к этому Uri из собственного кода, вы можете вызвать ContentResolver.openFileDescriptor() а затем использовать ParcelFileDescriptor.getFd() или detachFd() для получения традиционного целочисленного дескриптора файла POSIX.

Как вы проверяете, можете ли вы получить доступ к файлам / папкам?

По умолчанию Uris, возвращаемый с помощью Framework Storage Access Framework, не сохраняется при перезагрузках. Платформа «предлагает» возможность сохранения разрешения, но вам все равно нужно «взять» разрешение, если вы этого хотите. В нашем примере выше вы бы назвали:

  getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 

Вы всегда можете выяснить, какие сохраненные гранты вашему приложению имеют доступ через API ContentResolver.getPersistedUriPermissions() . Если вам больше не нужен доступ к сохраненному Uri, вы можете освободить его с помощью ContentResolver.releasePersistableUriPermission() .

Это доступно на KitKat?

Нет, мы не можем ретроактивно добавлять новые функции к более старым версиям платформы.

Могу ли я посмотреть, какие приложения имеют доступ к файлам / папкам?

В настоящее время нет пользовательского интерфейса, который показывает это, но вы можете найти подробности в разделе «Предоставленные разрешения Uri» для adb shell dumpsys activity providers для adb shell dumpsys activity providers .

Что произойдет, если приложение установлено для нескольких пользователей на одном устройстве?

Разрешения на разрешение Uri изолированы для каждого пользователя, так же как и все другие функциональности многопользовательской платформы. То есть, одно и то же приложение, работающее под двумя разными пользователями, не имеет дублирующих или общих разрешений Uri.

Можно ли отозвать разрешения?

Подпрограмма DocumentProvider может аннулировать разрешение в любое время, например, когда удаляется облачный документ. Наиболее распространенный способ обнаружения этих отозванных разрешений – это когда они исчезают из ContentResolver.getPersistedUriPermissions() упомянутых выше.

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

Будет ли запрос разрешать работу рекурсивно в выбранной папке?

Да, намерение ACTION_OPEN_DOCUMENT_TREE дает вам рекурсивный доступ как к существующим, так и к вновь созданным файлам и каталогам.

Предоставляет ли это возможность множественного выбора?

Да, с KitKat поддерживался множественный выбор, и вы можете разрешить его, установив EXTRA_ALLOW_MULTIPLE при запуске вашего намерения ACTION_OPEN_DOCUMENT . Вы можете использовать Intent.setType() или EXTRA_MIME_TYPES чтобы сузить типы файлов, которые можно выбрать:

http://developer.android.com/reference/android/content/Intent.html#ACTION_OPEN_DOCUMENT

Есть ли способ эмулятора попробовать новый API?

Да, основное устройство хранения данных должно появиться в сборщике, даже на эмуляторе. Если ваше приложение использует платформу доступа к хранилищу для доступа к разделяемому хранилищу, вам больше не нужны разрешения READ/WRITE_EXTERNAL_STORAGE и можно удалить их или использовать функцию android:maxSdkVersion чтобы запрашивать их только на более ранних версиях платформы.

Что происходит, когда пользователь заменяет SD-карту другим?

Когда задействуются физические носители, UUID (такой как серийный номер FAT) основного носителя всегда записывается в возвращаемый Uri. Система использует это, чтобы связать вас с носителем, который первоначально был выбран пользователем, даже если пользователь меняет носитель между несколькими слотами.

Если пользователь поменяется на второй карте, вам необходимо запросить доступ к новой карте. Поскольку система запоминает гранты на основе UUID, вы будете продолжать иметь ранее предоставленный доступ к исходной карте, если пользователь повторно вставляет ее позже.

http://en.wikipedia.org/wiki/Volume_serial_number

В моем проекте Android в Github, приведенном ниже, вы можете найти рабочий код, который позволяет писать на extSdCard на Android 5. Предполагается, что пользователь дает доступ ко всей SD-карте, а затем позволяет писать всюду на этой карте. (Если вы хотите иметь доступ только к одиночным файлам, все становится проще).

Снаплеты главного кода

Запуск платформы доступа к хранилищу:

 @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void triggerStorageAccessFramework() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS); } 

Обработка ответа из платформы доступа к хранилищу:

 @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Override public final void onActivityResult(final int requestCode, final int resultCode, final Intent resultData) { if (requestCode == SettingsFragment.REQUEST_CODE_STORAGE_ACCESS) { Uri treeUri = null; if (resultCode == Activity.RESULT_OK) { // Get Uri from Storage Access Framework. treeUri = resultData.getData(); // Persist URI in shared preference so that you can use it later. // Use your own framework here instead of PreferenceUtil. PreferenceUtil.setSharedPreferenceUri(R.string.key_internal_uri_extsdcard, treeUri); // Persist access permissions. final int takeFlags = resultData.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); getActivity().getContentResolver().takePersistableUriPermission(treeUri, takeFlags); } } } 

Получение выходного потока для файла через платформу доступа к хранилищу (с использованием сохраненного URL-адреса, предполагая, что это URL-адрес корневой папки внешней SD-карты)

 DocumentFile targetDocument = getDocumentFile(file, false); OutputStream outStream = Application.getAppContext(). getContentResolver().openOutputStream(targetDocument.getUri()); 

Это использует следующие вспомогательные методы:

 public static DocumentFile getDocumentFile(final File file, final boolean isDirectory) { String baseFolder = getExtSdCardFolder(file); if (baseFolder == null) { return null; } String relativePath = null; try { String fullPath = file.getCanonicalPath(); relativePath = fullPath.substring(baseFolder.length() + 1); } catch (IOException e) { return null; } Uri treeUri = PreferenceUtil.getSharedPreferenceUri(R.string.key_internal_uri_extsdcard); if (treeUri == null) { return null; } // start with root of SD card and then parse through document tree. DocumentFile document = DocumentFile.fromTreeUri(Application.getAppContext(), treeUri); String[] parts = relativePath.split("\\/"); for (int i = 0; i < parts.length; i++) { DocumentFile nextDocument = document.findFile(parts[i]); if (nextDocument == null) { if ((i < parts.length - 1) || isDirectory) { nextDocument = document.createDirectory(parts[i]); } else { nextDocument = document.createFile("image", parts[i]); } } document = nextDocument; } return document; } public static String getExtSdCardFolder(final File file) { String[] extSdPaths = getExtSdCardPaths(); try { for (int i = 0; i < extSdPaths.length; i++) { if (file.getCanonicalPath().startsWith(extSdPaths[i])) { return extSdPaths[i]; } } } catch (IOException e) { return null; } return null; } /** * Get a list of external SD card paths. (Kitkat or higher.) * * @return A list of external SD card paths. */ @TargetApi(Build.VERSION_CODES.KITKAT) private static String[] getExtSdCardPaths() { List<String> paths = new ArrayList<>(); for (File file : Application.getAppContext().getExternalFilesDirs("external")) { if (file != null && !file.equals(Application.getAppContext().getExternalFilesDir("external"))) { int index = file.getAbsolutePath().lastIndexOf("/Android/data"); if (index < 0) { Log.w(Application.TAG, "Unexpected external file dir: " + file.getAbsolutePath()); } else { String path = file.getAbsolutePath().substring(0, index); try { path = new File(path).getCanonicalPath(); } catch (IOException e) { // Keep non-canonical path. } paths.add(path); } } } return paths.toArray(new String[paths.size()]); } /** * Retrieve the application context. * * @return The (statically stored) application context */ public static Context getAppContext() { return Application.mApplication.getApplicationContext(); } 

Ссылка на полный код

https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/fragments/SettingsFragment.java#L521

а также

https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/FileUtil.java