Tạo trình cung cấp tài liệu tuỳ chỉnh

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 con DocumentsProvider, 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:

  1. Trong tệp tài nguyên bool.xml của bạn trong res/values/, hãy thêm dòng này:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. Trong tệp tài nguyên bool.xml của bạn trong res/values-v19/, hãy thêm dòng này:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. 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:

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()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&lt;&gt;();

    // Iterate over the list of supported mime types to find a match.
    for (int i=0; i &lt; 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:

Để biết thêm thông tin liên quan, hãy tham khảo: