lightbulb_outline Help shape the future of the Google Play Console, Android Studio, and Firebase. Start survey

Khuôn khổ Truy cập Kho lưu trữ

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 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:

  • Trình cung cấp tài liệu—Một trình cung cấp nội dung cho phép một dịch vụ lưu trữ (chẳng hạn như Google Drive) phát hiện các tệp mà nó quản lý. Trình cung cấp tài liệu được triển khai thành một lớp con của lớp DocumentsProvider. Sơ đồ tài liệu-trình cung cấp sẽ được dựa trên một phân cấp tệp truyền thống, cho dù cách thức trình cung cấp tài liệu của bạn trực tiếp lưu trữ dữ liệu là hoàn toàn do bạn. Nền tảng Android bao gồm một vài trình cung cấp tài liệu tích hợp, chẳng hạn như Downloads, Images, và Videos.
  • Ứng dụng máy khách—Một ứng dụng tùy chỉnh có chức năng gọi ra ý định ACTION_OPEN_DOCUMENT và/hoặc ACTION_CREATE_DOCUMENT và nhận các tệp được trả về bởi trình cung cấp tài liệu.
  • Bộ chọn—Một UI hệ thống cho phép người dùng truy cập tài liệu từ tất cả trình cung cấp tài liệu mà thỏa mãn các tiêu chí tìm kiếm của ứng dụng máy khách.

Một số tính năng được SAF cung cấp bao gồm:

  • Cho phép người dùng duyệt nội dung từ tất cả trình cung cấp tài liệu, không chỉ một ứng dụng duy nhất.
  • Giúp ứng dụng của bạn có thể có quyền truy cập lâu dài, cố định vào các tài liệu được sở hữu bởi một trình cung cấp tài liệu. Thông qua truy cập này, người dùng có thể thêm, chỉnh sửa, lưu và xóa tệp trên trình cung cấp.
  • Hỗ trợ nhiều tài khoản người dùng và các phần gốc tạm thời chẳng hạn như trình cung cấp bộ nhớ USB, nó chỉ xuất hiện nếu ổ đĩa được cắm vào.

Tổng quan

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 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:

data model

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:

  • Mỗi một trình cung cấp tài liệu sẽ báo cáo một hoặc nhiều "phần gốc" là điểm bắt đầu khám phá cây tài liệu. Mỗi phần gốc có một COLUMN_ROOT_ID duy nhất, và nó trỏ đến một tài liệu (thư mục) biểu diễn nội dung bên dưới phần gốc đó. Phần gốc có thể linh hoạt theo thiết kế để hỗ trợ các trường hợp sử dụng như nhiều tài khoản, thiết bị lưu trữ USB tạm thời, hoặc đăng nhập/đăng xuất người dùng.
  • Dưới mỗi phần gốc là một tài liệu đơn lẻ. Tài liệu đó sẽ trỏ tới 1 đến N tài liệu, mỗi tài liệu lại có thể trỏ tới 1 đến N tài liệu khác.
  • Mỗi bộ nhớ phụ trợ phủ bề mặt các tệp và thư mục riêng lẻ bằng cách tham chiếu chúng bằng một COLUMN_DOCUMENT_ID duy nhất. ID của tài liệu phải là duy nhất và không thay đổi sau khi được phát hành, do chúng được sử dụng để cấp URI không thay đổi giữa các lần khởi động lại thiết bị.
  • Tài liệu có thể là một tệp mở được (có một kiểu MIME cụ thể), hoặc một thư mục chứa các tài liệu bổ sung (có kiểu MIME MIME_TYPE_DIR).
  • Mỗi tài liệu có thể có các khả năng khác nhau như được mô tả bởi COLUMN_FLAGS. Ví dụ, FLAG_SUPPORTS_WRITE, FLAG_SUPPORTS_DELETE, và FLAG_SUPPORTS_THUMBNAIL. COLUMN_DOCUMENT_ID cũng có thể có trong nhiều thư mục.

Dòng Điều khiển

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 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ữ:

app

Hình 2. Dòng Khuôn khổ Truy cập Kho lưu trữ

