建立自訂文件供應程式

如果您開發的應用程式為檔案提供儲存服務 (例如 雲端儲存服務),可以透過 編寫自訂文件供應程式,以儲存儲存空間存取架構 (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」意圖僅適用於 。 如果您希望應用程式支援 ACTION_GET_CONTENT 搭載 Android 4.3 以下版本的裝置, 停用 ACTION_GET_CONTENT 意圖篩選器 資訊清單。A 罩杯 應考慮使用文件提供者和 ACTION_GET_CONTENT 彼此互斥如果應用程式同時支援 會在系統選擇器 UI 中顯示兩次,提供兩種存取方法 儲存的資料這樣會讓使用者感到困惑。

停用 裝置的 ACTION_GET_CONTENT 個意圖篩選器 搭載 Android 4.4 以上版本:

  1. bool.xml 資源檔案 res/values/ 底下,新增 這一行:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. bool.xml 資源檔案 res/values-v19/ 底下,新增 這一行:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. 新增 活動 別名來停用 ACTION_GET_CONTENT 意圖 。例如:
    <!-- This activity alias is added so that GET_CONTENT intent-filter
         can be disabled for builds on API level 19 and higher. -->
    <activity-alias android:name="com.android.example.app.MyPicker"
            android:targetActivity="com.android.example.app.MyActivity"
            ...
            android:enabled="@bool/atMostJellyBeanMR2">
        <intent-filter>
            <action android:name="android.intent.action.GET_CONTENT" />
            <category android:name="android.intent.category.OPENABLE" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:mimeType="image/*" />
            <data android:mimeType="video/*" />
        </intent-filter>
    </activity-alias>
    

合約

一般而言,當您編寫自訂內容供應器時,其中一項工作是 實作合約類別,詳情請參閱 內容供應器開發人員指南。合約類別是 public final 類別 ,其中包含 URI、資料欄名稱、MIME 類型及 與供應商相關的其他中繼資料SAF 提供這些合約類別 擁有:

舉例來說,以下是您在遊標變更時 查詢文件提供者的文件或根目錄:

Kotlin

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

Java

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

根的遊標需要包含某些必要欄。 這些資料欄包括:

文件的遊標必須包含下列必要欄:

建立 DocumentsProvider 的子類別

編寫自訂文件供應器的下一步,是將 抽象類別 DocumentsProvider。您必須至少 實作下列方法:

實作時,您只需要採用上述方法 你可能會想得好很多詳情請參閱 DocumentsProvider

定義根目錄

queryRoots() 實作需要傳回 Cursor,指向所有 寫入文件提供者的根目錄 DocumentsContract.Root

在以下程式碼片段中,projection 參數代表 呼叫者要返回的特定欄位。程式碼片段會建立新的遊標 並在其中新增一列:根目錄、頂層目錄 下載或圖片。大多數提供者都只有一個根。可能會有不只一個 例如有多個使用者帳戶時。在此情況下,只需新增 。

Kotlin

override fun queryRoots(projection: Array<out String>?): Cursor {
    // Use a MatrixCursor to build a cursor
    // with either the requested fields, or the default
    // projection if "projection" is null.
    val result = MatrixCursor(resolveRootProjection(projection))

    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result
    }

    // It's possible to have multiple roots (e.g. for multiple accounts in the
    // same app) -- just add multiple cursor rows.
    result.newRow().apply {
        add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT)

        // You can provide an optional summary, which helps distinguish roots
        // with the same title. You can also use this field for displaying an
        // user account name.
        add(DocumentsContract.Root.COLUMN_SUMMARY, context.getString(R.string.root_summary))

        // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
        // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
        // recently used documents will show up in the "Recents" category.
        // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
        // shares.
        add(
            DocumentsContract.Root.COLUMN_FLAGS,
            DocumentsContract.Root.FLAG_SUPPORTS_CREATE or
                DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or
                DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
        )

        // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
        add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.title))

        // This document id cannot change after it's shared.
        add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir))

        // The child MIME types are used to filter the roots and only present to the
        // user those roots that contain the desired type somewhere in their file hierarchy.
        add(DocumentsContract.Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir))
        add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDir.freeSpace)
        add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher)
    }

    return result
}

Java

@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {

    // Use a MatrixCursor to build a cursor
    // with either the requested fields, or the default
    // projection if "projection" is null.
    final MatrixCursor result =
            new MatrixCursor(resolveRootProjection(projection));

    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
    }

    // It's possible to have multiple roots (e.g. for multiple accounts in the
    // same app) -- just add multiple cursor rows.
    final MatrixCursor.RowBuilder row = result.newRow();
    row.add(Root.COLUMN_ROOT_ID, ROOT);

    // You can provide an optional summary, which helps distinguish roots
    // with the same title. You can also use this field for displaying an
    // user account name.
    row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary));

    // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
    // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
    // recently used documents will show up in the "Recents" category.
    // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
    // shares.
    row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
            Root.FLAG_SUPPORTS_RECENTS |
            Root.FLAG_SUPPORTS_SEARCH);

    // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
    row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title));

    // This document id cannot change after it's shared.
    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir));

    // The child MIME types are used to filter the roots and only present to the
    // user those roots that contain the desired type somewhere in their file hierarchy.
    row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir));
    row.add(Root.COLUMN_AVAILABLE_BYTES, baseDir.getFreeSpace());
    row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);

    return result;
}

如果文件供應器會連線至一組動態根目錄 (例如連至 USB) 或可供使用者登出的帳戶 可以更新文件 UI 來與這些變更同步 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

當使用者在挑選器 UI 中選擇根層級時,系統就會呼叫此方法。 此方法會擷取由 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 您的文件供應程式 新的文件。

如要支援文件建立功能,您的根層級必須具有 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 意圖時 不含 CATEGORY_OPENABLE 類別 接著,使用者就能在文件供應程式中選取這些虛擬檔案 。接著,文件供應程式將虛擬檔案 用另一種但可開啟的檔案格式 產生圖片,例如圖片 接著用戶端應用程式會開啟虛擬檔案供使用者查看。

如要宣告供應器中的文件為虛擬文件,您需要在 FLAG_VIRTUAL_DOCUMENT 標記為 queryDocument() 方法。此標記會提醒用戶端應用程式該檔案沒有直接存取權 位元碼表示法,且無法直接開啟。

如果您宣告文件供應程式中的檔案為虛擬檔案, 因此強烈建議您另外在 圖片或 PDF 等 MIME 類型。文件提供者 宣告其替代的 MIME 類型 可讓您查看虛擬檔案 getDocumentStreamTypes() 方法。當用戶端應用程式呼叫 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);
}

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

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

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