创建自定义文档提供程序

如果您要开发一款提供文件存储服务(例如 例如云端存档服务),那么您可以通过 存储访问框架 (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 intent 。 如果您希望应用支持 ACTION_GET_CONTENT ,以适应运行 Android 4.3 及更低版本的设备,您应该 在以下位置停用 ACTION_GET_CONTENT intent 过滤器: 您的清单。答 文档提供商和ACTION_GET_CONTENT 相互排斥。如果您同时支持这两者,您的应用 在系统选择器界面中出现了两次,提供两种不同的访问方式, 存储的数据这会对用户造成困扰。

您可以通过以下方法停用 设备的 ACTION_GET_CONTENT intent 过滤器 运行 Android 4.4 或更高版本:

  1. res/values/ 下的 bool.xml 资源文件中,添加 此行:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. res/values-v19/ 下的 bool.xml 资源文件中,添加 此行:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. 活动 别名来停用 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 时,需要完成的一项任务是 实现协定类,如 <ph type="x-smartling-placeholder"></ph> 内容提供方开发者指南。协定类是 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() 方法,如以下代码段所示。

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

当用户在选择器界面中选择您的根目录时,系统会调用此方法。 该方法会检索由指定文档 ID 的 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.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() 方法。当客户端应用调用 getStreamTypes(android.net.Uri, java.lang.String) 方法,系统会调用 getDocumentStreamTypes() 方法。通过 getDocumentStreamTypes() 方法随后会返回 文档提供程序支持相应文件。

客户确定 使文档提供程序能够以可查看的文件格式生成文档 格式时,客户端应用调用 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);
}

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

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

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