建立自訂文件供應程式

如果您正在開發應用程式 (例如雲端儲存服務) 提供檔案儲存空間服務,您可以編寫自訂文件供應器,透過儲存空間存取架構 (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. bool.xml 資源檔案的 res/values/ 底下,新增此行:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. bool.xml 資源檔案的 res/values-v19/ 底下,新增此行:
    <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() 時,需要使用 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() 方法更新文件 UI,讓文件 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() 時,必須使用 DocumentsContract.Document 中定義的資料欄,傳回指向指定目錄內所有檔案的 Cursor

當使用者在挑選器 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() 時,必須使用 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 意圖,文件供應器就能允許用戶端應用程式在文件供應器內建立新文件。

您的根必須含有 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 意圖時,使用者可以在文件供應器中選取這些虛擬檔案來查看檔案。接著,文件供應器會以其他可開啟的檔案格式 (例如圖片) 傳回虛擬檔案。接著,用戶端應用程式就能開啟虛擬檔案供使用者查看。

如要宣告供應器中的文件為虛擬文件,您需要將 FLAG_VIRTUAL_DOCUMENT 標記新增至 queryDocument() 方法傳回的檔案。這個旗標會通知用戶端應用程式,檔案未採用直接位元碼表示法且無法直接開啟。

如果您在文件供應器中宣告檔案屬於虛擬檔案,強烈建議您以圖片或 PDF 等其他 MIME 類型提供該檔案。文件供應器會覆寫 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);
}

如需與本頁相關的程式碼範例,請參閱:

如要查看與本頁相關的影片,請參閱:

如需其他相關資訊,請參閱: