Android NDK开发详解Content Resolver之创建自定义文档提供程序


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

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

清单

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

以 API 级别 19 或更高版本为目标平台。
声明自定义存储提供程序的 元素。
设置为 DocumentsProvider 子类名称的属性 android:name,即其类的名称(包括软件包名称):
com.example.android.storageprovider.MyCloudProvider。

属性 android:authority,即软件包名称(在此示例中是 com.example.android.storageprovider),以及内容提供程序的类型 (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 过滤器:

在 bool.xml 资源文件的 res/values/ 下,添加以下行:


<bool name="atMostJellyBeanMR2">true</bool>

在 bool.xml 资源文件的 res/values-v19/ 下,添加以下行:

<bool name="atMostJellyBeanMR2">false</bool>

添加 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>

合同

正如内容提供程序开发者指南中所述,通常在您编写自定义内容提供程序时,其中一项任务是实现合同类。合同类是 public final 类,其中包含 URI、列名称、MIME 类型以及适用于提供程序的其他元数据的常量定义。SAF 会为您提供这些合同类,因此您无需自行编写:

DocumentsContract.Document
DocumentsContract.Root
例如,当用户通过文档提供程序查询文档或根目录时,以下是您可能在光标中返回的列:

Kotlin

    private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
            DocumentsContract.Root.COLUMN_ROOT_ID,
            DocumentsContract.Root.COLUMN_MIME_TYPES,
            DocumentsContract.Root.COLUMN_FLAGS,
            DocumentsContract.Root.COLUMN_ICON,
            DocumentsContract.Root.COLUMN_TITLE,
            DocumentsContract.Root.COLUMN_SUMMARY,
            DocumentsContract.Root.COLUMN_DOCUMENT_ID,
            DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
    )
    private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf(
            DocumentsContract.Document.COLUMN_DOCUMENT_ID,
            DocumentsContract.Document.COLUMN_MIME_TYPE,
            DocumentsContract.Document.COLUMN_DISPLAY_NAME,
            DocumentsContract.Document.COLUMN_LAST_MODIFIED,
            DocumentsContract.Document.COLUMN_FLAGS,
            DocumentsContract.Document.COLUMN_SIZE
    )

Java

    private static final String[] DEFAULT_ROOT_PROJECTION =
            new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES,
            Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
            Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
            Root.COLUMN_AVAILABLE_BYTES,};
    private static final String[] DEFAULT_DOCUMENT_PROJECTION = new
            String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
            Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
            Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};

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

COLUMN_ROOT_ID
COLUMN_ICON
COLUMN_TITLE
COLUMN_FLAGS
COLUMN_DOCUMENT_ID
文档的光标需要包含以下必需列:

COLUMN_DOCUMENT_ID
COLUMN_DISPLAY_NAME
COLUMN_MIME_TYPE
COLUMN_FLAGS
COLUMN_SIZE
COLUMN_LAST_MODIFIED

创建 DocumentsProvider 的子类

编写自定义文档提供程序的下一步是子类化抽象类 DocumentsProvider。您必须至少实现以下方法:

queryRoots()
queryChildDocuments()
queryDocument()
openDocument()
上述是您必须严格实现的全部方法,还有很多您可能想要实现的方法。如需了解详情,请参阅 DocumentsProvider。

定义根目录

在实现 queryRoots() 时,您需要使用 DocumentsContract.Root 中定义的列,返回指向文档提供程序的所有根目录的 Cursor。

在以下代码段中,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() 时,您必须使用 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.AutoCloseOutputStream 或 ParcelFileDescriptor.AutoCloseInputStream 返回一个对象并发送另一个对象。

支持最新文档和搜索

您可以通过替换 queryRecentDocuments() 方法并返回 FLAG_SUPPORTS_RECENTS,在文档提供程序的根目录下提供最近修改过的文档列表。以下代码段显示了如何实现 方法的示例。

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 方法。

在这里插入图片描述

支持虚拟文件和备用文件格式

虚拟文件是 Android 7.0(API 级别 24)中引入的一项功能,可让文档提供程序提供没有直接字节码表示形式的文件的查看权限。要让其他应用能够查看虚拟文件,文档提供程序需要为虚拟文件生成可打开的替代文件。

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

要声明提供程序中的文档是虚拟的,您需要将 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);
    }

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

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

DevBytes: Android 4.4 Storage Access Framework: Provider(DevBytes: Android 4.4 存储访问框架:提供程序)
Storage Access Framework: Building a DocumentsProvider(存储访问框架:构建 DocumentsProvider)
Virtual Files in the Storage Access Framework(存储访问框架中的虚拟文件)
如需了解其他相关信息,请参阅:

Building a DocumentsProvider(创建 DocumentsProvider)
使用存储访问框架打开文件
内容提供程序基础知识

本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。

最后更新时间 (UTC):2019-12-27。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

五一编程

程序之路有我与你同行

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值