Tạo trình cung cấp tài liệu tuỳ 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ư dịch vụ lưu vào đám mây), bạn có thể cung cấp các tệp của mình thông qua Khung truy cập bộ nhớ (SAF) bằng cách viết một trình cung cấp tài liệu tuỳ chỉnh. Trang này mô tả cách tạo trình cung cấp tài liệu tuỳ chỉnh.

Để biết thêm thông tin về cách hoạt động của Khung truy cập bộ nhớ, hãy xem Tổng quan về Khung truy cập bộ nhớ.

Tệp kê khai

Để triển khai trình cung cấp tài liệu tuỳ chỉnh, hãy thêm đoạn mã sau vào tệp tệp kê khai:

  • Mục tiêu thuộc API cấp 19 trở lên.
  • Một phần tử <provider> khai báo dung lượng lưu trữ tuỳ chỉnh của bạn Google Cloud.
  • Thuộc tính android:name được đặt thành tên của thuộc tính Lớp con DocumentsProvider, là tên lớp, bao gồm cả tên gói:

    com.example.android.storageprovider.MyCloudProvider.

  • Thuộc tính android:authority, là tên gói của bạn (trong ví dụ này, com.example.android.storageprovider) cùng với loại trình cung cấp nội dung (documents).
  • Thuộc tính android:exported được đặt thành "true". Bạn phải xuất nhà cung cấp của mình để các ứng dụng khác có thể thấy thông tin đó.
  • Thuộc tính android:grantUriPermissions được đặt thành "true" Chế độ cài đặt này cho phép hệ thống cấp quyền truy cập cho các ứng dụng khác vào nội dung trong nhà cung cấp của bạn. Để thảo luận về cách các ứng dụng khác này có thể duy trì quyền truy cập của họ vào nội dung từ nhà cung cấp của bạn, xem Duy trì quyền truy cập.
  • Quyền MANAGE_DOCUMENTS. Theo mặc định, sẽ có nhà cung cấp cho mọi người. Khi thêm quyền này, nhà cung cấp của bạn sẽ bị hạn chế trong hệ thống. Quy định hạn chế này rất quan trọng vì tính bảo mật.
  • Bộ lọc ý định bao gồm Thao tác android.content.action.DOCUMENTS_PROVIDER để nhà cung cấp của bạn sẽ xuất hiện trong bộ chọn khi hệ thống tìm kiếm nhà cung cấp.

Dưới đây là phần trích dẫn từ tệp kê khai mẫu bao gồm trình cung cấp:

<manifest... >
    ...
    <uses-sdk
        android:minSdkVersion="19"
        android:targetSdkVersion="19" />
        ....
        <provider
            android:name="com.example.android.storageprovider.MyCloudProvider"
            android:authorities="com.example.android.storageprovider.documents"
            android:grantUriPermissions="true"
            android:exported="true"
            android:permission="android.permission.MANAGE_DOCUMENTS">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>
    </application>

</manifest>

Hỗ trợ thiết bị chạy Android 4.3 trở xuống

Chiến lược phát hành đĩa đơn Chỉ có ý định ACTION_OPEN_DOCUMENT trên các thiết bị chạy Android 4.4 trở lên. Nếu bạn muốn ứng dụng của mình hỗ trợ ACTION_GET_CONTENT tương thích với các thiết bị chạy Android 4.3 trở xuống, bạn nên tắt bộ lọc ý định ACTION_GET_CONTENT trong tệp kê khai của bạn cho các thiết bị chạy Android 4.4 trở lên. Đáp nhà cung cấp tài liệu và ACTION_GET_CONTENT cần được cân nhắc loại trừ lẫn nhau. Nếu bạn hỗ trợ cả hai cùng lúc, ứng dụng của bạn xuất hiện hai lần trong giao diện người dùng bộ chọn hệ thống, cung cấp hai cách truy cập dữ liệu bạn đã lưu trữ. Điều này gây nhầm lẫn cho người dùng.

Sau đây là cách khuyến nghị để vô hiệu hoá Bộ lọc ý định ACTION_GET_CONTENT cho thiết bị chạy Android phiên bản 4.4 trở lên:

  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 một hoạt động biệt hiệu để tắt ý định ACTION_GET_CONTENT bộ lọc cho phiên bản 4.4 (API cấp 19) trở lên. Ví dụ:
    <!-- This activity alias is added so that GET_CONTENT intent-filter
         can be disabled for builds on API level 19 and higher. -->
    <activity-alias android:name="com.android.example.app.MyPicker"
            android:targetActivity="com.android.example.app.MyActivity"
            ...
            android:enabled="@bool/atMostJellyBeanMR2">
        <intent-filter>
            <action android:name="android.intent.action.GET_CONTENT" />
            <category android:name="android.intent.category.OPENABLE" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:mimeType="image/*" />
            <data android:mimeType="video/*" />
        </intent-filter>
    </activity-alias>
    

Hợp đồng

Thông thường, khi bạn viết một trình cung cấp nội dung tuỳ chỉnh, một trong các nhiệm vụ là triển khai các lớp hợp đồng, như được mô tả trong Hướng dẫn cho nhà phát triển trình cung cấp nội dung. Lớp hợp đồng là một lớp public final chứa định nghĩa hằng số cho URI, tên cột, loại MIME và siêu dữ liệu khác liên quan đến nhà cung cấp. SAF cung cấp các lớp hợp đồng này cho bạn, vì vậy, bạn không cần phải viết sở hữu:

Ví dụ: đây là những cột mà bạn có thể trả về trong con trỏ khi trình cung cấp tài liệu của bạn được truy vấn về tài liệu hoặc thư mục gốc:

Kotlin

private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
        DocumentsContract.Root.COLUMN_ROOT_ID,
        DocumentsContract.Root.COLUMN_MIME_TYPES,
        DocumentsContract.Root.COLUMN_FLAGS,
        DocumentsContract.Root.COLUMN_ICON,
        DocumentsContract.Root.COLUMN_TITLE,
        DocumentsContract.Root.COLUMN_SUMMARY,
        DocumentsContract.Root.COLUMN_DOCUMENT_ID,
        DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
)
private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf(
        DocumentsContract.Document.COLUMN_DOCUMENT_ID,
        DocumentsContract.Document.COLUMN_MIME_TYPE,
        DocumentsContract.Document.COLUMN_DISPLAY_NAME,
        DocumentsContract.Document.COLUMN_LAST_MODIFIED,
        DocumentsContract.Document.COLUMN_FLAGS,
        DocumentsContract.Document.COLUMN_SIZE
)

Java

private static final String[] DEFAULT_ROOT_PROJECTION =
        new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES,
        Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
        Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
        Root.COLUMN_AVAILABLE_BYTES,};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new
        String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
        Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
        Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};

Con trỏ của bạn cho thư mục gốc cần bao gồm một số cột bắt buộc nhất định. Các cột này bao gồm:

Con trỏ cho tài liệu cần bao gồm các cột bắt buộc sau:

Tạo một lớp con của DocumentsProvider

Bước tiếp theo trong việc viết trình cung cấp tài liệu tuỳ chỉnh là tạo lớp con cho lớp trừu tượng DocumentsProvider. Ít nhất, bạn phải triển khai các phương thức sau:

Đây là những phương pháp duy nhất mà bạn bắt buộc phải triển khai, nhưng có nhiều thứ khác mà bạn có thể muốn. Xem DocumentsProvider để biết thông tin chi tiết.

Xác định một căn bậc

Việc triển khai queryRoots() của bạn cần trả về Cursor trỏ đến tất cả các thư mục gốc của trình cung cấp tài liệu, sử dụng các cột được xác định trong DocumentsContract.Root.

Trong đoạn mã sau, tham số projection đại diện cho các trường cụ thể mà phương thức gọi muốn nhận lại. Đoạn mã tạo một con trỏ mới và thêm một hàng vào đó—một thư mục gốc, một thư mục cấp cao nhất, chẳng hạn như Tệp đã tải xuống hoặc Hình ảnh. Hầu hết các trình cung cấp chỉ có một gốc. Bạn có thể có nhiều tài khoản, ví dụ: trong trường hợp có nhiều tài khoản người dùng. Trong trường hợp đó, chỉ cần thêm hàng thứ hai vào con trỏ.

Kotlin

override fun queryRoots(projection: Array<out String>?): Cursor {
    // Use a MatrixCursor to build a cursor
    // with either the requested fields, or the default
    // projection if "projection" is null.
    val result = MatrixCursor(resolveRootProjection(projection))

    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result
    }

    // It's possible to have multiple roots (e.g. for multiple accounts in the
    // same app) -- just add multiple cursor rows.
    result.newRow().apply {
        add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT)

        // You can provide an optional summary, which helps distinguish roots
        // with the same title. You can also use this field for displaying an
        // user account name.
        add(DocumentsContract.Root.COLUMN_SUMMARY, context.getString(R.string.root_summary))

        // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
        // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
        // recently used documents will show up in the "Recents" category.
        // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
        // shares.
        add(
            DocumentsContract.Root.COLUMN_FLAGS,
            DocumentsContract.Root.FLAG_SUPPORTS_CREATE or
                DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or
                DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
        )

        // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
        add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.title))

        // This document id cannot change after it's shared.
        add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir))

        // The child MIME types are used to filter the roots and only present to the
        // user those roots that contain the desired type somewhere in their file hierarchy.
        add(DocumentsContract.Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir))
        add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDir.freeSpace)
        add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher)
    }

    return result
}

Java

@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {

    // Use a MatrixCursor to build a cursor
    // with either the requested fields, or the default
    // projection if "projection" is null.
    final MatrixCursor result =
            new MatrixCursor(resolveRootProjection(projection));

    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
    }

    // It's possible to have multiple roots (e.g. for multiple accounts in the
    // same app) -- just add multiple cursor rows.
    final MatrixCursor.RowBuilder row = result.newRow();
    row.add(Root.COLUMN_ROOT_ID, ROOT);

    // You can provide an optional summary, which helps distinguish roots
    // with the same title. You can also use this field for displaying an
    // user account name.
    row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary));

    // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
    // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
    // recently used documents will show up in the "Recents" category.
    // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
    // shares.
    row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
            Root.FLAG_SUPPORTS_RECENTS |
            Root.FLAG_SUPPORTS_SEARCH);

    // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
    row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title));

    // This document id cannot change after it's shared.
    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir));

    // The child MIME types are used to filter the roots and only present to the
    // user those roots that contain the desired type somewhere in their file hierarchy.
    row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir));
    row.add(Root.COLUMN_AVAILABLE_BYTES, baseDir.getFreeSpace());
    row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);

    return result;
}

Nếu trình cung cấp tài liệu của bạn kết nối với một tập hợp gốc động—ví dụ: với USB thiết bị có thể bị ngắt kết nối hoặc tài khoản mà người dùng có thể đăng xuất — bạn có thể cập nhật giao diện người dùng của tài liệu để luôn đồng bộ với những thay đổi đó bằng cách sử dụng ContentResolver.notifyChange(), như minh hoạ trong đoạn mã sau đây.

Kotlin

val rootsUri: Uri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY)
context.contentResolver.notifyChange(rootsUri, null)

Java

Uri rootsUri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY);
context.getContentResolver().notifyChange(rootsUri, null);

Liệt kê tài liệu trong nhà cung cấp

Việc triển khai của bạn đối với queryChildDocuments() phải trả về Cursor trỏ đến tất cả tệp trong thư mục đã chỉ định, sử dụng các cột được xác định trong DocumentsContract.Document.

Phương thức này được gọi khi người dùng chọn thư mục gốc của bạn trong giao diện người dùng của bộ chọn. Phương thức này truy xuất phần tử con của ID tài liệu được chỉ định bởi COLUMN_DOCUMENT_ID. Sau đó, hệ thống sẽ gọi phương thức này bất cứ khi nào người dùng chọn một trong nhà cung cấp tài liệu của bạn.

Đoạn mã này tạo một con trỏ mới với các cột được yêu cầu, sau đó thêm thông tin về mọi phần tử con trực tiếp trong thư mục mẹ vào con trỏ. Phần tử con có thể là một hình ảnh, một thư mục khác – bất kỳ tệp nào:

Kotlin

override fun queryChildDocuments(
        parentDocumentId: String?,
        projection: Array<out String>?,
        sortOrder: String?
): Cursor {
    return MatrixCursor(resolveDocumentProjection(projection)).apply {
        val parent: File = getFileForDocId(parentDocumentId)
        parent.listFiles()
                .forEach { file ->
                    includeFile(this, null, file)
                }
    }
}

Java

@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
                              String sortOrder) throws FileNotFoundException {

    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    final File parent = getFileForDocId(parentDocumentId);
    for (File file : parent.listFiles()) {
        // Adds the file's display name, MIME type, size, and so on.
        includeFile(result, null, file);
    }
    return result;
}

Nhận thông tin tài liệu

Việc triển khai của bạn đối với queryDocument() phải trả về Cursor trỏ đến tệp được chỉ định, bằng cách sử dụng các cột được xác định trong DocumentsContract.Document.

queryDocument() trả về cùng một thông tin đã được chuyển vào queryChildDocuments(), nhưng đối với một tệp cụ thể:

Kotlin

override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor {
    // Create a cursor with the requested projection, or the default projection.
    return MatrixCursor(resolveDocumentProjection(projection)).apply {
        includeFile(this, documentId, null)
    }
}

Java

@Override
public Cursor queryDocument(String documentId, String[] projection) throws
        FileNotFoundException {

    // Create a cursor with the requested projection, or the default projection.
    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    includeFile(result, documentId, null);
    return result;
}

Nhà cung cấp tài liệu của bạn cũng có thể cung cấp hình thu nhỏ cho một tài liệu bằng cách ghi đè DocumentsProvider.openDocumentThumbnail() và thêm phương thức FLAG_SUPPORTS_THUMBNAIL gắn cờ cho các tệp được hỗ trợ. Đoạn mã sau đây cung cấp ví dụ về cách triển khai DocumentsProvider.openDocumentThumbnail().

Kotlin

override fun openDocumentThumbnail(
        documentId: String?,
        sizeHint: Point?,
        signal: CancellationSignal?
): AssetFileDescriptor {
    val file = getThumbnailFileForDocId(documentId)
    val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
    return AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH)
}

Java

@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint,
                                                     CancellationSignal signal)
        throws FileNotFoundException {

    final File file = getThumbnailFileForDocId(documentId);
    final ParcelFileDescriptor pfd =
        ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
}

Thận trọng: Trình cung cấp tài liệu không được trả về hình thu nhỏ nhiều hơn gấp đôi kích thước do tham số sizeHint chỉ định.

Mở tài liệu

Bạn phải triển khai openDocument() để trả về một ParcelFileDescriptor đại diện cho tệp được chỉ định. Các ứng dụng khác có thể dùng ParcelFileDescriptor được trả về để truyền dữ liệu. Hệ thống gọi phương thức này sau khi người dùng chọn một tệp, và ứng dụng khách yêu cầu quyền truy cập vào bằng cách gọi openFileDescriptor(). Ví dụ:

Kotlin

override fun openDocument(
        documentId: String,
        mode: String,
        signal: CancellationSignal
): ParcelFileDescriptor {
    Log.v(TAG, "openDocument, mode: $mode")
    // It's OK to do network operations in this method to download the document,
    // as long as you periodically check the CancellationSignal. If you have an
    // extremely large file to transfer from the network, a better solution may
    // be pipes or sockets (see ParcelFileDescriptor for helper methods).

    val file: File = getFileForDocId(documentId)
    val accessMode: Int = ParcelFileDescriptor.parseMode(mode)

    val isWrite: Boolean = mode.contains("w")
    return if (isWrite) {
        val handler = Handler(context.mainLooper)
        // Attach a close listener if the document is opened in write mode.
        try {
            ParcelFileDescriptor.open(file, accessMode, handler) {
                // Update the file with the cloud server. The client is done writing.
                Log.i(TAG, "A file with id $documentId has been closed! Time to update the server.")
            }
        } catch (e: IOException) {
            throw FileNotFoundException(
                    "Failed to open document with id $documentId and mode $mode"
            )
        }
    } else {
        ParcelFileDescriptor.open(file, accessMode)
    }
}

Java

@Override
public ParcelFileDescriptor openDocument(final String documentId,
                                         final String mode,
                                         CancellationSignal signal) throws
        FileNotFoundException {
    Log.v(TAG, "openDocument, mode: " + mode);
    // It's OK to do network operations in this method to download the document,
    // as long as you periodically check the CancellationSignal. If you have an
    // extremely large file to transfer from the network, a better solution may
    // be pipes or sockets (see ParcelFileDescriptor for helper methods).

    final File file = getFileForDocId(documentId);
    final int accessMode = ParcelFileDescriptor.parseMode(mode);

    final boolean isWrite = (mode.indexOf('w') != -1);
    if(isWrite) {
        // Attach a close listener if the document is opened in write mode.
        try {
            Handler handler = new Handler(getContext().getMainLooper());
            return ParcelFileDescriptor.open(file, accessMode, handler,
                        new ParcelFileDescriptor.OnCloseListener() {
                @Override
                public void onClose(IOException e) {

                    // Update the file with the cloud server. The client is done
                    // writing.
                    Log.i(TAG, "A file with id " +
                    documentId + " has been closed! Time to " +
                    "update the server.");
                }

            });
        } catch (IOException e) {
            throw new FileNotFoundException("Failed to open document with id"
            + documentId + " and mode " + mode);
        }
    } else {
        return ParcelFileDescriptor.open(file, accessMode);
    }
}

