На устройствах под управлением Android 4.4 (уровень API 19) и выше ваше приложение может взаимодействовать с поставщиком документов , включая внешние тома хранения и облачные хранилища, используя Storage Access Framework. Этот фреймворк позволяет пользователям взаимодействовать с системным средством выбора поставщика документов и выбирать конкретные документы и другие файлы для создания, открытия или изменения вашим приложением.
Поскольку пользователь участвует в выборе файлов или каталогов, к которым ваше приложение может получить доступ, этот механизм не требует никаких системных разрешений , что повышает контроль и конфиденциальность пользователя. Кроме того, эти файлы, которые хранятся вне каталога, предназначенного для конкретного приложения, и вне медиатеки, остаются на устройстве после удаления вашего приложения.
Использование данной структуры включает в себя следующие шаги:
- Приложение вызывает интент, содержащий действие, связанное с хранилищем. Это действие соответствует конкретному сценарию использования , который предоставляет фреймворк.
- Пользователь видит панель выбора системы, позволяющую просматривать поставщиков документов и выбирать место или документ, где будет выполняться действие, связанное с хранением.
- Приложение получает доступ на чтение и запись к URI, представляющему выбранное пользователем местоположение или документ. Используя этот URI, приложение может выполнять операции с выбранным местоположением .
Для поддержки доступа к медиафайлам на устройствах под управлением Android 9 (уровень API 28) или ниже необходимо объявить разрешение READ_EXTERNAL_STORAGE и установить значение maxSdkVersion равным 28 .
В этом руководстве описываются различные сценарии использования, которые поддерживает данная платформа для работы с файлами и другими документами. Также объясняется, как выполнять операции над выбранным пользователем местоположением.
Примеры использования для доступа к документам и другим файлам
Платформа Storage Access Framework поддерживает следующие сценарии использования для доступа к файлам и другим документам.
- Создайте новый файл
- Действие
ACTION_CREATE_DOCUMENTпозволяет пользователям сохранять файл в указанном месте. - Откройте документ или файл
- Действие
ACTION_OPEN_DOCUMENTпозволяет пользователям выбрать конкретный документ или файл для открытия. - Предоставить доступ к содержимому каталога
- Действие
ACTION_OPEN_DOCUMENT_TREE, доступное в Android 5.0 (уровень API 21) и выше, позволяет пользователям выбрать определенную директорию, предоставляя вашему приложению доступ ко всем файлам и подкаталогам в этой директории.
В следующих разделах приведены рекомендации по настройке каждого варианта использования.
Создайте новый файл
Используйте действие ACTION_CREATE_DOCUMENT , чтобы загрузить системное средство выбора файлов и позволить пользователю выбрать место для записи содержимого файла. Этот процесс аналогичен тому, который используется в диалоговых окнах «Сохранить как», применяемых в других операционных системах.
Примечание: ACTION_CREATE_DOCUMENT не может перезаписать существующий файл. Если ваше приложение попытается сохранить файл с тем же именем, система добавит число в скобках в конец имени файла.
Например, если ваше приложение попытается сохранить файл с именем confirmation.pdf в каталоге, где уже есть файл с таким именем, система сохранит новый файл с именем confirmation(1).pdf .
При настройке намерения укажите имя файла и MIME-тип, а также, при необходимости, укажите URI файла или каталога, который средство выбора файлов должно отображать при первой загрузке, используя дополнительный параметр намерения EXTRA_INITIAL_URI .
Следующий фрагмент кода показывает, как создать и вызвать Intent для создания файла:
Котлин
// Request code for creating a PDF document. const val CREATE_FILE = 1 private fun createFile(pickerInitialUri: Uri) { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "application/pdf" putExtra(Intent.EXTRA_TITLE, "invoice.pdf") // Optionally, specify a URI for the directory that should be opened in // the system file picker before your app creates the document. putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri) } startActivityForResult(intent, CREATE_FILE) }
Java
// Request code for creating a PDF document. private static final int CREATE_FILE = 1; private void createFile(Uri pickerInitialUri) { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("application/pdf"); intent.putExtra(Intent.EXTRA_TITLE, "invoice.pdf"); // Optionally, specify a URI for the directory that should be opened in // the system file picker when your app creates the document. intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri); startActivityForResult(intent, CREATE_FILE); }
Откройте файл
В вашем приложении документы могут использоваться в качестве единицы хранения, куда пользователи вводят данные, которыми они хотели бы поделиться с коллегами или импортировать в другие документы. В качестве примеров можно привести открытие пользователем документа, повышающего производительность, или книги, сохраненной в формате EPUB.
В таких случаях позвольте пользователю выбрать файл для открытия, вызвав интент ACTION_OPEN_DOCUMENT , который открывает приложение выбора файлов системы. Чтобы отображать только те типы файлов, которые поддерживает ваше приложение, укажите MIME-тип. Кроме того, вы можете дополнительно указать URI файла, который должно отображаться при первой загрузке приложения выбора файлов, используя дополнительный параметр интента EXTRA_INITIAL_URI .
Приведённый ниже фрагмент кода показывает, как создать и вызвать Intent для открытия PDF-документа:
Котлин
// Request code for selecting a PDF document. const val PICK_PDF_FILE = 2 fun openFile(pickerInitialUri: Uri) { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "application/pdf" // Optionally, specify a URI for the file that should appear in the // system file picker when it loads. putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri) } startActivityForResult(intent, PICK_PDF_FILE) }
Java
// Request code for selecting a PDF document. private static final int PICK_PDF_FILE = 2; private void openFile(Uri pickerInitialUri) { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("application/pdf"); // Optionally, specify a URI for the file that should appear in the // system file picker when it loads. intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri); startActivityForResult(intent, PICK_PDF_FILE); }
Ограничения доступа
В Android 11 (уровень API 30) и выше нельзя использовать действие интента ACTION_OPEN_DOCUMENT для запроса выбора пользователем отдельных файлов из следующих каталогов:
- Каталог
Android/data/и все его подкаталоги. - Каталог
Android/obb/и все его подкаталоги.
Предоставить доступ к содержимому каталога
Приложения для управления файлами и создания мультимедиа обычно управляют группами файлов в иерархии каталогов. Чтобы предоставить такую возможность в вашем приложении, используйте действие интента ACTION_OPEN_DOCUMENT_TREE , которое позволяет пользователю предоставить доступ ко всему дереву каталогов, за некоторыми исключениями, начиная с Android 11 (уровень API 30). После этого ваше приложение сможет получить доступ к любому файлу в выбранном каталоге и в любых его подкаталогах.
При использовании ACTION_OPEN_DOCUMENT_TREE ваше приложение получает доступ только к файлам в каталоге, выбранном пользователем. У вас нет доступа к файлам других приложений, находящихся за пределами этого выбранного пользователем каталога. Такой контролируемый пользователем доступ позволяет пользователям выбирать, какой контент они готовы предоставить вашему приложению.
При желании вы можете указать URI каталога, который средство выбора файлов должно отображать при первой загрузке, используя дополнительный параметр намерения EXTRA_INITIAL_URI .
Следующий фрагмент кода показывает, как создать и вызвать Intent для открытия каталога:
Котлин
fun openDirectory(pickerInitialUri: Uri) { // Choose a directory using the system's file picker. val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { // Optionally, specify a URI for the directory that should be opened in // the system file picker when it loads. putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri) } startActivityForResult(intent, your-request-code) }
Java
public void openDirectory(Uri uriToLoad) { // Choose a directory using the system's file picker. Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); // Optionally, specify a URI for the directory that should be opened in // the system file picker when it loads. intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uriToLoad); startActivityForResult(intent, your-request-code); }
Ограничения доступа
На Android 11 (уровень API 30) и выше вы не можете использовать действие интента ACTION_OPEN_DOCUMENT_TREE для запроса доступа к следующим каталогам:
- Корневой каталог внутреннего тома хранения.
- Корневой каталог каждого тома SD-карты, который производитель устройства считает надежным , независимо от того, эмулируется карта или является съемной. Надежный том — это тот, к которому приложение может успешно получить доступ в большинстве случаев.
- Каталог
Download.
Кроме того, в Android 11 (уровень API 30) и выше нельзя использовать действие интента ACTION_OPEN_DOCUMENT_TREE для запроса выбора пользователем отдельных файлов из следующих каталогов:
- Каталог
Android/data/и все его подкаталоги. - Каталог
Android/obb/и все его подкаталоги.
Выполнить операции в выбранном месте
После того, как пользователь выберет файл или каталог с помощью встроенного средства выбора файлов, вы можете получить URI выбранного элемента, используя следующий код в onActivityResult() :
Котлин
override fun onActivityResult( requestCode: Int, resultCode: Int, resultData: Intent?) { if (requestCode == your-request-code && resultCode == Activity.RESULT_OK) { // The result data contains a URI for the document or directory that // the user selected. resultData?.data?.also { uri -> // Perform operations on the document using its URI. } } }
Java
@Override public void onActivityResult(int requestCode, int resultCode, Intent resultData) { if (requestCode == your-request-code && resultCode == Activity.RESULT_OK) { // The result data contains a URI for the document or directory that // the user selected. Uri uri = null; if (resultData != null) { uri = resultData.getData(); // Perform operations on the document using its URI. } } }
Получив ссылку на URI выбранного элемента, ваше приложение может выполнять с ним ряд операций. Например, вы можете получить доступ к метаданным элемента, редактировать его на месте и удалять его.
В следующих разделах показано, как выполнять действия с файлами, выбранными пользователем.
Определите виды деятельности, которые поддерживает поставщик услуг.
Различные поставщики контента позволяют выполнять разные операции с документами, например, копировать документ или просматривать его миниатюру. Чтобы определить, какие операции поддерживает тот или иной поставщик, проверьте значение параметра Document.COLUMN_FLAGS . После этого пользовательский интерфейс вашего приложения сможет отображать только те параметры, которые поддерживает данный поставщик.
Сохранять разрешения
Когда ваше приложение открывает файл для чтения или записи, система предоставляет вашему приложению разрешение URI для этого файла, которое действует до перезагрузки устройства пользователя. Предположим, однако, что ваше приложение — это приложение для редактирования изображений, и вы хотите, чтобы пользователи могли получить доступ к 5 изображениям, которые они недавно редактировали, непосредственно из вашего приложения. Если устройство пользователя перезагрузилось, вам придется отправить пользователя обратно к системному средству выбора файлов, чтобы найти нужные файлы.
Для сохранения доступа к файлам после перезагрузки устройства и улучшения пользовательского опыта ваше приложение может «использовать» предоставляемое системой постоянное разрешение URI, как показано в следующем фрагменте кода:
Котлин
val contentResolver = applicationContext.contentResolver val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION // Check for the freshest data. contentResolver.takePersistableUriPermission(uri, takeFlags)
Java
final int takeFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); // Check for the freshest data. getContentResolver().takePersistableUriPermission(uri, takeFlags);
Изучите метаданные документа.
Имея URI документа, вы получаете доступ к его метаданным. Этот фрагмент кода извлекает метаданные документа, указанного в URI, и записывает их в лог:
Котлин
val contentResolver = applicationContext.contentResolver fun dumpImageMetaData(uri: Uri) { // The query, because it only applies to a single document, returns only // one row. There's no need to filter, sort, or select fields, // because we want all fields for one document. val cursor: Cursor? = contentResolver.query( uri, null, null, null, null, null) cursor?.use { // moveToFirst() returns false if the cursor has 0 rows. Very handy for // "if there's anything to look at, look at it" conditionals. if (it.moveToFirst()) { // Note it's called "Display Name". This is // provider-specific, and might not necessarily be the file name. val displayName: String = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME)) Log.i(TAG, "Display Name: $displayName") val sizeIndex: Int = it.getColumnIndex(OpenableColumns.SIZE) // If the size is unknown, the value stored is null. But because an // int can't be null, the behavior is implementation-specific, // and unpredictable. So as // a rule, check if it's null before assigning to an int. This will // happen often: The storage API allows for remote files, whose // size might not be locally known. val size: String = if (!it.isNull(sizeIndex)) { // Technically the column stores an int, but cursor.getString() // will do the conversion automatically. it.getString(sizeIndex) } else { "Unknown" } Log.i(TAG, "Size: $size") } } }
Java
public void dumpImageMetaData(Uri uri) { // The query, because it only applies to a single document, returns only // one row. There's no need to filter, sort, or select fields, // because we want all fields for one document. Cursor cursor = getActivity().getContentResolver() .query(uri, null, null, null, null, null); try { // moveToFirst() returns false if the cursor has 0 rows. Very handy for // "if there's anything to look at, look at it" conditionals. i&&f (cursor != null cursor.moveToFirst()) { // Note it's called "Display Name". This is // provider-specific, and might not necessarily be the file name. String displayName = cursor.getString( cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); Log.i(TAG, "Display Name: " + displayName); int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE); // If the size is unknown, the value stored is null. But because an // int can't be null, the behavior is implementation-specific, // and unpredictable. So as // a rule, check if it's null before assigning to an int. This will // happen often: The storage API allows for remote files, whose // size might not be locally known. String size = null; if (!cursor.isNull(sizeIndex)) { // Technically the column stores an int, but cursor.getString() // will do the conversion automatically. size = cursor.getString(sizeIndex); } else { size = "Unknown"; } Log.i(TAG, "Size: " + size); } } finally { cursor.close(); } }
Открыть документ
Имея ссылку на URI документа, вы можете открыть документ для дальнейшей обработки. В этом разделе приведены примеры открытия растрового изображения и входного потока.
Растровое изображение
Следующий фрагмент кода показывает, как открыть файл Bitmap по его URI:
Котлин
val contentResolver = applicationContext.contentResolver @Throws(IOException::class) private fun getBitmapFromUri(uri: Uri): Bitmap { val parcelFileDescriptor: ParcelFileDescriptor = contentResolver.openFileDescriptor(uri, "r") val fileDescriptor: FileDescriptor = parcelFileDescriptor.fileDescriptor val image: Bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor) parcelFileDescriptor.close() return image }
Java
private Bitmap getBitmapFromUri(Uri uri) throws IOException { ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(uri, "r"); FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor); parcelFileDescriptor.close(); return image; }
После открытия растрового изображения его можно отобразить в ImageView .
Входной поток
Следующий фрагмент кода показывает, как открыть объект InputStream по его URI. В этом фрагменте строки файла считываются в строку:
Котлин
val contentResolver = applicationContext.contentResolver @Throws(IOException::class) private fun readTextFromUri(uri: Uri): String { val stringBuilder = StringBuilder() contentResolver.openInputStream(uri)?.use { inputStream -> BufferedReader(InputStreamReader(inputStream)).use { reader -> var line: String? = reader.readLine() while (line != null) { stringBuilder.append(line) line = reader.readLine() } } } return stringBuilder.toString() }
Java
private String readTextFromUri(Uri uri) throws IOException { StringBuilder stringBuilder = new StringBuilder(); try (InputStream inputStream = getContentResolver().openInputStream(uri); BufferedReader reader = new BufferedReader( new InputStreamReader(Objects.requireNonNull(inputStream)))) { String line; while ((line = reader.readLine()) != null) { stringBuilder.append(line); } } return stringBuilder.toString(); }
Редактировать документ
С помощью Storage Access Framework можно редактировать текстовый документ прямо на месте.
Приведенный ниже фрагмент кода перезаписывает содержимое документа, представленного указанным URI:
Котлин
val contentResolver = applicationContext.contentResolver private fun alterDocument(uri: Uri) { try { contentResolver.openFileDescriptor(uri, "w")?.use { FileOutputStream(it.fileDescriptor).use { it.write( ("Overwritten at ${System.currentTimeMillis()}\n") .toByteArray() ) } } } catch (e: FileNotFoundException) { e.printStackTrace() } catch (e: IOException) { e.printStackTrace() } }
Java
private void alterDocument(Uri uri) { try { ParcelFileDescriptor pfd = getActivity().getContentResolver(). openFileDescriptor(uri, "w"); FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor()); fileOutputStream.write(("Overwritten at " + System.currentTimeMillis() + "\n").getBytes()); // Let the document provider know you're done by closing the stream. fileOutputStream.close(); pfd.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
Удалить документ
Если у вас есть URI документа, и Document.COLUMN_FLAGS этого документа содержится SUPPORTS_DELETE , вы можете удалить документ. Например:
Котлин
DocumentsContract.deleteDocument(applicationContext.contentResolver, uri)
Java
DocumentsContract.deleteDocument(applicationContext.contentResolver, uri);
Получить эквивалентный URI медиафайла
Метод getMediaUri() предоставляет URI хранилища медиафайлов, эквивалентный заданному URI поставщика документов. Оба URI относятся к одному и тому же элементу. Используя URI хранилища медиафайлов, вы можете проще получить доступ к медиафайлам из общего хранилища .
Метод getMediaUri() поддерживает URI ExternalStorageProvider . В Android 12 (уровень API 31) и выше этот метод также поддерживает URI MediaDocumentsProvider .
Откройте виртуальный файл
На Android 7.0 (уровень API 25) и выше ваше приложение может использовать виртуальные файлы, предоставляемые Storage Access Framework. Хотя виртуальные файлы не имеют двоичного представления, ваше приложение может открыть их содержимое, преобразуя их в другой тип файла или просматривая эти файлы с помощью действия ACTION_VIEW .
Для открытия виртуальных файлов вашему клиентскому приложению необходима специальная логика для их обработки. Если вам нужно получить байтовое представление файла — например, для предварительного просмотра — вам необходимо запросить у поставщика документов альтернативный MIME-тип.
После того, как пользователь сделает выбор, используйте URI в данных результатов, чтобы определить, является ли файл виртуальным, как показано в следующем фрагменте кода:
Котлин
private fun isVirtualFile(uri: Uri): Boolean { if (!DocumentsContract.isDocumentUri(this, uri)) { return false } val cursor: Cursor? = contentResolver.query( uri, arrayOf(DocumentsContract.Document.COLUMN_FLAGS), null, null, null ) val flags: Int = cursor?.use { if (cursor.moveToFirst()) { cursor.getInt(0) } else { 0 } } ?: 0 return flags and DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT != 0 }
Java
private boolean isVirtualFile(Uri uri) { if (!DocumentsContract.isDocumentUri(this, uri)) { return false; } Cursor cursor = getContentResolver().query( uri, new String[] { DocumentsContract.Document.COLUMN_FLAGS }, null, null, null); int flags = 0; if (cursor.moveToFirst()) { flags = cursor.getInt(0); } cursor.close(); return (flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) != 0; }
После проверки того, что документ является виртуальным файлом, вы можете преобразовать его в альтернативный MIME-тип, например, "image/png" . Следующий фрагмент кода показывает, как проверить, может ли виртуальный файл быть представлен как изображение, и если да, то получить входной поток из виртуального файла:
Котлин
@Throws(IOException::class) private fun getInputStreamForVirtualFile( uri: Uri, mimeTypeFilter: String): InputStream { val openableMimeTypes: Array<String>? = contentResolver.getStreamTypes(uri, mimeTypeFilter) return if (openableMimeTypes?.isNotEmpty() == true) { contentResolver .openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null) .createInputStream() } else { throw FileNotFoundException() } }
Java
private InputStream getInputStreamForVirtualFile(Uri uri, String mimeTypeFilter) throws IOException { ContentResolver resolver = getContentResolver(); String[] openableMimeTypes = resolver.getStreamTypes(uri, mimeTypeFilter); if (openableMimeTypes == null || openableMimeTypes.length < 1) { throw new FileNotFoundException(); } return resolver .openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null) .createInputStream(); }
Дополнительные ресурсы
Для получения дополнительной информации о том, как хранить документы и другие файлы, а также получать к ним доступ, обратитесь к следующим ресурсам.
Образцы
- ActionOpenDocument , доступен на GitHub.
- ActionOpenDocumentTree , доступен на GitHub.