Lưu ý điều sau đây:

  • Trong SAF, trình cung cấp và máy khách không tương tác trực tiếp với nhau. Một máy khách yêu cầu quyền để tương tác với tệp (cụ thể là quyền đọc, chỉnh sửa, tạo hoặc xóa tệp).
  • Tương tác bắt đầu khi một ứng dụng (trong ví dụ này này một ứng dụng ảnh) thể hiện ý định ACTION_OPEN_DOCUMENT hoặc ACTION_CREATE_DOCUMENT. Ý định có thể bao gồm các bộ lọc để cụ thể hơn các tiêu chí—ví dụ, "cấp cho tôi tất cả tệp mở được có kiểu MIME là 'image'."
  • Sau khi ý định thể hiện, bộ chọn của hệ thống sẽ đi đến từng trình cung cấp được đăng ký và hiển thị cho người dùng xem các phần gốc nội dung khớp với tiêu chí.
  • Bộ chọn cấp cho người dùng một giao diện tiêu chuẩn để truy cập tài liệu, mặc dù các trình cung cấp tài liệu liên quan có thể rất khác nhau. Ví dụ, hình 2 minh họa một trình cung cấp Google Drive, một trình cung cấp USB, và một trình cung cấp đám mâ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:

picker

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.

picker

Hình 4. Hình ảnh

Ghi một Ứng dụng Máy khách

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ư ACTION_PICK hay 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 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ợ.

ACTION_OPEN_DOCUMENT không nhằm mục đích thay thế cho 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:

  • Sử dụng ACTION_GET_CONTENT nếu bạn muốn ứng dụng của mình chỉ đơn thuần đọc/nhập dữ liệu. Bằng cách này, ứng dụng nhập một bản sao dữ liệu, chẳng hạn như một tệp hình ảnh.
  • Sử dụng ACTION_OPEN_DOCUMENT nếu bạn muốn ứng dụng của mình có quyền truy cập lâu dài, cố định vào các tài liệu được sở hữu bởi một trình cung cấp tài liệu. Ví dụ như trường hợp một ứng dụng chỉnh sửa ảnh cho phép người dùng chỉnh sửa các hình ảnh được lưu trữ trong một trình cung cấp tài liệu.

Phần này mô tả cách ghi các ứng dụng máy khách dựa trên ACTION_OPEN_DOCUMENT và các ý định ACTION_CREATE_DOCUMENT.

Đoạn mã HTML sau sử dụng 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:

  • Khi ứng dụng thể hiện ý định ACTION_OPEN_DOCUMENT , nó sẽ khởi chạy một bộ chọn để hiển thị tất cả trình cung cấp tài liệu khớp với tiêu chí.
  • Thêm thể loại CATEGORY_OPENABLE vào ý định sẽ lọc kết quả để chỉ hiển thị những tài liệu có thể mở được, chẳng hạn như tệp hình ảnh.
  • Câu lệnh intent.setType("image/*") sẽ lọc thêm để chỉ hiển thị những tài liệu có kiểu dữ liệu MIME hình ảnh.

Kết quả Tiến trình

Sau khi người dùng chọn một tài liệu trong bộ chọn, onActivityResult() sẽ được gọi. URI tro tới tài liệu được chọn sẽ nằm trong tham số resultData . Trích xuất UI bằng cách sử dụng 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);
        }
    }
}

Kiểm tra siêu dữ liệu tài liệu

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();
    }
}

Mở một tài liệu

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.

Bitmap

Sau đây là một ví dụ về cách bạn có thể mở một 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 AsyncTask. Sau khi mở bitmap, bạn có thể hiển thị nó trong một ImageView.

Nhận một InputStream

Sau đây là một ví dụ về cách mà bạn có thể nhận một 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();
}

Tạo một tài liệu mới

Ứ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 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 onActivityResult(), sao cho bạn có thể tiếp tục ghi nó.

Xóa một tài liệu

Nếu bạn có URI cho một tài liệu và Document.COLUMN_FLAGS của tài liệu chứa SUPPORTS_DELETE, bạn có thể xóa tài liệu đó. Ví dụ:

DocumentsContract.deleteDocument(getContentResolver(), uri);

Chỉnh sửa một tài liệu

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 ACTION_OPEN_DOCUMENT và sử dụng thể loại 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ừ 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 FileOutputStream từ 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();
    }
}

Cố định các quyền

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 getContentResolver().takePersistableUriPermission() để kiểm tra dữ liệu mới nhất.

Ghi một Trình cung cấp Tài liệu Tùy chỉnh

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.

Bản kê khai

Để 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:

  • Một mục tiêu API mức 19 hoặc cao hơn.
  • Một phần tử <provider> khai báo trình cung cấp lưu trữ tùy chỉnh của bạn.
  • Tên của trình cung cấp của bạn, là tên lớp của nó, bao gồm tên gói. Ví dụ: com.example.android.storageprovider.MyCloudProvider.
  • Tên thẩm quyền của bạn, tức là tên gói của bạn (trong ví dụ này là com.example.android.storageprovider) cộng với kiểu của trình cung cấp nội dung (documents). Ví dụ, com.example.android.storageprovider.documents.
  • Thuộc tính 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ó.
  • Thuộc tính 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.
  • Quyền MANAGE_DOCUMENTS. Theo mặc định, một trình cung cấp sẽ có sẵn đối với mọi người. Việc thêm quyền này sẽ hạn chế trình cung cấp của bạn vào hệ thống. Hạn chế này có ý nghĩa quan trọng đối với vấn đề bảo mật.
  • Thuộc tính android:enabled được đặt thành một giá trị boolean được định nghĩa trong một tệp tài nguyên. Mục đích của thuộc tính này là để vô hiệu hóa trình cung cấp trên các thiết bị chạy phiên bản Android 4.3 hoặc thấp hơn. Ví dụ, android:enabled="@bool/atLeastKitKat". Bên cạnh việc nêu thuộc tính này trong bản kê khai, bạn cần làm như sau:
    • Trong tệp tài nguyên bool.xml của bạn bên dưới res/values/, hãy thêm dòng sau:
      <bool name="atLeastKitKat">false</bool>
    • Trong tệp tài nguyên bool.xml của bạn bên dưới res/values-v19/, hãy thêm dòng sau:
      <bool name="atLeastKitKat">true</bool>
  • Một bộ lọc ý định chứa hành động android.content.action.DOCUMENTS_PROVIDER, sao cho trình cung cấp của bạn xuất hiện trong bộ chọn khi hệ thống tìm kiếm trình cung cấp.

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>

Hỗ trợ các thiết bị chạy phiên bản Android 4.3 và thấp hơn

Ý định 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ợ 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 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à 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 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:

  1. Trong tệp tài nguyên bool.xml của bạn bên dưới res/values/, hãy thêm dòng sau:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. Trong tệp tài nguyên bool.xml của bạn bên dưới res/values-v19/, hãy thêm dòng sau:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. Thêm một bí danh hoạt động để vô hiệu hóa bộ lọc ý định ACTION_GET_CONTENT đối với các phiên bản 4.4 (API mức 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 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 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,};

Phân lớp con DocumentsProvider

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 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 DocumentsProvider để biết chi tiết.

Triển khai queryRoots

Việc bạn triển khai queryRoots() phải trả về một 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 DocumentsContract.Root.

Trong đoạn mã HTML sau, tham số 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;
}

Triển khai queryChildDocuments

Việc bạn triển khai queryChildDocuments() phải trả về một 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 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;
}

Triển khai queryDocument

Việc bạn triển khai queryDocument() phải trả về một 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 DocumentsContract.Document.

Phương pháp queryDocument() trả về cùng thông tin đã được chuyển trong 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;
}

Triển khai openDocument

Bạn phải triển khai openDocument() để trả về một ParcelFileDescriptor biểu diễn tệp được chỉ định. Các ứng dụng khác có thể sử dụng 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 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);
    }
}

Bảo mật

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 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 getContentResolver().notifyChange(). Bạn còn nhớ 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 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);
}