Nếu đang phát triển một ứng dụng cung cấp dịch vụ lưu trữ cho tệp (chẳng hạn như dịch vụ lưu vào đám mây), thì bạn có thể cung cấp các tệp của mình thông qua Khung truy cập bộ nhớ (SAF) bằng cách viết một trình cung cấp tài liệu tuỳ chỉnh. Trang này mô tả cách tạo trình cung cấp tài liệu tuỳ chỉnh.
Để biết thêm thông tin về cách hoạt động của Khung truy cập bộ nhớ, hãy xem bài viết Tổng quan về Khung truy cập bộ nhớ.
Tệp kê khai
Để triển khai một trình cung cấp tài liệu tuỳ chỉnh, hãy thêm nội dung sau vào tệp kê khai của ứng dụng:
- Mục tiêu của API cấp 19 trở lên.
- Phần tử
<provider>
khai báo nhà cung cấp bộ nhớ tuỳ chỉnh. -
Thuộc tính
android:name
được đặt thành tên của lớp conDocumentsProvider
, tức là tên lớp của lớp con đó, bao gồm cả tên gói:com.example.android.storageprovider.MyCloudProvider
. -
Thuộc tính
android:authority
, là tên gói của bạn (trong ví dụ này làcom.example.android.storageprovider
) cùng với loại nhà cung cấp nội dung (documents
). - Đã đặt thuộc tính
android:exported
thành"true"
. Bạn phải xuất trình cung cấp của mình để các ứng dụng khác có thể thấy. - Thuộc tính
android:grantUriPermissions
được đặt thành"true"
. Chế độ cài đặt này cho phép hệ thống cấp cho các ứng dụng khác quyền truy cập vào nội dung trong nhà cung cấp của bạn. Để thảo luận về cách các ứng dụng khác này có thể duy trì quyền truy cập vào nội dung từ nhà cung cấp của bạn, hãy xem phần Quyền truy cập liên tục. - Quyền
MANAGE_DOCUMENTS
. Theo mặc định, mọi người đều có thể sử dụng một nhà cung cấp. Việc thêm quyền này sẽ hạn chế nhà cung cấp của bạn trong hệ thống. Quy định hạn chế này rất quan trọng đối với tính bảo mật. - Bộ lọc ý định bao gồm thao tác
android.content.action.DOCUMENTS_PROVIDER
để nhà cung cấp của bạn xuất hiện trong bộ chọn khi hệ thống tìm kiếm nhà cung cấp.
Dưới đây là phần trích dẫn từ tệp kê khai mẫu có chứa nhà cung cấp:
<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>
Hỗ trợ các thiết bị chạy Android 4.3 trở xuống
Ý định ACTION_OPEN_DOCUMENT
chỉ có trên các thiết bị chạy Android 4.4 trở lên.
Nếu muốn ứng dụng của mình hỗ trợ ACTION_GET_CONTENT
phù hợp với các thiết bị đang chạy Android 4.3 trở xuống, bạn nên tắt bộ lọc ý định ACTION_GET_CONTENT
trong tệp kê khai đối với các thiết bị chạy Android 4.4 trở lên. Nhà cung cấp tài liệu và ACTION_GET_CONTENT
nên được xem là loại trừ lẫn nhau. Nếu bạn hỗ trợ cả hai đồng thời, thì ứng dụng sẽ xuất hiện hai lần trong giao diện người dùng của bộ chọn hệ thống, cung cấp hai cách để truy cập vào dữ liệu bạn đã lưu trữ. Điều này gây nhầm lẫn cho người dùng.
Dưới đây là cách nên dùng để tắt bộ lọc ý định ACTION_GET_CONTENT
cho các thiết bị chạy Android phiên bản 4.4 trở lên:
- Trong tệp tài nguyên
bool.xml
của bạn trongres/values/
, hãy thêm dòng này:<bool name="atMostJellyBeanMR2">true</bool>
- Trong tệp tài nguyên
bool.xml
của bạn trongres/values-v19/
, hãy thêm dòng này:<bool name="atMostJellyBeanMR2">false</bool>
- Thêm email đại diện hoạt động để tắt bộ lọc ý định
ACTION_GET_CONTENT
cho phiên bản 4.4 (API cấp 19) trở lên. Ví dụ:<!-- 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>
Hợp đồng
Thông thường, khi bạn viết một trình cung cấp nội dung tuỳ chỉnh, một trong những nhiệm vụ là triển khai các lớp hợp đồng, như mô tả trong hướng dẫn dành cho nhà phát triển
Trình cung cấp nội dung. Lớp hợp đồng là một lớp public final
chứa các định nghĩa không đổi cho URI, tên cột, loại MIME và các siêu dữ liệu khác liên quan đến trình cung cấp. SAF cung cấp các lớp hợp đồng này cho bạn, vì vậy, bạn không cần tự viết:
Ví dụ: đây là các cột mà bạn có thể trả về trong con trỏ khi trình cung cấp tài liệu của bạn được truy vấn về tài liệu hoặc thư mục gốc:
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,};
Con trỏ cho thư mục gốc cần bao gồm một số cột bắt buộc nhất định. Các cột đó là:
Con trỏ cho tài liệu cần bao gồm các cột bắt buộc sau:
COLUMN_DOCUMENT_ID
COLUMN_DISPLAY_NAME
COLUMN_MIME_TYPE
COLUMN_FLAGS
COLUMN_SIZE
COLUMN_LAST_MODIFIED
Tạo lớp con của DocumentsProvider
Bước tiếp theo khi viết trình cung cấp tài liệu tuỳ chỉnh là phân lớp con của lớp trừu tượng DocumentsProvider
. Ở mức tối thiểu, bạn phải
triển khai các phương thức sau:
Đây là những phương thức duy nhất bạn bắt buộc phải triển khai, nhưng vẫn còn nhiều phương thức khác mà bạn nên làm. Hãy xem DocumentsProvider
để biết thông tin chi tiết.
Xác định gốc
Khi triển khai queryRoots()
, bạn cần trả về Cursor
trỏ đến tất cả thư mục gốc của nhà cung cấp tài liệu, sử dụng các cột được xác định trong DocumentsContract.Root
.
Trong đoạn mã sau, tham số projection
đại diện cho các trường cụ thể mà phương thức gọi muốn lấy lại. Đoạn mã tạo một con trỏ mới
và thêm một hàng vào đó – một gốc, thư mục cấp cao nhất, như
Downloads hoặc Hình ảnh. Hầu hết các nhà cung cấp chỉ có một gốc. Bạn có thể có nhiều tài khoản, chẳng hạn như trong trường hợp có nhiều tài khoản người dùng. Trong trường hợp đó, chỉ cần thêm
hàng thứ hai vào con trỏ.
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; }
Nếu nhà cung cấp tài liệu của bạn kết nối với một nhóm gốc động (ví dụ: với một thiết bị USB có thể bị ngắt kết nối hoặc một tài khoản mà người dùng có thể đăng xuất), thì bạn có thể cập nhật giao diện người dùng của tài liệu để luôn đồng bộ hoá với những thay đổi đó bằng phương thức ContentResolver.notifyChange()
, như minh hoạ trong đoạn mã sau.
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);
Liệt kê tài liệu trong nhà cung cấp
Quá trình triển khai queryChildDocuments()
phải trả về Cursor
trỏ đến tất cả các tệp trong thư mục được chỉ định, sử dụng các cột được xác định trong DocumentsContract.Document
.
Phương thức này được gọi khi người dùng chọn thư mục gốc của bạn trong giao diện người dùng của bộ chọn.
Phương thức này sẽ truy xuất phần tử con của mã nhận dạng tài liệu do COLUMN_DOCUMENT_ID
chỉ định.
Sau đó, hệ thống sẽ gọi phương thức này bất cứ khi nào người dùng chọn một thư mục con trong nhà cung cấp tài liệu của bạn.
Đoạn mã này tạo một con trỏ mới với các cột được yêu cầu, sau đó thêm thông tin về mọi phần tử con ngay trong thư mục mẹ vào con trỏ. Phần tử con có thể là một hình ảnh, một thư mục khác và bất kỳ tệp nào:
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; }
Nhận thông tin tài liệu
Khi triển khai queryDocument()
, bạn phải trả về Cursor
trỏ đến tệp được chỉ định, bằng cách sử dụng các cột được xác định trong DocumentsContract.Document
.
Phương thức queryDocument()
trả về cùng một thông tin đã được truyền trong queryChildDocuments()
, nhưng đối với một tệp cụ thể:
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; }
Nhà cung cấp tài liệu cũng có thể cung cấp hình thu nhỏ cho một tài liệu bằng cách ghi đè phương thức DocumentsProvider.openDocumentThumbnail()
và thêm cờ FLAG_SUPPORTS_THUMBNAIL
vào các tệp được hỗ trợ.
Đoạn mã sau đây cung cấp một ví dụ về cách triển khai 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); }
Thận trọng: Nhà cung cấp tài liệu không nên trả về hình thu nhỏ quá gấp đôi kích thước do tham số sizeHint
chỉ định.
Mở tài liệu
Bạn phải triển khai openDocument()
để trả về ParcelFileDescriptor
biểu thị
tệp đã chỉ định. Các ứng dụng khác có thể dùng ParcelFileDescriptor
được trả về để truyền trực tuyến dữ liệu. Hệ thống sẽ gọi phương thức này sau khi người dùng chọn một tệp và ứng dụng khách yêu cầu quyền truy cập vào tệp đó bằng cách gọi openFileDescriptor()
.
Ví dụ:
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); } }
Nếu nhà cung cấp tài liệu của bạn truyền trực tuyến tệp hoặc xử lý các cấu trúc dữ liệu phức tạp, hãy cân nhắc triển khai các phương thức createReliablePipe()
hoặc createReliableSocketPair()
.
Các phương thức này cho phép bạn tạo một cặp đối tượng ParcelFileDescriptor
, trong đó bạn có thể trả về một đối tượng và gửi đối tượng còn lại qua ParcelFileDescriptor.AutoCloseOutputStream
hoặc ParcelFileDescriptor.AutoCloseInputStream
.
Hỗ trợ các tài liệu gần đây và tính năng tìm kiếm
Bạn có thể cung cấp danh sách các tài liệu đã sửa đổi gần đây trong thư mục gốc của trình cung cấp tài liệu bằng cách ghi đè phương thức queryRecentDocuments()
và trả về FLAG_SUPPORTS_RECENTS
. Đoạn mã sau đây cho thấy ví dụ về cách triển khai các phương thức 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; }
Bạn có thể lấy mã hoàn chỉnh cho đoạn mã ở trên bằng cách tải mã mẫu StorageProvider của bạn xuống.
Hỗ trợ tạo tài liệu
Bạn có thể cho phép các ứng dụng khách tạo tệp trong nhà cung cấp tài liệu của mình.
Nếu ứng dụng khách gửi ý định ACTION_CREATE_DOCUMENT
, thì nhà cung cấp tài liệu của bạn có thể cho phép ứng dụng khách đó tạo tài liệu mới trong trình cung cấp tài liệu.
Để hỗ trợ tạo tài liệu, thư mục gốc của bạn cần có cờ FLAG_SUPPORTS_CREATE
.
Các thư mục cho phép tạo tệp mới trong đó cần có cờ FLAG_DIR_SUPPORTS_CREATE
.
Nhà cung cấp tài liệu của bạn cũng cần triển khai phương thức createDocument()
. Khi người dùng chọn một thư mục trong trình cung cấp tài liệu của bạn để lưu tệp mới, trình cung cấp tài liệu sẽ nhận được lệnh gọi đến createDocument()
. Trong quá trình triển khai phương thức createDocument()
, bạn sẽ trả về một COLUMN_DOCUMENT_ID
mới cho tệp. Sau đó, ứng dụng khách có thể sử dụng mã nhận dạng đó để xử lý tệp và cuối cùng, hãy gọi openDocument()
để ghi vào tệp mới.
Đoạn mã sau đây minh hoạ cách tạo tệp mới trong trình cung cấp tài liệu.
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); }
Bạn có thể lấy mã hoàn chỉnh cho đoạn mã ở trên bằng cách tải mã mẫu StorageProvider của bạn xuống.
Các tính năng quản lý tài liệu hỗ trợ
Ngoài việc mở, tạo và xem tệp, nhà cung cấp tài liệu của bạn cũng có thể cho phép các ứng dụng khách đổi tên, sao chép, di chuyển và xoá tệp. Để thêm chức năng quản lý tài liệu vào trình cung cấp tài liệu, hãy thêm cờ vào cột COLUMN_FLAGS
của tài liệu để biểu thị chức năng được hỗ trợ. Bạn cũng cần triển khai phương thức tương ứng của lớp DocumentsProvider
.
Bảng sau đây cung cấp cờ COLUMN_FLAGS
và phương thức DocumentsProvider
mà nhà cung cấp tài liệu cần triển khai để hiển thị các tính năng cụ thể.
Tính năng | Gắn cờ | Phương thức |
---|---|---|
Xoá tệp |
FLAG_SUPPORTS_DELETE
|
deleteDocument()
|
Đổi tên tệp |
FLAG_SUPPORTS_RENAME
|
renameDocument()
|
Sao chép tệp vào thư mục mẹ mới trong nhà cung cấp tài liệu |
FLAG_SUPPORTS_COPY
|
copyDocument()
|
Di chuyển tệp từ thư mục này sang thư mục khác trong nhà cung cấp tài liệu |
FLAG_SUPPORTS_MOVE
|
moveDocument()
|
Xoá tệp khỏi thư mục mẹ |
FLAG_SUPPORTS_REMOVE
|
removeDocument()
|
Hỗ trợ tệp ảo và các định dạng tệp thay thế
Tệp ảo, một tính năng được giới thiệu trong Android 7.0 (API cấp 24), cho phép nhà cung cấp tài liệu cung cấp quyền xem đối với các tệp không biểu thị mã byte trực tiếp. Để cho phép các ứng dụng khác xem tệp ảo, nhà cung cấp tài liệu của bạn cần tạo một bản trình bày tệp thay thế có thể mở cho các tệp ảo.
Ví dụ: hãy tưởng tượng một trình cung cấp tài liệu chứa định dạng tệp mà các ứng dụng khác không thể mở trực tiếp, về cơ bản là một tệp ảo.
Khi ứng dụng khách gửi ý định ACTION_VIEW
không thuộc danh mục CATEGORY_OPENABLE
, người dùng có thể chọn các tệp ảo này trong trình cung cấp tài liệu để xem. Sau đó, trình cung cấp tài liệu sẽ trả về tệp ảo ở một định dạng tệp khác nhưng có thể mở như hình ảnh.
Sau đó, ứng dụng khách có thể mở tệp ảo để người dùng xem.
Để khai báo rằng một tài liệu trong trình cung cấp là ảo, bạn cần thêm cờ FLAG_VIRTUAL_DOCUMENT
vào tệp mà phương thức queryDocument()
trả về. Cờ này thông báo cho các ứng dụng khách rằng tệp này không biểu thị mã byte trực tiếp và không thể mở trực tiếp.
Nếu khai báo rằng một tệp trong nhà cung cấp tài liệu của bạn là tệp ảo, bạn nên cung cấp tệp đó trong một loại MIME khác, chẳng hạn như hình ảnh hoặc PDF. Nhà cung cấp tài liệu khai báo các loại MIME thay thế mà nhà cung cấp hỗ trợ để xem tệp ảo bằng cách ghi đè phương thức getDocumentStreamTypes()
. Khi các ứng dụng khách gọi phương thức getStreamTypes(android.net.Uri, java.lang.String)
, hệ thống sẽ gọi phương thức getDocumentStreamTypes()
của trình cung cấp tài liệu. Sau đó, phương thức getDocumentStreamTypes()
sẽ trả về một mảng các loại MIME thay thế mà trình cung cấp tài liệu hỗ trợ cho tệp.
Sau khi ứng dụng xác định rằng nhà cung cấp tài liệu có thể tạo tài liệu ở định dạng tệp có thể xem, ứng dụng khách sẽ gọi phương thức openTypedAssetFileDescriptor()
. Phương thức này sẽ gọi nội bộ phương thức openTypedDocument()
của trình cung cấp tài liệu. Trình cung cấp tài liệu sẽ trả tệp cho ứng dụng ở định dạng tệp được yêu cầu.
Đoạn mã sau đây minh hoạ cách triển khai đơn giản của các phương thức getDocumentStreamTypes()
và 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(); }
Bảo mật
Giả sử nhà cung cấp tài liệu của bạn là một dịch vụ bộ nhớ trên đám mây được bảo vệ bằng mật khẩu và bạn muốn đảm bảo rằng người dùng đã đăng nhập trước khi bắt đầu chia sẻ tệp của họ.
Ứng dụng của bạn nên làm gì nếu người dùng không đăng nhập? Giải pháp là trả về gốc 0 trong quá trình triển khai queryRoots()
. Điều đó có nghĩa là con trỏ gốc trống:
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; }
Bước còn lại là gọi getContentResolver().notifyChange()
.
Bạn có nhớ DocumentsContract
không? Chúng tôi đang sử dụng nó để tạo URI này. Đoạn mã sau đây yêu cầu hệ thống truy vấn thư mục gốc của trình cung cấp tài liệu mỗi khi trạng thái đăng nhập của người dùng thay đổi. Nếu người dùng chưa đăng nhập, lệnh gọi đến queryRoots()
sẽ trả về một con trỏ trống, như minh hoạ ở trên. Điều này đảm bảo rằng tài liệu của nhà cung cấp chỉ xuất hiện nếu người dùng đã đăng nhập vào nhà cung cấp đó.
Kotlin
private fun onLoginButtonClick() { loginOrLogout() getContentResolver().notifyChange( DocumentsContract.buildRootsUri(AUTHORITY), null ) }
Java
private void onLoginButtonClick() { loginOrLogout(); getContentResolver().notifyChange(DocumentsContract .buildRootsUri(AUTHORITY), null); }
Để xem mã mẫu liên quan đến trang này, hãy tham khảo:
Đối với những video liên quan đến trang này, hãy tham khảo:
- DevBytes: Khung truy cập bộ nhớ Android 4.4: Nhà cung cấp
- Khung truy cập bộ nhớ: Xây dựng DocumentsProvider
- Tệp ảo trong Khung truy cập bộ nhớ (Storage Access Framework)
Để biết thêm thông tin liên quan, hãy tham khảo:
- Xây dựng DocumentsProvider
- Mở tệp bằng khung truy cập bộ nhớ (Storage Access Framework)
- Thông tin cơ bản về Trình cung cấp nội dung