Nếu bạn đ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), 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 Tổng quan về Khung truy cập bộ nhớ.
Tệp kê khai
Để triển khai trình cung cấp tài liệu tuỳ chỉnh, hãy thêm đoạn mã sau vào tệp tệp kê khai:
- Mục tiêu thuộc API cấp 19 trở lên.
- Một phần tử
<provider>
khai báo dung lượng lưu trữ tuỳ chỉnh của bạn Google Cloud. -
Thuộc tính
android:name
được đặt thành tên của thuộc tính Lớp conDocumentsProvider
, là tên lớp, 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,com.example.android.storageprovider
) cùng với loại trình cung cấp nội dung (documents
). - Thuộc tính
android:exported
được đặt thành"true"
. Bạn phải xuất nhà cung cấp của mình để các ứng dụng khác có thể thấy thông tin đó. - 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 quyền truy cập cho các ứng dụng khác 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 của họ vào nội dung từ nhà cung cấp của bạn, xem Duy trì quyền truy cập. - Quyền
MANAGE_DOCUMENTS
. Theo mặc định, sẽ có nhà cung cấp cho mọi người. Khi thêm quyền này, nhà cung cấp của bạn sẽ bị hạn chế trong hệ thống. Quy định hạn chế này rất quan trọng vì 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 sẽ 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 bao gồm trì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ợ thiết bị chạy Android 4.3 trở xuống
Chiến lược phát hành đĩa đơn
Chỉ có ý định ACTION_OPEN_DOCUMENT
trên các thiết bị chạy Android 4.4 trở lên.
Nếu bạn muốn ứng dụng của mình hỗ trợ ACTION_GET_CONTENT
tương thích với các thiết bị 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 của bạn cho các thiết bị chạy Android 4.4 trở lên. Đáp
nhà cung cấp tài liệu và ACTION_GET_CONTENT
cần được cân nhắc
loại trừ lẫn nhau. Nếu bạn hỗ trợ cả hai cùng lúc, ứng dụng của bạn
xuất hiện hai lần trong giao diện người dùng bộ chọn hệ thống, cung cấp hai cách truy cập
dữ liệu bạn đã lưu trữ. Điều này gây nhầm lẫn cho người dùng.
Sau đây là cách khuyến nghị để vô hiệu hoá
Bộ lọc ý định ACTION_GET_CONTENT
cho 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 một
hoạt động
biệt hiệu để tắt ý định
ACTION_GET_CONTENT
bộ lọc 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 các nhiệm vụ là
triển khai các lớp hợp đồng, như được mô tả trong
Hướng dẫn 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 định nghĩa hằng số cho URI, tên cột, loại MIME và
siêu dữ liệu khác liên quan đến 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 phải viết
sở hữu:
Ví dụ: đây là những 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ỏ của bạn 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 này bao gồm:
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 một lớp con của DocumentsProvider
Bước tiếp theo trong việc viết trình cung cấp tài liệu tuỳ chỉnh là tạo lớp con cho
lớp trừu tượng DocumentsProvider
. Ít nhất, bạn phải
triển khai các phương thức sau:
Đây là những phương pháp duy nhất mà bạn bắt buộc phải triển khai, nhưng
có nhiều thứ khác mà bạn có thể muốn. Xem DocumentsProvider
để biết thông tin chi tiết.
Xác định một căn bậc
Việc triển khai queryRoots()
của bạn cần trả về Cursor
trỏ đến tất cả
các thư mục gốc của trì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 nhận lại. Đoạn mã tạo một con trỏ mới
và thêm một hàng vào đó—một thư mục gốc, một thư mục cấp cao nhất, chẳng hạn như
Tệp đã tải xuống hoặc Hình ảnh. Hầu hết các trình cung cấp chỉ có một gốc. Bạn có thể có nhiều tài khoản,
ví dụ: 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 trình cung cấp tài liệu của bạn kết nối với một tập hợp gốc động—ví dụ: với USB
thiết bị có thể bị ngắt kết nối hoặc tài khoản mà người dùng có thể đăng xuất — 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ộ với những thay đổi đó bằng cách sử dụng
ContentResolver.notifyChange()
, như minh hoạ trong đoạn mã sau đây.
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
Việc triển khai của bạn đối với
queryChildDocuments()
phải trả về Cursor
trỏ đến tất cả tệp trong
thư mụ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 truy xuất phần tử con của ID tài liệu được chỉ định bởi
COLUMN_DOCUMENT_ID
.
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
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 trực tiếp 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 – 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
Việc triển khai của bạn đối với
queryDocument()
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
.
queryDocument()
trả về cùng một thông tin đã được chuyển vào
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ủa bạn cũng có thể cung cấp hình thu nhỏ cho một tài liệu bằng cách
ghi đè
DocumentsProvider.openDocumentThumbnail()
và thêm phương thức
FLAG_SUPPORTS_THUMBNAIL
gắn cờ cho các tệp được hỗ trợ.
Đoạn mã sau đây cung cấp 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:
Trình cung cấp tài liệu không được trả về hình thu nhỏ nhiều hơn 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ề một ParcelFileDescriptor
đại diện cho
tệp được chỉ định. Các ứng dụng khác có thể dùng ParcelFileDescriptor
được trả về
để truyền dữ liệu. Hệ thống 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 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 các tệp hoặc xử lý các công việc phức tạp
cấu trúc dữ liệu, hãy cân nhắc việc triển khai
createReliablePipe()
hoặc
createReliableSocketPair()
.
Các phương thức đó cho phép bạn tạo một cặp
Đối tượng ParcelFileDescriptor
mà bạn có thể trả về một đối tượng
rồi gửi tệp còn lại qua một
ParcelFileDescriptor.AutoCloseOutputStream
hoặc
ParcelFileDescriptor.AutoCloseInputStream
.
Hỗ trợ nội dung tìm kiếm và tài liệu gần đây
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
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ể nhận mã hoàn chỉnh cho đoạn mã ở trên bằng cách tải xuống StorageProvider mã mẫu.
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 một ứng dụng khách gửi một ACTION_CREATE_DOCUMENT
ý định của bạn, trì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
.
Thư mục cho phép tạo tệp mới trong thư mục cần có
FLAG_DIR_SUPPORTS_CREATE
cờ.
Nhà cung cấp tài liệu của bạn cũng cần triển khai
createDocument()
. Khi người dùng chọn một thư mục trong
trình cung cấp tài liệu để lưu tệp mới, trình cung cấp tài liệu sẽ nhận được cuộc gọi đến
createDocument()
. Trong quá trình triển khai
createDocument()
, bạn sẽ trả về một phương thức mới
COLUMN_DOCUMENT_ID
cho
. Sau đó, ứng dụng khách có thể dùng mã nhận dạng đó để lấy tên người dùng cho tệp đó
và cuối cùng là 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 một 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ể nhận mã hoàn chỉnh cho đoạn mã ở trên bằng cách tải xuống StorageProvider mã mẫu.
Hỗ trợ các tính năng quản lý tài liệu
Ngoài 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 ứ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
nhà cung cấp tài liệu của bạn, hãy thêm một cờ vào
COLUMN_FLAGS
cột
để cho biết 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 DocumentsProvider
.
Bảng sau đây cung cấp
Cờ COLUMN_FLAGS
và phương thức DocumentsProvider
mà một 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 một thư mục mẹ mới trong trì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 trình cung cấp tài liệu |
FLAG_SUPPORTS_MOVE
|
moveDocument()
|
Xoá một tệp khỏi thư mục mẹ của tệp đó |
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 ra mắt trong Android 7.0 (API cấp 24), cho phép các trình cung cấp tài liệu để cung cấp quyền xem tệp không có đại diện trực tiếp cho mã byte. Cách 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 tệp có thể mở thay thế đại diện cho các tệp ảo.
Ví dụ: hãy tưởng tượng rằng trình cung cấp tài liệu có chứa một 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 một ứng dụng khách gửi ý định ACTION_VIEW
không có danh mục CATEGORY_OPENABLE
,
thì 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
ở định dạng tệp khác nhưng có thể mở được, chẳng hạn như hình ảnh.
Sau đó, ứng dụng có thể mở tệp ảo để người dùng xem.
Để khai báo rằng tài liệu trong trình cung cấp là tài liệu ảo, bạn cần thêm thuộc tính
FLAG_VIRTUAL_DOCUMENT
gắn cờ vào tệp được trả về bởi
queryDocument()
. Cờ này cảnh báo các ứng dụng khách rằng tệp không có
biểu thị mã byte và không thể mở trực tiếp.
Nếu bạn khai báo rằng một tệp trong trình cung cấp tài liệu là tệp ảo,
bạn nên cung cấp mã này trong
Loại MIME như hình ảnh hoặc tệp PDF. Trình cung cấp tài liệu
khai báo loại MIME thay thế mà nó
hỗ trợ xem tệp ảo bằng cách ghi đè
getDocumentStreamTypes()
. Khi ứng dụng khách gọi hàm
getStreamTypes(android.net.Uri, java.lang.String)
thì hệ thống sẽ gọi phương thức
getDocumentStreamTypes()
của trình cung cấp tài liệu. Chiến lược phát hành đĩa đơn
getDocumentStreamTypes()
sau đó 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ợ tệp.
Sau khi khách hàng xác định
nhà cung cấp tài liệu có thể tạo tài liệu trong một tệp có thể xem được
thì ứng dụng khách sẽ gọi hàm
openTypedAssetFileDescriptor()
phương thức này gọi nội bộ
openTypedDocument()
. Trình cung cấp tài liệu trả về tệp cho ứng dụng khách trong
đị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
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ụ lưu trữ 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ạn 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 chưa đăng nhập? Giải pháp là trả lại
bằng 0 trong quá trình triển khai queryRoots()
. Tức 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 sử dụng công cụ này để khiến
URI này. Đoạn mã sau đây yêu cầu hệ thống truy vấn các gốc của
nhà cung cấp tài liệu bất cứ khi nào trạng thái đăng nhập của người dùng thay đổi. Nếu người dùng không
đã đăng nhập, lệnh gọi đến queryRoots()
sẽ trả về
con trỏ trống, như được hiển thị ở trên. Điều này giúp đảm bảo rằng giấy tờ của nhà cung cấp chỉ
có sẵ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); }
Đối với 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 Trình cung cấp tài liệu
- Tệp ảo trong Khung truy cập bộ nhớ
Để biết thêm thông tin liên quan, hãy tham khảo:
- Xây dựng Trình cung cấp tài liệu
- Mở tệp bằng Khung truy cập bộ nhớ
- Kiến thức cơ bản về trình cung cấp nội dung