Создайте собственный поставщик документов

Если вы разрабатываете приложение, предоставляющее услуги хранения файлов (например, службу сохранения в облаке), вы можете сделать свои файлы доступными через Storage Access Framework (SAF), написав собственный поставщик документов. На этой странице описано, как создать собственный поставщик документов.

Дополнительные сведения о том, как работает 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 или выше:

  1. В файле ресурсов bool.xml в разделе res/values/ добавьте следующую строку:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. В файле ресурсов bool.xml в разделе res/values-v19/ добавьте следующую строку:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. Добавьте псевдоним действия , чтобы отключить фильтр намерений 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 предоставляет вам эти классы контрактов, поэтому вам не нужно писать свои собственные:

Например, вот столбцы, которые вы можете вернуть в курсоре, когда у вашего поставщика документов запрашиваются документы или корень:

Котлин

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
)

Ява

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,};

Ваш курсор для корня должен включать определенные обязательные столбцы. Эти столбцы:

Курсор для документов должен включать следующие обязательные столбцы:

Создайте подкласс DocumentsProvider.

Следующим шагом в написании собственного поставщика документов является создание подкласса абстрактного класса DocumentsProvider . Как минимум, вы должны реализовать следующие методы:

Это единственные методы, которые вам строго необходимо реализовать, но вы можете захотеть использовать гораздо больше. Подробности см. DocumentsProvider .

Определить корень

Ваша реализация queryRoots() должна возвращать Cursor указывающий на все корневые каталоги вашего поставщика документов, используя столбцы, определенные в DocumentsContract.Root .

В следующем фрагменте кода параметр projection представляет конкретные поля, которые вызывающая сторона хочет получить обратно. Фрагмент создает новый курсор и добавляет к нему одну строку — один корень, каталог верхнего уровня, например «Загрузки» или «Изображения». Большинство провайдеров имеют только один корень. У вас может быть несколько учетных записей, например, в случае нескольких учетных записей пользователей. В этом случае просто добавьте к курсору вторую строку.

Котлин

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
}

Ява

@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() , как показано в следующем фрагменте кода.

Котлин

val rootsUri: Uri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY)
context.contentResolver.notifyChange(rootsUri, null)

Ява

Uri rootsUri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY);
context.getContentResolver().notifyChange(rootsUri, null);

Список документов в провайдере

Ваша реализация queryChildDocuments() должна возвращать Cursor , указывающий на все файлы в указанном каталоге, используя столбцы, определенные в DocumentsContract.Document .

Этот метод вызывается, когда пользователь выбирает ваш корень в пользовательском интерфейсе средства выбора. Метод извлекает дочерние элементы документа с идентификатором, указанным COLUMN_DOCUMENT_ID . Затем система вызывает этот метод каждый раз, когда пользователь выбирает подкаталог в вашем поставщике документов.

Этот фрагмент создает новый курсор с запрошенными столбцами, а затем добавляет к курсору информацию о каждом непосредственном дочернем элементе в родительском каталоге. Дочерним элементом может быть изображение, другой каталог — любой файл:

Котлин

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)
                }
    }
}

Ява

@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() , но для конкретного файла:

Котлин

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)
    }
}

Ява

@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() .

Котлин

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)
}

Ява

@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() . Например:

Котлин

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)
    }
}

Ява

@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() .

Котлин

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
}

Ява

@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() для записи в новый файл.

В следующем фрагменте кода показано, как создать новый файл в поставщике документов.

Котлин

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)
}

Ява

@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() .

Котлин

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()
        }
    }
}

Ява

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&lt;&gt;();

    // Iterate over the list of supported mime types to find a match.
    for (int i=0; i &lt; SUPPORTED_MIME_TYPES.length; i++) {
        if (SUPPORTED_MIME_TYPES[i].equals(mimeTypeFilter)) {
            requestedMimeTypes.add(SUPPORTED_MIME_TYPES[i]);
        }
    }
    return (String[])requestedMimeTypes.toArray();
}

Безопасность

Предположим, что ваш поставщик документов — это облачное хранилище, защищенное паролем, и вы хотите убедиться, что пользователи вошли в систему, прежде чем вы начнете делиться их файлами. Что должно делать ваше приложение, если пользователь не вошел в систему? Решение состоит в том, чтобы вернуть нулевые корни в вашей реализации queryRoots() . То есть пустой корневой курсор:

Котлин

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
    }

Ява

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() возвращает пустой курсор, как показано выше. Это гарантирует, что документы провайдера будут доступны только в том случае, если пользователь вошел в систему провайдера.

Котлин

private fun onLoginButtonClick() {
    loginOrLogout()
    getContentResolver().notifyChange(
        DocumentsContract.buildRootsUri(AUTHORITY),
        null
    )
}

Ява

private void onLoginButtonClick() {
    loginOrLogout();
    getContentResolver().notifyChange(DocumentsContract
            .buildRootsUri(AUTHORITY), null);
}

Пример кода, связанного с этой страницей, см.:

Видео, связанные с этой страницей, см.:

Для получения дополнительной информации см.: