page.title=Khuôn khổ Truy cập Kho lưu trữ @jd:body
Android 4.4 (API mức 19) giới thiệu Khuôn khổ Truy cập Kho lưu trữ (SAF). SAF giúp người dùng đơn giản hóa việc duyệt và mở tài liệu, hình ảnh và các tệp khác giữa tất cả trình cung cấp lưu trữ tài liệu mà họ thích. UI tiêu chuẩn, dễ sử dụng cho phép người dùng duyệt tệp và truy cập hoạt động gần đây một cách nhất quán giữa các ứng dụng và trình cung cấp.
Dịch vụ lưu trữ đám mây hoặc cục bộ có thể tham gia vào hệ sinh thái này bằng cách triển khai một {@link android.provider.DocumentsProvider} để gói gọn các dịch vụ của mình. Những ứng dụng máy khách cần truy cập vào tài liệu của một trình cung cấp có thể tích hợp với SAF chỉ bằng một vài dòng mã.
SAF bao gồm:
Một số tính năng được SAF cung cấp bao gồm:
SAF tập trung xoay quanh một trình cung cấp nội dung là một lớp con của lớp {@link android.provider.DocumentsProvider}. Trong một trình cung cấp tài liệu, dữ liệu được cấu trúc thành một phân cấp tệp truyền thống:
Hình 1. Mô hình dữ liệu của trình cung cấp tài liệu. Một Phần gốc chỉ đến một Tài liệu duy nhất, sau đó nó bắt đầu xòe ra toàn bộ cây.
Lưu ý điều sau đây:
Như nêu trên, mô hình dữ liệu của trình cung cấp tài liệu được dựa trên một phân cấp tệp truyền thống. Tuy nhiên, bạn có thể thực tế lưu trữ dữ liệu của mình bằng bất kỳ cách nào mà mình thích, miễn là nó có thể được truy cập thông qua API {@link android.provider.DocumentsProvider}. Ví dụ, bạn có thể sử dụng kho lưu trữ đám mây dựa trên tag cho dữ liệu của mình.
Hình 2 minh họa một ví dụ về cách mà một ứng dụng ảnh có thể sử dụng SAF để truy cập dữ liệu được lưu trữ:
Hình 2. Dòng Khuôn khổ Truy cập Kho lưu trữ
Lưu ý điều sau đây:
Hình 3 minh họa một bộ chọn mà trong đó một người dùng đang tìm kiếm hình ảnh đã chọn một tài khoản Google Drive:
Hình 3. Bộ chọn
Khi người dùng chọn Google Drive, hình ảnh được hiển thị như minh họa trong hình 4. Từ điểm đó trở đi, người dùng có thể tương tác với chúng theo bất kỳ cách nào được hỗ trợ bởi trình cung cấp và ứng dụng máy khách.
Hình 4. Hình ảnh
Trên phiên bản Android 4.3 và thấp hơn, nếu bạn muốn ứng dụng của mình truy xuất một tệp từ một ứng dụng khác, nó phải gọi ra một ý định chẳng hạn như {@link android.content.Intent#ACTION_PICK} hay {@link android.content.Intent#ACTION_GET_CONTENT}. Khi đó, người dùng phải chọn một ứng dụng duy nhất mà từ đó họ chọn một tệp và ứng dụng được chọn phải cung cấp một giao diện người dùng để người dùng duyệt và chọn từ các tệp có sẵn.
Trên phiên bản Android 4.4 trở lên, bạn có thêm một tùy chọn là sử dụng ý định {@link android.content.Intent#ACTION_OPEN_DOCUMENT}, nó hiển thị một UI bộ chọn được điều khiển bởi hệ thống, cho phép người dùng duyệt tất cả tệp mà các ứng dụng khác đã cung cấp. Từ UI duy nhất này, người dùng có thể chọn một tệp từ bất kỳ ứng dụng nào được hỗ trợ.
{@link android.content.Intent#ACTION_OPEN_DOCUMENT} không nhằm mục đích thay thế cho {@link android.content.Intent#ACTION_GET_CONTENT}. Bạn nên sử dụng cái nào sẽ phụ thuộc vào nhu cầu của ứng dụng của bạn:
Phần này mô tả cách ghi các ứng dụng máy khách dựa trên {@link android.content.Intent#ACTION_OPEN_DOCUMENT} và các ý định {@link android.content.Intent#ACTION_CREATE_DOCUMENT}.
Đoạn mã HTML sau sử dụng {@link android.content.Intent#ACTION_OPEN_DOCUMENT} để tìm kiếm các trình cung cấp tài liệu mà chứa tệp hình ảnh:
private static final int READ_REQUEST_CODE = 42; ... /** * Fires an intent to spin up the "file chooser" UI and select an image. */ public void performFileSearch() { // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file // browser. Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); // Filter to only show results that can be "opened", such as a // file (as opposed to a list of contacts or timezones) intent.addCategory(Intent.CATEGORY_OPENABLE); // Filter to show only images, using the image MIME data type. // If one wanted to search for ogg vorbis files, the type would be "audio/ogg". // To search for all documents available via installed storage providers, // it would be "*/*". intent.setType("image/*"); startActivityForResult(intent, READ_REQUEST_CODE); }
Lưu ý điều sau đây:
Sau khi người dùng chọn một tài liệu trong bộ chọn, {@link android.app.Activity#onActivityResult onActivityResult()} sẽ được gọi. URI tro tới tài liệu được chọn sẽ nằm trong tham số {@code resultData} . Trích xuất UI bằng cách sử dụng {@link android.content.Intent#getData getData()}. Sau khi có nó, bạn có thể sử dụng nó để truy xuất tài liệu mà người dùng muốn. Ví dụ:
@Override public void onActivityResult(int requestCode, int resultCode, Intent resultData) { // The ACTION_OPEN_DOCUMENT intent was sent with the request code // READ_REQUEST_CODE. If the request code seen here doesn't match, it's the // response to some other intent, and the code below shouldn't run at all. if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) { // The document selected by the user won't be returned in the intent. // Instead, a URI to that document will be contained in the return intent // provided to this method as a parameter. // Pull that URI using resultData.getData(). Uri uri = null; if (resultData != null) { uri = resultData.getData(); Log.i(TAG, "Uri: " + uri.toString()); showImage(uri); } } }
Sau khi có URI cho một tài liệu, bạn có quyền truy cập siêu dữ liệu của nó. Đoạn mã HTML này bắt siêu dữ liệu cho một tài liệu được quy định bởi URI, và ghi lại nó:
public void dumpImageMetaData(Uri uri) { // The query, since it only applies to a single document, will only return // one row. There's no need to filter, sort, or select fields, since we want // all fields for one document. Cursor cursor = getActivity().getContentResolver() .query(uri, null, null, null, null, null); try { // moveToFirst() returns false if the cursor has 0 rows. Very handy for // "if there's anything to look at, look at it" conditionals. if (cursor != null && cursor.moveToFirst()) { // Note it's called "Display Name". This is // provider-specific, and might not necessarily be the file name. String displayName = cursor.getString( cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); Log.i(TAG, "Display Name: " + displayName); int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE); // If the size is unknown, the value stored is null. But since an // int can't be null in Java, the behavior is implementation-specific, // which is just a fancy term for "unpredictable". So as // a rule, check if it's null before assigning to an int. This will // happen often: The storage API allows for remote files, whose // size might not be locally known. String size = null; if (!cursor.isNull(sizeIndex)) { // Technically the column stores an int, but cursor.getString() // will do the conversion automatically. size = cursor.getString(sizeIndex); } else { size = "Unknown"; } Log.i(TAG, "Size: " + size); } } finally { cursor.close(); } }
Sau khi có URI cho một tài liệu, bạn có thể mở nó hoặc làm bất kỳ điều gì mà bạn muốn.
Sau đây là một ví dụ về cách bạn có thể mở một {@link android.graphics.Bitmap}:
private Bitmap getBitmapFromUri(Uri uri) throws IOException { ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(uri, "r"); FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor); parcelFileDescriptor.close(); return image; }
Lưu ý rằng bạn không nên thực hiện thao tác này trên luồng UI. Thực hiện điều này dưới nền bằng cách sử dụng {@link android.os.AsyncTask}. Sau khi mở bitmap, bạn có thể hiển thị nó trong một {@link android.widget.ImageView}.
Sau đây là một ví dụ về cách mà bạn có thể nhận một {@link java.io.InputStream} từ URI. Trong đoạn mã HTML này, các dòng tệp đang được đọc thành một xâu:
private String readTextFromUri(Uri uri) throws IOException { InputStream inputStream = getContentResolver().openInputStream(uri); BufferedReader reader = new BufferedReader(new InputStreamReader( inputStream)); StringBuilder stringBuilder = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { stringBuilder.append(line); } fileInputStream.close(); parcelFileDescriptor.close(); return stringBuilder.toString(); }
Ứng dụng của bạn có thể tạo một tài liệu mới trong một trình cung cấp tài liệu bằng cách sử dụng ý định {@link android.content.Intent#ACTION_CREATE_DOCUMENT} . Để tạo một tệp, bạn cấp cho ý định của mình một kiểu MIME và tên tệp, và khởi chạy nó bằng một mã yêu cầu duy nhất. Phần còn lại sẽ được làm hộ bạn:
// Here are some examples of how you might call this method. // The first parameter is the MIME type, and the second parameter is the name // of the file you are creating: // // createFile("text/plain", "foobar.txt"); // createFile("image/png", "mypicture.png"); // Unique request code. private static final int WRITE_REQUEST_CODE = 43; ... private void createFile(String mimeType, String fileName) { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); // Filter to only show results that can be "opened", such as // a file (as opposed to a list of contacts or timezones). intent.addCategory(Intent.CATEGORY_OPENABLE); // Create a file with the requested MIME type. intent.setType(mimeType); intent.putExtra(Intent.EXTRA_TITLE, fileName); startActivityForResult(intent, WRITE_REQUEST_CODE); }
Sau khi tạo một tài liệu mới, bạn có thể nhận URI của tài liệu trong {@link android.app.Activity#onActivityResult onActivityResult()}, sao cho bạn có thể tiếp tục ghi nó.
Nếu bạn có URI cho một tài liệu và {@link android.provider.DocumentsContract.Document#COLUMN_FLAGS Document.COLUMN_FLAGS} của tài liệu chứa {@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE SUPPORTS_DELETE}, bạn có thể xóa tài liệu đó. Ví dụ:
DocumentsContract.deleteDocument(getContentResolver(), uri);
Bạn có thể sử dụng SAF để chỉnh sửa một tài liệu văn bản ngay tại chỗ. Đoạn mã HTML này thể hiện ý định {@link android.content.Intent#ACTION_OPEN_DOCUMENT} và sử dụng thể loại {@link android.content.Intent#CATEGORY_OPENABLE} để chỉ hiển thị những tài liệu có thể mở được. Nó lọc thêm để chỉ hiển thị những tệp văn bản:
private static final int EDIT_REQUEST_CODE = 44; /** * Open a file for writing and append some text to it. */ private void editDocument() { // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's // file browser. Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); // Filter to only show results that can be "opened", such as a // file (as opposed to a list of contacts or timezones). intent.addCategory(Intent.CATEGORY_OPENABLE); // Filter to show only text files. intent.setType("text/plain"); startActivityForResult(intent, EDIT_REQUEST_CODE); }
Tiếp theo, từ {@link android.app.Activity#onActivityResult onActivityResult()} (xem Kết quả tiến trình) bạn có thể gọi mã để thực hiện chỉnh sửa. Đoạn mã HTML sau nhận được một {@link java.io.FileOutputStream} từ {@link android.content.ContentResolver}. Theo mặc định, nó sử dụng chế độ “ghi”. Cách tốt nhất là yêu cầu lượng quyền truy cập bạn cần ở mức ít nhất, vì thế đừng yêu cầu quyền đọc/ghi nếu bạn chỉ cần quyền ghi:
private void alterDocument(Uri uri) { try { ParcelFileDescriptor pfd = getActivity().getContentResolver(). openFileDescriptor(uri, "w"); FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor()); fileOutputStream.write(("Overwritten by MyCloud at " + System.currentTimeMillis() + "\n").getBytes()); // Let the document provider know you're done by closing the stream. fileOutputStream.close(); pfd.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
Khi ứng dụng của bạn mở một tệp để đọc hoặc ghi, hệ thống sẽ cấp cho ứng dụng của bạn một quyền URI được cấp cho tệp đó. Quyền này sẽ kéo dài tới khi thiết bị của bạn khởi động lại. Nhưng giả sử ứng dụng của bạn là một ứng dụng chỉnh sửa hình ảnh, và bạn muốn người dùng có thể truy cập 5 hình ảnh cuối cùng mà họ đã chỉnh sửa, trực tiếp từ ứng dụng của bạn. Nếu thiết bị của người dùng đã khởi động lại, bạn sẽ phải gửi người dùng trở lại bộ chọn hệ thống để tìm các tệp đó, đây rõ ràng không phải là cách lý tưởng.
Để tránh điều này xảy ra, bạn có thể cố định các quyền mà hệ thống cấp cho ứng dụng của bạn. Ứng dụng của bạn sẽ "nhận" cấp quyền URI có thể cố định mà hệ thống cung cấp một cách hiệu quả. Điều này cho phép người dùng có quyền liên tục truy cập các tệp đó thông qua ứng dụng của bạn, ngay cả khi thiết bị đã bị khởi động lại:
final int takeFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); // Check for the freshest data. getContentResolver().takePersistableUriPermission(uri, takeFlags);
Còn một bước cuối cùng. Bạn có thể đã lưu các URI gần đây nhất mà ứng dụng của bạn đã truy cập, nhưng chúng còn thể không còn hợp lệ—một ứng dụng khác có thể đã xóa hoặc sửa đổi tài liệu. Vì thế, bạn luôn nên gọi {@code getContentResolver().takePersistableUriPermission()} để kiểm tra dữ liệu mới nhất.
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ư một dịch vụ lưu trữ đám mây), bạn có thể cung cấp các tệp của mình thông qua SAF bằng cách ghi một trình cung cấp tài liệu tùy chỉnh. Phần này mô tả cách làm điều này.
Để triển khai một trình cung cấp tài liệu tùy chỉnh, hãy thêm nội dung sau vào bản kê khai của ứng dụng của bạn:
<provider>
khai báo trình cung cấp lưu trữ
tùy chỉnh của bạn. com.example.android.storageprovider.MyCloudProvider
.com.example.android.storageprovider
) cộng với kiểu của trình cung cấp nội dung
(documents
). Ví dụ, {@code com.example.android.storageprovider.documents}.android:exported
được đặt 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 nó.android:grantUriPermissions
được đặt thành
"true"
. Thiết đặ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 trình cung cấp của bạn. Để thảo luận về cách cố định quyền được cấp cho
một tài liệu cụ thể, hãy xem phầnCố định các quyền.<bool name="atLeastKitKat">false</bool>
<bool name="atLeastKitKat">true</bool>
Sau đây là các đoạn trích từ một bản kê khai mẫu chứa một 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" android:enabled="@bool/atLeastKitKat"> <intent-filter> <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> </intent-filter> </provider> </application> </manifest>
Ý định {@link android.content.Intent#ACTION_OPEN_DOCUMENT} chỉ có sẵn trên các thiết bị chạy phiên bản Android 4.4 trở lên. Nếu bạn muốn ứng dụng của mình hỗ trợ {@link android.content.Intent#ACTION_GET_CONTENT} để tạo điều kiện cho các thiết bị đang chạy phiên bản Android 4.3 và thấp hơn, bạn nên vô hiệu hóa bộ lọc ý định {@link android.content.Intent#ACTION_GET_CONTENT} trong bản kê khai của bạn cho các thiết bị chạy phiên bản Android 4.4 trở lên. Một trình cung cấp tài liệu và {@link android.content.Intent#ACTION_GET_CONTENT} nên được xem xét loại trừ lẫn nhau. Nếu bạn hỗ trợ cả hai đồng thời, ứng dụng của bạn sẽ xuất hiện hai lần trong UI của bộ chọn hệ thống, đưa ra hai cách khác nhau để truy cập dữ liệu đã lưu của bạn. Điều này có thể khiến người dùng bị nhầm lẫn.
Sau đây là cách được khuyến cáo để vô hiệu hóa bộ lọc ý định {@link android.content.Intent#ACTION_GET_CONTENT} đối với các thiết bị chạy phiên bản Android 4.4 hoặc cao hơn:
<bool name="atMostJellyBeanMR2">true</bool>
<bool name="atMostJellyBeanMR2">false</bool>
<!-- 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>
Thường khi bạn ghi một trình cung cấp nội dung tùy chỉnh, một trong những tác 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 {@code public final} mà chứa các định nghĩa hằng số cho URI, tên cột, kiểu MIME và siêu dữ liệu khác liên quan tới trình cung cấp. SAF cung cấp những lớp hợp đồng này cho bạn, vì thế bạn không cần tự ghi:
Ví dụ, sau đây là các cột bạn có thể trả về trong một con chạy 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 phần gốc:
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,};
Bước tiếp theo trong khi ghi một trình cung cấp tài liệu tùy chỉnh đó là phân lớp con cho lớp tóm tắt {@link android.provider.DocumentsProvider}. Tối thiểu, bạn cần triển khai các phương pháp sau:
Đây là những phương pháp duy nhất mà bạn được yêu cầu phải triển khai, nhưng còn nhiều phương pháp nữa mà bạn có thể muốn triển khai. Xem {@link android.provider.DocumentsProvider} để biết chi tiết.
Việc bạn triển khai {@link android.provider.DocumentsProvider#queryRoots queryRoots()} phải trả về một {@link android.database.Cursor} trỏ về tất cả thư mục gốc trong trình cung cấp tài liệu của bạn, bằng cách sử dụng các cột được định nghĩa trong {@link android.provider.DocumentsContract.Root}.
Trong đoạn mã HTML sau, tham số {@code projection} biểu diễn các trường cụ thể mà hàm gọi muốn nhận về. Đoạn mã HTML tạo một con chạy mới và thêm một hàng vào nó—một thư mục gốc, mức cao nhất, như Downloads hoặc Images. Hầu hết các trình cung cấp chỉ có một phần gốc. Bạn có thể có nhiều hơn một, ví dụ, trong trường hợp nhiều tài khoản người dùng. Trong trường hợp đó, chỉ cần thêm một hàng thứ hai vào con chạy.
@Override public Cursor queryRoots(String[] projection) throws FileNotFoundException { // Create 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. // Construct one row for a root called "MyCloud". final MatrixCursor.RowBuilder row = result.newRow(); row.add(Root.COLUMN_ROOT_ID, ROOT); 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 once it's shared. row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir)); // The child MIME types are used to filter the roots and only present to the // user roots that contain the desired type somewhere in their file hierarchy. row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(mBaseDir)); row.add(Root.COLUMN_AVAILABLE_BYTES, mBaseDir.getFreeSpace()); row.add(Root.COLUMN_ICON, R.drawable.ic_launcher); return result; }
Việc bạn triển khai {@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()} phải trả về một {@link android.database.Cursor} mà chỉ đến tất cả tệp trong thư mục được chỉ định, bằng cách sử dụng các cột được định nghĩa trong {@link android.provider.DocumentsContract.Document}.
Phương pháp này được gọi khi bạn chọn một thư mục gốc ứng dụng trong UI bộ chọn. Nó nhận được tài liệu con của một thư mục nằm dưới phần gốc. Nó có thể được gọi ở bất kỳ mức nào trong phân cấp tệp , không chỉ phần gốc. Đoạn mã HTML này tạo một con chạy mới bằng các cột được yêu cầu, sau đó thêm thông tin về mọi tệp con trực tiếp trong thư mục mẹ vào con chạy. Tệp con có thể là một hình ảnh, một thư mục khác—bất kỳ tệp nào:
@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; }
Việc bạn triển khai {@link android.provider.DocumentsProvider#queryDocument queryDocument()} phải trả về một {@link android.database.Cursor} mà chỉ đến tệp được chỉ định, bằng cách sử dụng các cột được định nghĩa trong {@link android.provider.DocumentsContract.Document}.
Phương pháp {@link android.provider.DocumentsProvider#queryDocument queryDocument()} trả về cùng thông tin đã được chuyển trong {@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}, nhưng là đối với một tệp cụ thể:
@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; }
Bạn phải triển khai {@link android.provider.DocumentsProvider#openDocument openDocument()} để trả về một {@link android.os.ParcelFileDescriptor} biểu diễn tệp được chỉ định. Các ứng dụng khác có thể sử dụng {@link android.os.ParcelFileDescriptor} được trả về để truyền phát dữ liệu. Hệ thống gọi phương pháp này sau khi người dùng chọn một tệp và ứng dụng máy khách yêu cầu truy cập nó bằng cách gọi {@link android.content.ContentResolver#openFileDescriptor openFileDescriptor()}. Ví dụ:
@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 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); } }
Giả sử trình cung cấp tài liệu của bạn là một dịch vụ lưu trữ đá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 được đă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 không đăng nhập? Giải pháp là trả về phần gốc 0 trong triển khai {@link android.provider.DocumentsProvider#queryRoots queryRoots()} của bạn. Cụ thể là một con chạy gốc trống:
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 {@code getContentResolver().notifyChange()}. Bạn còn nhớ {@link android.provider.DocumentsContract} chứ? Chúng ta đang sử dụng nó để tạo URI này. Đoạn mã HTML sau báo cho hệ thống truy vấn các phần gốc trong trình cung cấp tài liệu của bạn 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 được đăng nhập, lệnh gọi tới {@link android.provider.DocumentsProvider#queryRoots queryRoots()} sẽ trả về một con chạy trống như minh họa bên trên. Điều này đảm bảo rằng tài liệu của một trình cung cấp chỉ có sẵn nếu người dùng đăng nhập vào trình cung cấp đó.
private void onLoginButtonClick() { loginOrLogout(); getContentResolver().notifyChange(DocumentsContract .buildRootsUri(AUTHORITY), null); }