Nếu nhà cung cấp tài liệu của bạn truyền trực tuyến các tệp hoặc xử lý các công việc phức tạp cấu trúc dữ liệu, hãy cân nhắc việc triển khai createReliablePipe() hoặc createReliableSocketPair(). Các phương thức đó cho phép bạn tạo một cặp Đối tượng ParcelFileDescriptor mà bạn có thể trả về một đối tượng rồi gửi tệp còn lại qua một ParcelFileDescriptor.AutoCloseOutputStream hoặc ParcelFileDescriptor.AutoCloseInputStream.

Hỗ trợ nội dung tìm kiếm và tài liệu gần đây

Bạn có thể cung cấp danh sách các tài liệu đã sửa đổi gần đây trong thư mục gốc của trình cung cấp tài liệu bằng cách ghi đè Phương thức queryRecentDocuments() và trả về FLAG_SUPPORTS_RECENTS, Đoạn mã sau đây cho thấy ví dụ về cách triển khai queryRecentDocuments().

Kotlin

override fun queryRecentDocuments(rootId: String?, projection: Array<out String>?): Cursor {
    // This example implementation walks a
    // local file structure to find the most recently
    // modified files.  Other implementations might
    // include making a network call to query a
    // server.

    // Create a cursor with the requested projection, or the default projection.
    val result = MatrixCursor(resolveDocumentProjection(projection))

    val parent: File = getFileForDocId(rootId)

    // Create a queue to store the most recent documents,
    // which orders by last modified.
    val lastModifiedFiles = PriorityQueue(
            5,
            Comparator<File> { i, j ->
                Long.compare(i.lastModified(), j.lastModified())
            }
    )

    // Iterate through all files and directories
    // in the file structure under the root.  If
    // the file is more recent than the least
    // recently modified, add it to the queue,
    // limiting the number of results.
    val pending : MutableList<File> = mutableListOf()

    // Start by adding the parent to the list of files to be processed
    pending.add(parent)

    // Do while we still have unexamined files
    while (pending.isNotEmpty()) {
        // Take a file from the list of unprocessed files
        val file: File = pending.removeAt(0)
        if (file.isDirectory) {
            // If it's a directory, add all its children to the unprocessed list
            pending += file.listFiles()
        } else {
            // If it's a file, add it to the ordered queue.
            lastModifiedFiles.add(file)
        }
    }

    // Add the most recent files to the cursor,
    // not exceeding the max number of results.
    for (i in 0 until Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size)) {
        val file: File = lastModifiedFiles.remove()
        includeFile(result, null, file)
    }
    return result
}

Java

@Override
public Cursor queryRecentDocuments(String rootId, String[] projection)
        throws FileNotFoundException {

    // This example implementation walks a
    // local file structure to find the most recently
    // modified files.  Other implementations might
    // include making a network call to query a
    // server.

    // Create a cursor with the requested projection, or the default projection.
    final MatrixCursor result =
        new MatrixCursor(resolveDocumentProjection(projection));

    final File parent = getFileForDocId(rootId);

    // Create a queue to store the most recent documents,
    // which orders by last modified.
    PriorityQueue lastModifiedFiles =
        new PriorityQueue(5, new Comparator() {

        public int compare(File i, File j) {
            return Long.compare(i.lastModified(), j.lastModified());
        }
    });

    // Iterate through all files and directories
    // in the file structure under the root.  If
    // the file is more recent than the least
    // recently modified, add it to the queue,
    // limiting the number of results.
    final LinkedList pending = new LinkedList();

    // Start by adding the parent to the list of files to be processed
    pending.add(parent);

    // Do while we still have unexamined files
    while (!pending.isEmpty()) {
        // Take a file from the list of unprocessed files
        final File file = pending.removeFirst();
        if (file.isDirectory()) {
            // If it's a directory, add all its children to the unprocessed list
            Collections.addAll(pending, file.listFiles());
        } else {
            // If it's a file, add it to the ordered queue.
            lastModifiedFiles.add(file);
        }
    }

    // Add the most recent files to the cursor,
    // not exceeding the max number of results.
    for (int i = 0; i < Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size()); i++) {
        final File file = lastModifiedFiles.remove();
        includeFile(result, null, file);
    }
    return result;
}

Bạn có thể nhận mã hoàn chỉnh cho đoạn mã ở trên bằng cách tải xuống StorageProvider mã mẫu.

Hỗ trợ tạo tài liệu

Bạn có thể cho phép các ứng dụng khách tạo tệp trong nhà cung cấp tài liệu của mình. Nếu một ứng dụng khách gửi một ACTION_CREATE_DOCUMENT ý định của bạn, trình cung cấp tài liệu của bạn có thể cho phép ứng dụng khách đó tạo tài liệu mới trong trình cung cấp tài liệu.

Để hỗ trợ tạo tài liệu, thư mục gốc của bạn cần có Cờ FLAG_SUPPORTS_CREATE. Thư mục cho phép tạo tệp mới trong thư mục cần có FLAG_DIR_SUPPORTS_CREATE cờ.

Nhà cung cấp tài liệu của bạn cũng cần triển khai createDocument(). Khi người dùng chọn một thư mục trong trình cung cấp tài liệu để lưu tệp mới, trình cung cấp tài liệu sẽ nhận được cuộc gọi đến createDocument(). Trong quá trình triển khai createDocument(), bạn sẽ trả về một phương thức mới COLUMN_DOCUMENT_ID cho . Sau đó, ứng dụng khách có thể dùng mã nhận dạng đó để lấy tên người dùng cho tệp đó và cuối cùng là gọi openDocument() để ghi vào tệp mới.

Đoạn mã sau đây minh hoạ cách tạo tệp mới trong một trình cung cấp tài liệu.

Kotlin

override fun createDocument(documentId: String?, mimeType: String?, displayName: String?): String {
    val parent: File = getFileForDocId(documentId)
    val file: File = try {
        File(parent.path, displayName).apply {
            createNewFile()
            setWritable(true)
            setReadable(true)
        }
    } catch (e: IOException) {
        throw FileNotFoundException(
                "Failed to create document with name $displayName and documentId $documentId"
        )
    }

    return getDocIdForFile(file)
}

Java

@Override
public String createDocument(String documentId, String mimeType, String displayName)
        throws FileNotFoundException {

    File parent = getFileForDocId(documentId);
    File file = new File(parent.getPath(), displayName);
    try {
        file.createNewFile();
        file.setWritable(true);
        file.setReadable(true);
    } catch (IOException e) {
        throw new FileNotFoundException("Failed to create document with name " +
                displayName +" and documentId " + documentId);
    }
    return getDocIdForFile(file);
}

Bạn có thể nhận mã hoàn chỉnh cho đoạn mã ở trên bằng cách tải xuống StorageProvider mã mẫu.

Hỗ trợ các tính năng quản lý tài liệu

Ngoài mở, tạo và xem tệp, nhà cung cấp tài liệu của bạn cũng có thể cho phép ứng dụng khách đổi tên, sao chép, di chuyển và xoá tệp. Để thêm chức năng quản lý tài liệu vào nhà cung cấp tài liệu của bạn, hãy thêm một cờ vào COLUMN_FLAGS cột để cho biết chức năng được hỗ trợ. Bạn cũng cần triển khai phương thức tương ứng của DocumentsProvider .

Bảng sau đây cung cấp Cờ COLUMN_FLAGS và phương thức DocumentsProvider mà một tài liệu cần triển khai để hiển thị các tính năng cụ thể.

Tính năng Gắn cờ Phương thức
Xoá tệp FLAG_SUPPORTS_DELETE deleteDocument()
Đổi tên tệp FLAG_SUPPORTS_RENAME renameDocument()
Sao chép tệp vào một thư mục mẹ mới trong trình cung cấp tài liệu FLAG_SUPPORTS_COPY copyDocument()
Di chuyển tệp từ thư mục này sang thư mục khác trong trình cung cấp tài liệu FLAG_SUPPORTS_MOVE moveDocument()
Xoá một tệp khỏi thư mục mẹ của tệp đó FLAG_SUPPORTS_REMOVE removeDocument()

Hỗ trợ tệp ảo và các định dạng tệp thay thế

Tệp ảo, một tính năng ra mắt trong Android 7.0 (API cấp 24), cho phép các trình cung cấp tài liệu để cung cấp quyền xem tệp không có đại diện trực tiếp cho mã byte. Cách cho phép các ứng dụng khác xem tệp ảo: nhà cung cấp tài liệu của bạn cần tạo một tệp có thể mở thay thế đại diện cho các tệp ảo.

Ví dụ: hãy tưởng tượng rằng trình cung cấp tài liệu có chứa một tệp mà các ứng dụng khác không thể mở trực tiếp, về cơ bản là một tệp ảo. Khi một ứng dụng khách gửi ý định ACTION_VIEW không có danh mục CATEGORY_OPENABLE, thì người dùng có thể chọn các tệp ảo này trong trình cung cấp tài liệu để xem. Sau đó, trình cung cấp tài liệu sẽ trả về tệp ảo ở định dạng tệp khác nhưng có thể mở được, chẳng hạn như hình ảnh. Sau đó, ứng dụng có thể mở tệp ảo để người dùng xem.

Để khai báo rằng tài liệu trong trình cung cấp là tài liệu ảo, bạn cần thêm thuộc tính FLAG_VIRTUAL_DOCUMENT gắn cờ vào tệp được trả về bởi queryDocument() . Cờ này cảnh báo các ứng dụng khách rằng tệp không có biểu thị mã byte và không thể mở trực tiếp.

Nếu bạn khai báo rằng một tệp trong trình cung cấp tài liệu là tệp ảo, bạn nên cung cấp mã này trong Loại MIME như hình ảnh hoặc tệp PDF. Trình cung cấp tài liệu khai báo loại MIME thay thế mà nó hỗ trợ xem tệp ảo bằng cách ghi đè getDocumentStreamTypes() . Khi ứng dụng khách gọi hàm getStreamTypes(android.net.Uri, java.lang.String) thì hệ thống sẽ gọi phương thức getDocumentStreamTypes() của trình cung cấp tài liệu. Chiến lược phát hành đĩa đơn getDocumentStreamTypes() sau đó trả về một mảng các loại MIME thay thế mà trình cung cấp tài liệu hỗ trợ tệp.

Sau khi khách hàng xác định nhà cung cấp tài liệu có thể tạo tài liệu trong một tệp có thể xem được thì ứng dụng khách sẽ gọi hàm openTypedAssetFileDescriptor() phương thức này gọi nội bộ openTypedDocument() . Trình cung cấp tài liệu trả về tệp cho ứng dụng khách trong định dạng tệp được yêu cầu.

Đoạn mã sau đây minh hoạ cách triển khai đơn giản của getDocumentStreamTypes()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ụ lưu trữ trên đám mây được bảo vệ bằng mật khẩu và bạn muốn đảm bảo rằng người dùng đã đăng nhập trước khi bạn bắt đầu chia sẻ tệp của họ. Ứng dụng của bạn nên làm gì nếu người dùng chưa đăng nhập? Giải pháp là trả lại bằng 0 trong quá trình triển khai queryRoots(). Tức là con trỏ gốc trống:

Kotlin

override fun queryRoots(projection: Array<out String>): Cursor {
...
    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result
    }

Java

public Cursor queryRoots(String[] projection) throws FileNotFoundException {
...
    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
}

Bước còn lại là gọi getContentResolver().notifyChange(). Bạn có nhớ DocumentsContract không? Chúng tôi sử dụng công cụ này để khiến URI này. Đoạn mã sau đây yêu cầu hệ thống truy vấn các gốc của nhà cung cấp tài liệu bất cứ khi nào trạng thái đăng nhập của người dùng thay đổi. Nếu người dùng không đã đăng nhập, lệnh gọi đến queryRoots() sẽ trả về con trỏ trống, như được hiển thị ở trên. Điều này giúp đảm bảo rằng giấy tờ của nhà cung cấp chỉ có sẵn nếu người dùng đăng nhập vào nhà cung cấp.

Kotlin

private fun onLoginButtonClick() {
    loginOrLogout()
    getContentResolver().notifyChange(
        DocumentsContract.buildRootsUri(AUTHORITY),
        null
    )
}

Java

private void onLoginButtonClick() {
    loginOrLogout();
    getContentResolver().notifyChange(DocumentsContract
            .buildRootsUri(AUTHORITY), null);
}

Đối với mã mẫu liên quan đến trang này, hãy tham khảo:

Đối với những video liên quan đến trang này, hãy tham khảo:

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