创建自定义文档提供程序

如果您正在开发一个为文件提供存储服务的应用(例如云保存服务),您可以通过编写自定义文档提供程序,通过存储访问框架 (SAF) 提供文件。此页面介绍如何创建自定义文档提供程序。

有关存储访问框架工作原理的更多信息,请参阅存储访问框架概述

清单

要实现自定义文档提供程序,请将以下内容添加到您的应用的清单中

  • 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 及更低版本的设备,则应在清单中为运行 Android 4.4 或更高版本的设备禁用 ACTION_GET_CONTENT 意图过滤器。文档提供程序和 ACTION_GET_CONTENT 应视为互斥的。如果您同时支持两者,则您的应用在系统选择器 UI 中出现两次,提供了两种不同的访问存储数据的方式。这对用户来说令人困惑。

以下是为运行 Android 4.4 或更高版本的设备禁用 ACTION_GET_CONTENT 意图过滤器的推荐方法

  1. res/values/ 下的 bool.xml 资源文件中,添加以下行
    <bool name="atMostJellyBeanMR2">true</bool>
  2. res/values-v19/ 下的 bool.xml 资源文件中,添加以下行
    <bool name="atMostJellyBeanMR2">false</bool>
  3. 添加一个 活动别名 来禁用 4.4(API 级别 19)及更高版本中的 ACTION_GET_CONTENT 意图过滤器。例如
    <!-- 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,};

根目录的游标需要包含某些必需的列。这些列是

文档的游标需要包含以下必需列

创建 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() 方法更新文档 UI 以与这些更改保持同步,如下面的代码段所示。

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 中定义的列。

当用户在选择器 UI 中选择您的根目录时,会调用此方法。该方法检索由 COLUMN_DOCUMENT_ID 指定的文档 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.AutoCloseOutputStreamParcelFileDescriptor.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。然后,客户端应用可以使用该 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)中引入的功能 虚拟文件 允许文档提供程序提供对没有直接字节码表示的文件的查看访问权限。要使其他应用能够查看虚拟文件,您的文档提供程序需要为虚拟文件生成可替代的可打开的文件表示形式。

例如,假设文档提供程序包含其他应用无法直接打开的文件格式,本质上是虚拟文件。当客户端应用发送不带 CATEGORY_OPENABLE 类别的 ACTION_VIEW 意图时,用户可以在文档提供程序内选择这些虚拟文件进行查看。然后,文档提供程序以不同的但可打开的文件格式(例如图像)返回虚拟文件。然后,客户端应用可以打开虚拟文件供用户查看。

要声明提供程序中的文档是虚拟的,您需要向 queryDocument() 方法返回的文件添加 FLAG_VIRTUAL_DOCUMENT 标志。此标志会提醒客户端应用该文件没有直接的字节码表示形式,无法直接打开。

如果在文档提供程序中声明文件为虚拟文件,强烈建议您以其他 MIME 类型(例如图像或 PDF)提供该文件。文档提供程序通过覆盖 getDocumentStreamTypes() 方法来声明其支持的用于查看虚拟文件的备用 MIME 类型。当客户端应用调用 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&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() 的实现中返回零个根。也就是说,一个空的根游标。

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

有关此页面的示例代码,请参阅

有关此页面的视频,请参阅

有关其他相关信息,请参阅