创建自定义文档提供程序

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

如需详细了解存储访问框架的工作原理,请参阅存储访问框架概览

清单

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

  • 以 API 级别 19 或更高版本为目标平台。
  • <provider> 元素,用于声明您的自定义存储空间提供程序。
  • 设置为 DocumentsProvider 子类名称的属性 android:name(即其类名称,包括软件包名称):

    com.example.android.storageprovider.MyCloudProvider

  • 属性 android:authority 属性,即您的软件包名称(在本例中为 com.example.android.storageprovider)加上 content provider 的类型 (documents)。
  • 设置为 "true" 的属性 android:exported。 您必须导出提供程序,以便其他应用可以看到。
  • 设置为 "true" 的属性 android:grantUriPermissions。此设置允许系统授予其他应用对提供程序中内容的访问权限。如需查看有关这些其他应用如何持久访问来自提供程序的内容的讨论,请参阅保留权限
  • MANAGE_DOCUMENTS 权限。默认情况下,提供方可供所有人使用。添加此权限会将您的提供程序限制为系统。此限制对于确保安全性至关重要。
  • 一个包含 android.content.action.DOCUMENTS_PROVIDER 操作的 intent 过滤器,以便您的提供程序在系统搜索提供程序时出现在选择器中。

以下代码摘录自包含提供程序的示例清单:

<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 intent 仅适用于搭载 Android 4.4 及更高版本的设备。如果您希望应用支持 ACTION_GET_CONTENT 以适应搭载 Android 4.3 及更低版本的设备,则应针对搭载 Android 4.4 或更高版本的设备在清单中停用 ACTION_GET_CONTENT intent 过滤器。文档提供程序和 ACTION_GET_CONTENT 应视为互斥。如果您同时支持这两者,您的应用会在系统选择器界面中出现两次,提供两种不同的存储数据访问方式。这会对用户造成困扰。

若要在搭载 Android 4.4 或更高版本的设备上停用 ACTION_GET_CONTENT intent 过滤器,建议通过以下方法:

  1. res/values/ 下的 bool.xml 资源文件中,添加以下行:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. res/values-v19/ 下的 bool.xml 资源文件中,添加以下行:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. 添加 activity 别名,以针对版本 4.4(API 级别 19)及更高版本停用 ACTION_GET_CONTENT intent 过滤器。例如:
    <!-- 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>
    

合同

编写自定义 content provider 时,其中一个任务是实现协定类,如 内容提供程序开发者指南中所述。协定类是一种 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() 实现需要使用 DocumentsContract.Root 中定义的列返回一个指向文档提供程序的所有根目录的 Cursor

在以下代码段中,projection 参数表示调用方想要返回的特定字段。该代码段会创建一个新光标并向其中添加一行:一个根目录,一个顶级目录,如“Downloads”或“Images”。大部分提供程序只有一个根目录。您可能有多个网域,例如,当您有多个用户帐号时。在这种情况下,只需再向光标添加一行即可。

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() 实现必须使用 DocumentsContract.Document 中定义的列返回一个指向指定目录中所有文件的 Cursor

当用户在选择器界面中选择根目录时,系统会调用此方法。该方法检索由 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() 实现必须使用 DocumentsContract.Document 中定义的列返回一个指向指定文件的 Cursor

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 intent,您的文档提供程序可允许该客户端应用在文档提供程序内创建新文档。

如需支持创建文档,您的根目录需要具有 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)中引入的一项功能,可让文档提供程序提供对无直接字节码表示形式的文件的查看权限。为使其他应用能够查看虚拟文件,您的文档提供程序需要为虚拟文件生成替代的可打开文件表示形式。

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

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

如果您在文档提供程序中声明某个文件是虚拟文件,强烈建议您使用其他 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);
}

如需查看此页涉及的示例代码,请参阅:

如需观看此页涉及的视频,请参阅:

如需了解其他相关信息,请参阅: