Как использовать новый 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(File), который эмулирует поведение DocumentsProvider.
Вопросы
У меня есть несколько вопросов о новом API:
- Как вы на самом деле используете это?
- Согласно сообщению, ОС запомнит, что приложению было дано разрешение на доступ к файлам / папкам. Как проверить, есть ли у вас доступ к файлам / папкам? Есть ли функция, которая возвращает мне список файлов / папок, к которым я могу получить доступ?
- Как вы решаете эту проблему на Kitkat? Это часть библиотеки поддержки?
- Есть ли в ОС экран настроек, который показывает, какие приложения имеют доступ к каким файлам / папкам?
- Что произойдет, если приложение установлено для нескольких пользователей на одном устройстве?
- Есть ли другая документация / руководство по этому новому API?
- Можно ли отозвать разрешения? Если да, есть ли намерение, которое отправляется в приложение?
- Будет ли запрашивать разрешение рекурсивно работать на выбранной папке?
- Позволит ли использование разрешения также дать пользователю возможность множественного выбора по выбору пользователя? Или приложение должно специально указать намерение, какие файлы / папки разрешить?
- Есть ли на эмуляторе способ попробовать новый API? Я имею в виду, что у него есть раздел SD-карты, но он работает как основное внешнее хранилище, поэтому весь доступ к нему уже предоставлен (с помощью простого разрешения).
- Что происходит, когда пользователь заменяет SD-карту на другую?
3 ответа
Много хороших вопросов, давайте копаться.:)
Как вы используете его?
Вот отличное руководство по взаимодействию с Storage Access Framework в KitKat:
https://developer.android.com/guide/topics/providers/document-provider.html
Взаимодействие с новыми 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();
}
}
Ури вернулся DocumentFile.getUri()
является достаточно гибким для использования с различными API-интерфейсами платформы. Например, вы можете поделиться им, используя Intent.setData()
с Intent.FLAG_GRANT_READ_URI_PERMISSION
,
Если вы хотите получить доступ к этому Uri из собственного кода, вы можете позвонить ContentResolver.openFileDescriptor()
а затем использовать ParcelFileDescriptor.getFd()
или же detachFd()
получить традиционное целое число дескриптора файла POSIX.
Как проверить, есть ли у вас доступ к файлам / папкам?
По умолчанию Uris, возвращенный через намерения Storage Access Frameworks, не сохраняется при перезагрузке. Платформа "предлагает" возможность сохранить разрешение, но вам все равно нужно "получить" разрешение, если вы этого хотите. В нашем примере выше вы бы позвонили:
getContentResolver().takePersistableUriPermission(treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
Вы всегда можете выяснить, какие постоянные гранты имеет доступ к вашему приложению через ContentResolver.getPersistedUriPermissions()
API. Если вам больше не нужен доступ к сохраненному Uri, вы можете освободить его с помощью ContentResolver.releasePersistableUriPermission()
,
Это доступно на KitKat?
Нет, мы не можем задним числом добавить новые функциональные возможности в более старые версии платформы.
Могу ли я увидеть, какие приложения имеют доступ к файлам / папкам?
В настоящее время нет пользовательского интерфейса, который показывает это, но вы можете найти подробности в разделе "Предоставленные разрешения Uri" adb shell dumpsys activity providers
выход.
Что произойдет, если приложение установлено для нескольких пользователей на одном устройстве?
Разрешения Uri изолированы для каждого пользователя, как и все другие функции многопользовательской платформы. То есть одно и то же приложение, работающее под двумя разными пользователями, не имеет перекрывающихся или общих разрешений Uri.
Можно ли отозвать разрешения?
Поддерживающий DocumentProvider может отозвать разрешение в любое время, например, при удалении облачного документа. Наиболее распространенный способ обнаружить эти отозванные разрешения - это когда они исчезают из ContentResolver.getPersistedUriPermissions()
упомянутое выше.
Разрешения также аннулируются всякий раз, когда данные приложения очищаются для любого приложения, участвующего в предоставлении.
Будет ли запрашивать разрешение рекурсивно работать на выбранной папке?
Да, ACTION_OPEN_DOCUMENT_TREE
intent предоставляет рекурсивный доступ как к существующим, так и к вновь созданным файлам и каталогам.
Это позволяет множественный выбор?
Да, множественный выбор поддерживается с KitKat, и вы можете разрешить его, установив EXTRA_ALLOW_MULTIPLE
при запуске вашего ACTION_OPEN_DOCUMENT
намерение. Ты можешь использовать Intent.setType()
или же EXTRA_MIME_TYPES
сузить типы файлов, которые можно выбрать:
http://developer.android.com/reference/android/content/Intent.html
Есть ли на эмуляторе способ попробовать новый API?
Да, основное устройство общего хранения должно появляться в средстве выбора даже на эмуляторе. Если ваше приложение использует только Storage Access Framework для доступа к общему хранилищу, вам больше не нужно READ/WRITE_EXTERNAL_STORAGE
разрешения на все и могут удалить их или использовать android:maxSdkVersion
возможность запрашивать их только на старых версиях платформы.
Что происходит, когда пользователь заменяет SD-карту на другую?
Когда задействован физический носитель, UUID (такой как серийный номер FAT) базового носителя всегда записывается в возвращенный Uri. Система использует это, чтобы подключить вас к носителю, который первоначально выбрал пользователь, даже если пользователь меняет носитель между несколькими слотами.
Если пользователь поменяет вторую карту, вам нужно будет запросить доступ к новой карте. Поскольку система запоминает разрешения для каждого UUID, у вас останется ранее предоставленный доступ к исходной карте, если пользователь повторно вставит ее позже.
В моем 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);
}
Обработка ответа от Storage Access Framework:
@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);
}
}
}
Получение outputStream для файла через Storage Access Framework (используя сохраненный 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();
}
Ссылка на полный код
а также
Это просто дополнительный ответ.
После создания нового файла вам может потребоваться сохранить его местоположение в базе данных и прочитать его завтра. Вы можете прочитать, восстановить его снова, используя этот метод:
/**
* Get {@link DocumentFile} object from SD card.
* @param directory SD card ID followed by directory name, for example {@code 6881-2249:Download/Archive},
* where ID for SD card is {@code 6881-2249}
* @param fileName for example {@code intel_haxm.zip}
* @return <code>null</code> if does not exist
*/
public static DocumentFile getExternalFile(Context context, String directory, String fileName){
Uri uri = Uri.parse("content://com.android.externalstorage.documents/tree/" + directory);
DocumentFile parent = DocumentFile.fromTreeUri(context, uri);
return parent != null ? parent.findFile(fileName) : null;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == SettingsFragment.REQUEST_CODE_STORAGE_ACCESS && resultCode == RESULT_OK) {
int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
getContentResolver().takePersistableUriPermission(data.getData(), takeFlags);
String sdcard = data.getDataString().replace("content://com.android.externalstorage.documents/tree/", "");
try {
sdcard = URLDecoder.decode(sdcard, "ISO-8859-1");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// for example, sdcardId results "6312-2234"
String sdcardId = sdcard.substring(0, sdcard.indexOf(':'));
// save to preferences if you want to use it later
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
preferences.edit().putString("sdcard", sdcardId).apply();
}
}