Как использовать новый API доступа к SD-карте, представленный для Android 5.0 (Lollipop)?
Фон
В Android 4.4 (KitKat), Google сделал доступ к SD-карте довольно ограниченным.
Начиная с Android Lollipop (5.0), разработчики могут использовать новый API, который просит пользователя подтвердить доступ к определенным папкам, как написано на это сообщение Google-Groups.
Проблема
Сообщение направляет вас посетить два веб-сайта:
Это выглядит как внутренний пример (возможно, позже будет показан в демонстрациях API), но довольно сложно понять, что происходит.
Это официальная документация по новому API, но в нем недостаточно информации о том, как ее использовать.
Вот что он вам говорит:
Если вам действительно нужен полный доступ ко всему поддереву документов, начните с запуска ACTION_OPEN_DOCUMENT_TREE, чтобы пользователь мог выбрать каталог. Затем передайте полученный getData() в fromTreeUri (Context, Uri), чтобы начать работу с выбранным пользователем деревом.
При навигации по дереву экземпляров DocumentFile вы всегда можете использовать getUri(), чтобы получить Uri, представляющий базовый документ для этот объект для использования с openInputStream (Uri) и т.д.
Чтобы упростить код на устройствах, работающих под управлением KITKAT или ранее, вы можете используйте fromFile (Файл), который эмулирует поведение DocumentProvider.
Вопросы
У меня есть несколько вопросов о новом API:
- Как вы его используете?
- Согласно сообщению, ОС будет помнить, что приложению было предоставлено разрешение на доступ к файлам/папкам. Как вы проверяете, можете ли вы получить доступ к файлам/папкам? Есть ли функция, которая возвращает мне список файлов/папок, к которым я могу получить доступ?
- Как вы справляетесь с этой проблемой в Kitkat? Является ли это частью библиотеки поддержки?
- Есть ли экран настроек в ОС, который показывает, какие приложения имеют доступ к файлам/папкам?
- Что произойдет, если приложение установлено для нескольких пользователей на одном устройстве?
- Есть ли другая документация/учебник об этом новом API?
- Можно ли отозвать разрешения? Если да, то есть ли намерение отправляться в приложение?
- Будет ли запрос разрешать работу рекурсивно в выбранной папке?
- Будет ли использование разрешения также давать пользователю возможность множественного выбора по выбору пользователя? Или приложение должно конкретно указывать, какие файлы/папки разрешить?
- Есть ли способ эмулятора попробовать новый API? Я имею в виду, что у него есть раздел SD-карты, но он работает как основное внешнее хранилище, поэтому весь доступ к нему уже предоставлен (с использованием простого разрешения).
- Что происходит, когда пользователь заменяет SD-карту другим?
Ответы
Ответ 1
Множество хороших вопросов, дайте понять.:)
Как вы его используете?
Здесь отличный учебник для взаимодействия с платформой доступа к хранилищу в 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, возвращаемый с помощью возможностей 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
.
Что произойдет, если приложение установлено для нескольких пользователей на одном устройстве?
Разрешения на разрешение Uri изолированы для каждого пользователя, как и все другие функции многопользовательской платформы. То есть, одно и то же приложение, работающее под двумя разными пользователями, не имеет наложенных или общих разрешений Uri.
Можно ли отозвать разрешения?
Подпрограмма DocumentProvider может аннулировать разрешение в любое время, например, когда удаляется облачный документ. Наиболее распространенный способ обнаружения этих отозванных разрешений - это когда они исчезают из ContentResolver.getPersistedUriPermissions()
, упомянутых выше.
Разрешения также отменяется всякий раз, когда данные приложения очищаются для любого приложения, участвующего в гранте.
Будет ли запрос рекурсивно работать в выбранной папке?
Yep, намерение 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
Ответ 2
В моем проекте Android в Github, приведенном ниже, вы можете найти рабочий код, который позволяет писать на extSdCard на Android 5. Предполагается, что пользователь дает доступ ко всей SD-карте, а затем позволяет писать всюду на этой карте. (Если вы хотите иметь доступ только к одиночным файлам, все становится проще.)
Snipplets главного кода
Запуск платформы доступа к хранилищу:
@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