На устройствах с Android 4.4 (уровень API 19) и более поздними версиями приложение может взаимодействовать с поставщиком документов, включая внешние накопители и облачные хранилища, с помощью Storage Access Framework. Этот фреймворк позволяет пользователям взаимодействовать с системным инструментом выбора, чтобы указать поставщика документов и выбрать определенные документы и другие файлы, которые ваше приложение может создавать, открывать или изменять.
Поскольку пользователь сам выбирает файлы и каталоги, к которым у приложения будет доступ, этот механизм не требует системных разрешений, а пользователь получает больше контроля над своими данными. Кроме того, эти файлы, которые хранятся за пределами каталога приложения и хранилища мультимедиа, остаются на устройстве после удаления приложения.
Чтобы использовать фреймворк, выполните следующие действия:
- Приложение вызывает намерение, которое содержит действие, связанное с хранилищем. Это действие соответствует определенному варианту использования, который доступен в фреймворке.
- Пользователь видит системный инструмент выбора, позволяющий ему просматривать поставщика документов и выбирать местоположение или документ, где выполняется действие, связанное с хранилищем.
- Приложение получает доступ на чтение и запись к URI, который представляет выбранное пользователем местоположение или документ. С помощью этого URI приложение может выполнять операции с выбранным местоположением.
Чтобы обеспечить доступ к медиафайлам на устройствах с Android 9 (уровень API 28) или более ранней версией, объявите разрешение READ_EXTERNAL_STORAGE и задайте для maxSdkVersion значение 28.
В этом руководстве описаны различные варианты использования фреймворка для работы с файлами и другими документами. и выполнять операции с выбранным местоположением.
Примеры использования для доступа к документам и другим файлам
Платформа доступа к хранилищу поддерживает следующие сценарии доступа к файлам и другим документам:
- Как создать файл
- Действие намерения
ACTION_CREATE_DOCUMENTпозволяет пользователям сохранять файл в определенном месте. - Как открыть документ или файл
- Действие intent
ACTION_OPEN_DOCUMENTпозволяет пользователям выбрать документ или файл, который нужно открыть. - Как предоставить доступ к содержимому каталога
- Действие намерения
ACTION_OPEN_DOCUMENT_TREE, доступное в Android 5.0 (уровень API 21) и более поздних версиях, позволяет пользователям выбирать определенный каталог, предоставляя приложению доступ ко всем файлам и подкаталогам в нем.
В следующих разделах приведены инструкции по настройке для каждого случая.
Создать файл
Используйте действие intent ACTION_CREATE_DOCUMENT, чтобы загрузить системное окно выбора файлов и позволить пользователю выбрать место для записи содержимого файла. Этот процесс похож на тот, который используется в диалоговых окнах "Сохранить как" в других операционных системах.
Примечание. ACTION_CREATE_DOCUMENT не может перезаписать существующий файл. Если приложение попытается сохранить файл с тем же именем, система добавит в конец имени файла число в скобках.
Например, если приложение пытается сохранить файл с названием confirmation.pdf в каталоге, где уже есть файл с таким названием, система сохранит новый файл под названием confirmation(1).pdf.
При настройке намерения укажите название и MIME-тип файла, а также (необязательно) URI файла или каталога, который должен отображаться в окне выбора файлов при первой загрузке, используя дополнительное намерение EXTRA_INITIAL_URI.
В следующем фрагменте кода показано, как создать и вызвать намерение для создания файла:
Kotlin
// 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 файла, который должен отображаться в окне выбора файлов при его первой загрузке, используя дополнительный параметр intent EXTRA_INITIAL_URI.
В следующем фрагменте кода показано, как создать и вызвать намерение для открытия PDF-документа:
Kotlin
// 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) и более поздних версиях нельзя использовать действие intent ACTION_OPEN_DOCUMENT, чтобы запрашивать у пользователя выбор отдельных файлов из следующих каталогов:
- Каталог
Android/data/и все его подкаталоги. - Каталог
Android/obb/и все его подкаталоги.
Как предоставить доступ к содержимому каталога
Приложения для управления файлами и создания медиаконтента обычно управляют группами файлов в иерархии каталогов. Чтобы реализовать эту возможность в приложении, используйте действие интента ACTION_OPEN_DOCUMENT_TREE, которое позволяет пользователю предоставить доступ ко всему дереву каталогов, за некоторыми исключениями, начиная с Android 11 (уровня API 30). После этого приложение сможет получить доступ к любому файлу в выбранном каталоге и его подкаталогах.
При использовании ACTION_OPEN_DOCUMENT_TREE приложение получает доступ только к файлам в каталоге, выбранном пользователем. У вас не будет доступа к файлам других приложений, которые находятся за пределами выбранного пользователем каталога. Это позволяет пользователям выбирать, каким именно контентом они готовы поделиться с вашим приложением.
При необходимости вы можете указать URI каталога, который должен отображаться в окне выбора файлов при его первой загрузке, с помощью дополнительного параметра намерения EXTRA_INITIAL_URI.
В приведенном ниже фрагменте кода показано, как создать и вызвать намерение для открытия каталога.
Kotlin
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) и более поздних версиях нельзя использовать действие intent ACTION_OPEN_DOCUMENT_TREE, чтобы запрашивать доступ к следующим каталогам:
- Корневой каталог внутренней памяти.
- Корневой каталог каждого тома SD-карты, который производитель устройства считает надежным, независимо от того, является ли карта эмулированной или съемной. Надежным считается том, к которому приложение может успешно получить доступ в большинстве случаев.
- Каталог
Download.
Кроме того, в Android 11 (уровень API 30) и более поздних версиях нельзя использовать действие интента ACTION_OPEN_DOCUMENT_TREE, чтобы запрашивать у пользователя выбор отдельных файлов из следующих каталогов:
- Каталог
Android/data/и все его подкаталоги. - Каталог
Android/obb/и все его подкаталоги.
Выполнять операции в выбранном местоположении
После того как пользователь выберет файл или каталог в окне выбора файлов, вы можете получить URI выбранного объекта, используя следующий код в onActivityResult():
Kotlin
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 для этого файла, которое действует до перезапуска устройства пользователя. Однако предположим, что ваше приложение предназначено для редактирования изображений и вы хотите, чтобы пользователи могли получать доступ к пяти последним отредактированным изображениям непосредственно из вашего приложения. Если устройство пользователя было перезапущено, вам придется отправить пользователя обратно к системному инструменту выбора, чтобы найти файлы.
Чтобы сохранить доступ к файлам после перезапуска устройства и сделать приложение удобнее, вы можете использовать разрешение на постоянный доступ к URI, которое предлагает система, как показано в следующем фрагменте кода:
Kotlin
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, и записывает их в журнал.
Kotlin
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. if (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:
Kotlin
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. В этом фрагменте строки файла считываются в строку:
Kotlin
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:
Kotlin
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, вы можете удалить документ. Пример:
Kotlin
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 в данных результатов, чтобы определить, является ли файл виртуальным, как показано в следующем фрагменте кода:
Kotlin
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". В следующем фрагменте кода показано, как проверить, можно ли представить виртуальный файл в виде изображения, и если да, то как получить входной поток из виртуального файла:
Kotlin
@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.