Если вы разрабатываете приложение, которое предоставляет услуги хранения файлов (например, сервис сохранения в облаке), вы можете сделать свои файлы доступными через Storage Access Framework (SAF), написав собственный поставщик документов. This page describes how to create a custom document provider.
Подробную информацию о том, как работает Storage Access Framework, можно найти в обзоре Storage Access Framework.
Манифест
Чтобы реализовать собственный поставщик документов, добавьте в манифест приложения следующее:
- Целевой уровень API не ниже 19.
- Элемент
<provider>, который объявляет ваш собственный поставщик хранилища. -
Атрибут
android:name, в котором указано название подклассаDocumentsProvider(название класса, включая название пакета):com.example.android.storageprovider.MyCloudProvider. -
Атрибут
android:authority, который состоит из названия пакета (в этом примере –com.example.android.storageprovider) и типа поставщика контента (documents). - Для атрибута
android:exportedзадано значение"true". Чтобы другие приложения могли видеть поставщика, его нужно экспортировать. - Атрибут
android:grantUriPermissionsсо значением"true". Этот параметр позволяет системе предоставлять другим приложениям доступ к контенту вашего поставщика. О том, как другие приложения могут сохранять доступ к контенту вашего поставщика, читайте в разделе Сохранение разрешений. - Разрешение
MANAGE_DOCUMENTS. По умолчанию поставщик доступен всем. Добавление этого разрешения ограничивает доступ к вашему поставщику. Это ограничение важно для безопасности. - Фильтр интентов, который включает действие
android.content.action.DOCUMENTS_PROVIDER, чтобы ваш поставщик появлялся в окне выбора, когда система ищет поставщиков.
Ниже приведены фрагменты примера манифеста, в котором указан поставщик.
<manifest... > ... <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="19" /> .... <provider android:name="com.example.android.storageprovider.MyCloudProvider" android:authorities="com.example.android.storageprovider.documents" android:grantUriPermissions="true" android:exported="true" android:permission="android.permission.MANAGE_DOCUMENTS"> <intent-filter> <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> </intent-filter> </provider> </application> </manifest>
Поддержка устройств с Android 4.3 и более ранними версиями
Намерение ACTION_OPEN_DOCUMENT доступно только на устройствах с Android 4.4 и более поздними версиями.
Если вы хотите, чтобы ваше приложение поддерживало ACTION_GET_CONTENT для устройств с Android 4.3 и более ранними версиями, отключите фильтр интентов ACTION_GET_CONTENT в манифесте для устройств с Android 4.4 и более поздними версиями. Поставщик документов и ACTION_GET_CONTENT должны быть взаимоисключающими. Если вы поддерживаете оба варианта одновременно, ваше приложение дважды появится в интерфейсе системного инструмента выбора, предлагая два разных способа доступа к сохраненным данным. Это может запутать пользователей.
Ниже описан рекомендуемый способ отключения фильтра интентов ACTION_GET_CONTENT для устройств с Android 4.4 или более поздней версии.
- Добавьте в файл ресурсов
bool.xmlв каталогеres/values/следующую строку:<bool name="atMostJellyBeanMR2">true</bool> - Добавьте в файл ресурсов
bool.xmlв каталогеres/values-v19/следующую строку:<bool name="atMostJellyBeanMR2">false</bool> - Добавьте псевдоним действия, чтобы отключить фильтр интентов
ACTION_GET_CONTENTдля версий 4.4 (уровень API 19) и выше. Пример:<!-- This activity alias is added so that GET_CONTENT intent-filter can be disabled for builds on API level 19 and higher. --> <activity-alias android:name="com.android.example.app.MyPicker" android:targetActivity="com.android.example.app.MyActivity" ... android:enabled="@bool/atMostJellyBeanMR2"> <intent-filter> <action android:name="android.intent.action.GET_CONTENT" /> <category android:name="android.intent.category.OPENABLE" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="image/*" /> <data android:mimeType="video/*" /> </intent-filter> </activity-alias>
Соглашения
Обычно при написании собственного поставщика контента одной из задач является реализация классов контрактов, как описано в руководстве для разработчиков
Поставщики контента. Класс контракта – это класс public final, который содержит определения констант для URI, названий столбцов, MIME-типов и других метаданных, относящихся к поставщику. SAF предоставляет следующие классы контрактов, поэтому вам не нужно писать собственные:
Например, ниже приведены столбцы, которые могут быть возвращены в курсоре, когда поставщик документов запрашивает документы или корневой каталог:
Kotlin
private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf( DocumentsContract.Root.COLUMN_ROOT_ID, DocumentsContract.Root.COLUMN_MIME_TYPES, DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.COLUMN_ICON, DocumentsContract.Root.COLUMN_TITLE, DocumentsContract.Root.COLUMN_SUMMARY, DocumentsContract.Root.COLUMN_DOCUMENT_ID, DocumentsContract.Root.COLUMN_AVAILABLE_BYTES ) private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf( DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_LAST_MODIFIED, DocumentsContract.Document.COLUMN_FLAGS, DocumentsContract.Document.COLUMN_SIZE )
Java
private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES,}; private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};
Курсор для корневого элемента должен включать определенные обязательные столбцы. Эти столбцы перечислены далее.
Курсор для документов должен содержать следующие обязательные столбцы:
COLUMN_DOCUMENT_IDCOLUMN_DISPLAY_NAMECOLUMN_MIME_TYPECOLUMN_FLAGSCOLUMN_SIZECOLUMN_LAST_MODIFIED
Создание подкласса DocumentsProvider
Следующий шаг при написании пользовательского поставщика документов – создание подкласса абстрактного класса DocumentsProvider. Как минимум, необходимо реализовать следующие методы:
Это единственные методы, которые вы обязаны реализовать, но есть и другие, которые могут вам пригодиться. Подробную информацию можно найти в статье DocumentsProvider.
Определить корень
Реализация queryRoots() должна возвращать Cursor, указывающий на все корневые каталоги поставщика документов, используя столбцы, определенные в DocumentsContract.Root.
В следующем фрагменте параметр projection представляет определенные поля, которые вызывающий абонент хочет получить обратно. Фрагмент создает новый курсор и добавляет в него одну строку – один корень, каталог верхнего уровня, например "Скачанные файлы" или "Изображения". У большинства поставщиков только один корневой сертификат. У вас может быть несколько аккаунтов, например, если у вас несколько аккаунтов пользователей. В этом случае просто добавьте
вторую строку к курсору.
Kotlin
override fun queryRoots(projection: Array<out String>?): Cursor { // Use a MatrixCursor to build a cursor // with either the requested fields, or the default // projection if "projection" is null. val result = MatrixCursor(resolveRootProjection(projection)) // If user is not logged in, return an empty root cursor. This removes our // provider from the list entirely. if (!isUserLoggedIn()) { return result } // It's possible to have multiple roots (e.g. for multiple accounts in the // same app) -- just add multiple cursor rows. result.newRow().apply { add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT) // You can provide an optional summary, which helps distinguish roots // with the same title. You can also use this field for displaying an // user account name. add(DocumentsContract.Root.COLUMN_SUMMARY, context.getString(R.string.root_summary)) // FLAG_SUPPORTS_CREATE means at least one directory under the root supports // creating documents. FLAG_SUPPORTS_RECENTS means your application's most // recently used documents will show up in the "Recents" category. // FLAG_SUPPORTS_SEARCH allows users to search all documents the application // shares. add( DocumentsContract.Root.COLUMN_FLAGS, DocumentsContract.Root.FLAG_SUPPORTS_CREATE or DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or DocumentsContract.Root.FLAG_SUPPORTS_SEARCH ) // COLUMN_TITLE is the root title (e.g. Gallery, Drive). add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.title)) // This document id cannot change after it's shared. add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir)) // The child MIME types are used to filter the roots and only present to the // user those roots that contain the desired type somewhere in their file hierarchy. add(DocumentsContract.Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir)) add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDir.freeSpace) add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher) } return result }
Java
@Override public Cursor queryRoots(String[] projection) throws FileNotFoundException { // Use a MatrixCursor to build a cursor // with either the requested fields, or the default // projection if "projection" is null. final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); // If user is not logged in, return an empty root cursor. This removes our // provider from the list entirely. if (!isUserLoggedIn()) { return result; } // It's possible to have multiple roots (e.g. for multiple accounts in the // same app) -- just add multiple cursor rows. final MatrixCursor.RowBuilder row = result.newRow(); row.add(Root.COLUMN_ROOT_ID, ROOT); // You can provide an optional summary, which helps distinguish roots // with the same title. You can also use this field for displaying an // user account name. row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary)); // FLAG_SUPPORTS_CREATE means at least one directory under the root supports // creating documents. FLAG_SUPPORTS_RECENTS means your application's most // recently used documents will show up in the "Recents" category. // FLAG_SUPPORTS_SEARCH allows users to search all documents the application // shares. row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_SEARCH); // COLUMN_TITLE is the root title (e.g. Gallery, Drive). row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title)); // This document id cannot change after it's shared. row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir)); // The child MIME types are used to filter the roots and only present to the // user those roots that contain the desired type somewhere in their file hierarchy. row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir)); row.add(Root.COLUMN_AVAILABLE_BYTES, baseDir.getFreeSpace()); row.add(Root.COLUMN_ICON, R.drawable.ic_launcher); return result; }
Если ваш поставщик документов подключается к динамическому набору корней (например, к USB-устройству, которое может быть отключено, или к учетной записи, из которой пользователь может выйти), вы можете обновить пользовательский интерфейс документа, чтобы синхронизировать его с этими изменениями, используя метод ContentResolver.notifyChange(), как показано в следующем фрагменте кода.
Kotlin
val rootsUri: Uri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY) context.contentResolver.notifyChange(rootsUri, null)
Java
Uri rootsUri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY); context.getContentResolver().notifyChange(rootsUri, null);
Просмотр документов в поставщике
Ваша реализация queryChildDocuments() должна возвращать Cursor, указывающий на все файлы в указанном каталоге, используя столбцы, определенные в DocumentsContract.Document.
Этот метод вызывается, когда пользователь выбирает ваш корневой каталог в интерфейсе выбора.
Метод извлекает дочерние элементы идентификатора документа, указанного в COLUMN_DOCUMENT_ID.
Система вызывает этот метод каждый раз, когда пользователь выбирает подкаталог в поставщике документов.
Этот фрагмент кода создает новый курсор с запрошенными столбцами, а затем добавляет в него информацию о каждом дочернем элементе в родительском каталоге. Дочерним элементом может быть изображение, другой каталог – любой файл:
Kotlin
override fun queryChildDocuments( parentDocumentId: String?, projection: Array<out String>?, sortOrder: String? ): Cursor { return MatrixCursor(resolveDocumentProjection(projection)).apply { val parent: File = getFileForDocId(parentDocumentId) parent.listFiles() .forEach { file -> includeFile(this, null, file) } } }
Java
@Override public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); final File parent = getFileForDocId(parentDocumentId); for (File file : parent.listFiles()) { // Adds the file's display name, MIME type, size, and so on. includeFile(result, null, file); } return result; }
Получить информацию о документе
Ваша реализация queryDocument() должна возвращать Cursor, указывающий на заданный файл, с использованием столбцов, определенных в DocumentsContract.Document.
Метод queryDocument() возвращает ту же информацию, которая была передана в queryChildDocuments(), но для определенного файла:
Kotlin
override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor { // Create a cursor with the requested projection, or the default projection. return MatrixCursor(resolveDocumentProjection(projection)).apply { includeFile(this, documentId, null) } }
Java
@Override public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { // Create a cursor with the requested projection, or the default projection. final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); includeFile(result, documentId, null); return result; }
Поставщик документов также может предоставлять уменьшенные изображения документов, переопределив метод DocumentsProvider.openDocumentThumbnail() и добавив флаг FLAG_SUPPORTS_THUMBNAIL к поддерживаемым файлам.
В приведенном ниже фрагменте кода показано, как реализовать DocumentsProvider.openDocumentThumbnail().
Kotlin
override fun openDocumentThumbnail( documentId: String?, sizeHint: Point?, signal: CancellationSignal? ): AssetFileDescriptor { val file = getThumbnailFileForDocId(documentId) val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) return AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH) }
Java
@Override public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { final File file = getThumbnailFileForDocId(documentId); final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH); }
Внимание! Поставщик документов не должен возвращать значки, размер которых более чем в два раза превышает значение, указанное в параметре sizeHint.
Как открыть документ
Вы должны реализовать openDocument(), чтобы возвращать ParcelFileDescriptor, представляющий указанный файл. Другие приложения могут использовать возвращенный ParcelFileDescriptor для потоковой передачи данных. Система вызывает этот метод после того, как пользователь выбирает файл, а клиентское приложение запрашивает доступ к нему, вызывая openFileDescriptor().
Пример:
Kotlin
override fun openDocument( documentId: String, mode: String, signal: CancellationSignal ): ParcelFileDescriptor { Log.v(TAG, "openDocument, mode: $mode") // It's OK to do network operations in this method to download the document, // as long as you periodically check the CancellationSignal. If you have an // extremely large file to transfer from the network, a better solution may // be pipes or sockets (see ParcelFileDescriptor for helper methods). val file: File = getFileForDocId(documentId) val accessMode: Int = ParcelFileDescriptor.parseMode(mode) val isWrite: Boolean = mode.contains("w") return if (isWrite) { val handler = Handler(context.mainLooper) // Attach a close listener if the document is opened in write mode. try { ParcelFileDescriptor.open(file, accessMode, handler) { // Update the file with the cloud server. The client is done writing. Log.i(TAG, "A file with id $documentId has been closed! Time to update the server.") } } catch (e: IOException) { throw FileNotFoundException( "Failed to open document with id $documentId and mode $mode" ) } } else { ParcelFileDescriptor.open(file, accessMode) } }
Java
@Override public ParcelFileDescriptor openDocument(final String documentId, final String mode, CancellationSignal signal) throws FileNotFoundException { Log.v(TAG, "openDocument, mode: " + mode); // It's OK to do network operations in this method to download the document, // as long as you periodically check the CancellationSignal. If you have an // extremely large file to transfer from the network, a better solution may // be pipes or sockets (see ParcelFileDescriptor for helper methods). final File file = getFileForDocId(documentId); final int accessMode = ParcelFileDescriptor.parseMode(mode); final boolean isWrite = (mode.indexOf('w') != -1); if(isWrite) { // Attach a close listener if the document is opened in write mode. try { Handler handler = new Handler(getContext().getMainLooper()); return ParcelFileDescriptor.open(file, accessMode, handler, new ParcelFileDescriptor.OnCloseListener() { @Override public void onClose(IOException e) { // Update the file with the cloud server. The client is done // writing. Log.i(TAG, "A file with id " + documentId + " has been closed! Time to " + "update the server."); } }); } catch (IOException e) { throw new FileNotFoundException("Failed to open document with id" + documentId + " and mode " + mode); } } else { return ParcelFileDescriptor.open(file, accessMode); } }
Если поставщик документов передает файлы в потоковом режиме или обрабатывает сложные структуры данных, рекомендуем реализовать методы createReliablePipe() или createReliableSocketPair().
Эти методы позволяют создать пару объектов ParcelFileDescriptor, один из которых можно вернуть, а другой – отправить через ParcelFileDescriptor.AutoCloseOutputStream или ParcelFileDescriptor.AutoCloseInputStream.
Поддержка недавних документов и поиска
Вы можете предоставить список недавно измененных документов в корневом каталоге поставщика документов, переопределив метод queryRecentDocuments() и возвратив FLAG_SUPPORTS_RECENTS. В следующем фрагменте кода показан пример реализации методов queryRecentDocuments().
Kotlin
override fun queryRecentDocuments(rootId: String?, projection: Array<out String>?): Cursor { // This example implementation walks a // local file structure to find the most recently // modified files. Other implementations might // include making a network call to query a // server. // Create a cursor with the requested projection, or the default projection. val result = MatrixCursor(resolveDocumentProjection(projection)) val parent: File = getFileForDocId(rootId) // Create a queue to store the most recent documents, // which orders by last modified. val lastModifiedFiles = PriorityQueue( 5, Comparator<File> { i, j -> Long.compare(i.lastModified(), j.lastModified()) } ) // Iterate through all files and directories // in the file structure under the root. If // the file is more recent than the least // recently modified, add it to the queue, // limiting the number of results. val pending : MutableList<File> = mutableListOf() // Start by adding the parent to the list of files to be processed pending.add(parent) // Do while we still have unexamined files while (pending.isNotEmpty()) { // Take a file from the list of unprocessed files val file: File = pending.removeAt(0) if (file.isDirectory) { // If it's a directory, add all its children to the unprocessed list pending += file.listFiles() } else { // If it's a file, add it to the ordered queue. lastModifiedFiles.add(file) } } // Add the most recent files to the cursor, // not exceeding the max number of results. for (i in 0 until Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size)) { val file: File = lastModifiedFiles.remove() includeFile(result, null, file) } return result }
Java
@Override public Cursor queryRecentDocuments(String rootId, String[] projection) throws FileNotFoundException { // This example implementation walks a // local file structure to find the most recently // modified files. Other implementations might // include making a network call to query a // server. // Create a cursor with the requested projection, or the default projection. final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); final File parent = getFileForDocId(rootId); // Create a queue to store the most recent documents, // which orders by last modified. PriorityQueue lastModifiedFiles = new PriorityQueue(5, new Comparator() { public int compare(File i, File j) { return Long.compare(i.lastModified(), j.lastModified()); } }); // Iterate through all files and directories // in the file structure under the root. If // the file is more recent than the least // recently modified, add it to the queue, // limiting the number of results. final LinkedList pending = new LinkedList(); // Start by adding the parent to the list of files to be processed pending.add(parent); // Do while we still have unexamined files while (!pending.isEmpty()) { // Take a file from the list of unprocessed files final File file = pending.removeFirst(); if (file.isDirectory()) { // If it's a directory, add all its children to the unprocessed list Collections.addAll(pending, file.listFiles()); } else { // If it's a file, add it to the ordered queue. lastModifiedFiles.add(file); } } // Add the most recent files to the cursor, // not exceeding the max number of results. for (int i = 0; i < Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size()); i++) { final File file = lastModifiedFiles.remove(); includeFile(result, null, file); } return result; }
Полный код для приведенного выше фрагмента можно получить, скачав пример кода StorageProvider.
Создание документов поддержки
Вы можете разрешить клиентским приложениям создавать файлы в поставщике документов.
Если клиентское приложение отправляет намерение ACTION_CREATE_DOCUMENT, поставщик документов может разрешить ему создавать новые документы в своем хранилище.
Чтобы поддерживать создание документов, у корневого каталога должен быть флаг FLAG_SUPPORTS_CREATE.
Каталоги, в которых разрешено создавать новые файлы, должны иметь флаг FLAG_DIR_SUPPORTS_CREATE.
Поставщик документов также должен реализовать метод createDocument(). Когда пользователь выбирает каталог в поставщике документов для сохранения нового файла, поставщик документов получает вызов createDocument(). Внутри реализации метода createDocument() вы возвращаете новый COLUMN_DOCUMENT_ID для файла. Затем клиентское приложение может использовать этот идентификатор, чтобы получить дескриптор файла и вызвать метод openDocument() для записи в новый файл.
В следующем фрагменте кода показано, как создать файл в поставщике документов.
Kotlin
override fun createDocument(documentId: String?, mimeType: String?, displayName: String?): String { val parent: File = getFileForDocId(documentId) val file: File = try { File(parent.path, displayName).apply { createNewFile() setWritable(true) setReadable(true) } } catch (e: IOException) { throw FileNotFoundException( "Failed to create document with name $displayName and documentId $documentId" ) } return getDocIdForFile(file) }
Java
@Override public String createDocument(String documentId, String mimeType, String displayName) throws FileNotFoundException { File parent = getFileForDocId(documentId); File file = new File(parent.getPath(), displayName); try { file.createNewFile(); file.setWritable(true); file.setReadable(true); } catch (IOException e) { throw new FileNotFoundException("Failed to create document with name " + displayName +" and documentId " + documentId); } return getDocIdForFile(file); }
Полный код для приведенного выше фрагмента можно получить, скачав пример кода StorageProvider.
Поддержка функций управления документами
Помимо открытия, создания и просмотра файлов, поставщик документов также может разрешить клиентским приложениям переименовывать, копировать, перемещать и удалять файлы. Чтобы добавить в поставщик документов функции управления документами, добавьте флаг в столбец COLUMN_FLAGS документа, чтобы указать поддерживаемую функцию. Вам также нужно реализовать соответствующий метод класса DocumentsProvider.
В таблице ниже указаны флаг COLUMN_FLAGS и метод DocumentsProvider, которые поставщик документов должен реализовать, чтобы предоставить доступ к определенным функциям.
| Функция | Флаг | Метод |
|---|---|---|
| Как удалить файл |
FLAG_SUPPORTS_DELETE
|
deleteDocument()
|
| Переименовать файл |
FLAG_SUPPORTS_RENAME
|
renameDocument()
|
| Копирование файла в новый родительский каталог в поставщике документов |
FLAG_SUPPORTS_COPY
|
copyDocument()
|
| Переместить файл из одного каталога в другой в поставщике документов. |
FLAG_SUPPORTS_MOVE
|
moveDocument()
|
| Как удалить файл из родительского каталога |
FLAG_SUPPORTS_REMOVE
|
removeDocument()
|
Поддержка виртуальных файлов и альтернативных форматов файлов
Виртуальные файлы – функция, представленная в Android 7.0 (уровень API 24). Она позволяет поставщикам документов предоставлять доступ к просмотру файлов, у которых нет прямого представления в виде байт-кода. Чтобы другие приложения могли просматривать виртуальные файлы, поставщик документов должен создать альтернативное представление файла, которое можно открыть.
Например, поставщик документов может содержать файл, формат которого другие приложения не могут открыть напрямую, по сути, виртуальный файл.
Если клиентское приложение отправляет намерение ACTION_VIEW без категории CATEGORY_OPENABLE, пользователи могут выбирать виртуальные файлы в поставщике документов для просмотра. Затем поставщик документов возвращает виртуальный файл в другом формате, который можно открыть, например в виде изображения.
После этого клиентское приложение может открыть виртуальный файл для просмотра.
Чтобы объявить, что документ в поставщике является виртуальным, необходимо добавить флаг FLAG_VIRTUAL_DOCUMENT в файл, возвращаемый методом queryDocument(). Этот флаг сообщает клиентским приложениям, что у файла нет прямого представления в виде байт-кода и его нельзя открыть напрямую.
Если вы объявляете, что файл в вашем поставщике документов является виртуальным, настоятельно рекомендуется сделать его доступным в другом MIME-типе, например в виде изображения или PDF-файла. Поставщик документов объявляет альтернативные MIME-типы, которые он поддерживает для просмотра виртуального файла, переопределяя метод getDocumentStreamTypes(). Когда клиентские приложения вызывают метод getStreamTypes(android.net.Uri, java.lang.String), система вызывает метод getDocumentStreamTypes() поставщика документов. Затем метод getDocumentStreamTypes() возвращает массив альтернативных MIME-типов, которые поставщик документов поддерживает для файла.
После того как клиент определит, что поставщик документов может создать документ в формате файла, доступном для просмотра, клиентское приложение вызывает метод openTypedAssetFileDescriptor(), который внутренне вызывает метод openTypedDocument() поставщика документов. Поставщик документов возвращает файл клиентскому приложению в запрошенном формате.
В приведенном ниже фрагменте кода показана простая реализация методов getDocumentStreamTypes() и openTypedDocument().
Kotlin
var SUPPORTED_MIME_TYPES : Array<String> = arrayOf("image/png", "image/jpg") override fun openTypedDocument( documentId: String?, mimeTypeFilter: String, opts: Bundle?, signal: CancellationSignal? ): AssetFileDescriptor? { return try { // Determine which supported MIME type the client app requested. when(mimeTypeFilter) { "image/jpg" -> openJpgDocument(documentId) "image/png", "image/*", "*/*" -> openPngDocument(documentId) else -> throw IllegalArgumentException("Invalid mimeTypeFilter $mimeTypeFilter") } } catch (ex: Exception) { Log.e(TAG, ex.message) null } } override fun getDocumentStreamTypes(documentId: String, mimeTypeFilter: String): Array<String> { return when (mimeTypeFilter) { "*/*", "image/*" -> { // Return all supported MIME types if the client app // passes in '*/*' or 'image/*'. SUPPORTED_MIME_TYPES } else -> { // Filter the list of supported mime types to find a match. SUPPORTED_MIME_TYPES.filter { it == mimeTypeFilter }.toTypedArray() } } }
Java
public static String[] SUPPORTED_MIME_TYPES = {"image/png", "image/jpg"}; @Override public AssetFileDescriptor openTypedDocument(String documentId, String mimeTypeFilter, Bundle opts, CancellationSignal signal) { try { // Determine which supported MIME type the client app requested. if ("image/png".equals(mimeTypeFilter) || "image/*".equals(mimeTypeFilter) || "*/*".equals(mimeTypeFilter)) { // Return the file in the specified format. return openPngDocument(documentId); } else if ("image/jpg".equals(mimeTypeFilter)) { return openJpgDocument(documentId); } else { throw new IllegalArgumentException("Invalid mimeTypeFilter " + mimeTypeFilter); } } catch (Exception ex) { Log.e(TAG, ex.getMessage()); } finally { return null; } } @Override public String[] getDocumentStreamTypes(String documentId, String mimeTypeFilter) { // Return all supported MIME tyupes if the client app // passes in '*/*' or 'image/*'. if ("*/*".equals(mimeTypeFilter) || "image/*".equals(mimeTypeFilter)) { return SUPPORTED_MIME_TYPES; } ArrayList requestedMimeTypes = new ArrayList<>(); // Iterate over the list of supported mime types to find a match. for (int i=0; i < SUPPORTED_MIME_TYPES.length; i++) { if (SUPPORTED_MIME_TYPES[i].equals(mimeTypeFilter)) { requestedMimeTypes.add(SUPPORTED_MIME_TYPES[i]); } } return (String[])requestedMimeTypes.toArray(); }
Безопасность
Предположим, что ваш поставщик документов – это защищенное паролем облачное хранилище, и вы хотите убедиться, что пользователи вошли в систему, прежде чем разрешить им делиться файлами.
Что должно делать приложение, если пользователь не вошел в аккаунт? Решение состоит в том, чтобы возвращать
ноль корней в вашей реализации queryRoots(). То есть пустой корневой курсор:
Kotlin
override fun queryRoots(projection: Array<out String>): Cursor { ... // If user is not logged in, return an empty root cursor. This removes our // provider from the list entirely. if (!isUserLoggedIn()) { return result }
Java
public Cursor queryRoots(String[] projection) throws FileNotFoundException { ... // If user is not logged in, return an empty root cursor. This removes our // provider from the list entirely. if (!isUserLoggedIn()) { return result; }
Второй шаг – позвонить в getContentResolver().notifyChange().
Помните DocumentsContract? Мы используем его для создания
этого URI. Следующий фрагмент кода указывает системе запрашивать корневые каталоги поставщика документов при каждом изменении статуса входа пользователя. Если пользователь не вошел в систему, вызов queryRoots() возвращает пустой курсор, как показано выше. Это гарантирует, что документы поставщика будут доступны только в том случае, если пользователь вошел в систему поставщика.
Kotlin
private fun onLoginButtonClick() { loginOrLogout() getContentResolver().notifyChange( DocumentsContract.buildRootsUri(AUTHORITY), null ) }
Java
private void onLoginButtonClick() { loginOrLogout(); getContentResolver().notifyChange(DocumentsContract .buildRootsUri(AUTHORITY), null); }
Образцы кода, связанные с этой страницей, можно найти в следующих источниках:
Видео по теме:
- DevBytes: Android 4.4 Storage Access Framework: Provider
- Storage Access Framework: создание DocumentsProvider
- Виртуальные файлы в Storage Access Framework
Дополнительную информацию можно найти в следующих статьях:
- Создание DocumentsProvider
- Как открывать файлы с помощью Storage Access Framework
- Основы работы с поставщиками контента