如果您正在開發應用程式 (例如雲端儲存服務) 提供檔案儲存空間服務,您可以編寫自訂文件供應器,透過儲存空間存取架構 (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
意圖篩選器:
- 在
bool.xml
資源檔案的res/values/
底下,新增此行:<bool name="atMostJellyBeanMR2">true</bool>
- 在
bool.xml
資源檔案的res/values-v19/
底下,新增此行:<bool name="atMostJellyBeanMR2">false</bool>
- 新增活動別名,在 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,};
根目錄的遊標必須包含特定的必要資料欄。這些欄分別是:
文件的遊標必須包含下列必要欄:
COLUMN_DOCUMENT_ID
COLUMN_DISPLAY_NAME
COLUMN_MIME_TYPE
COLUMN_FLAGS
COLUMN_SIZE
COLUMN_LAST_MODIFIED
建立 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.AutoCloseOutputStream
或 ParcelFileDescriptor.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<>(); // Iterate over the list of supported mime types to find a match. for (int i=0; i < 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); }
如需與本頁相關的程式碼範例,請參閱:
如要查看與本頁相關的影片,請參閱:
如需其他相關資訊,請參閱: