Truy cập tài liệu và các tệp khác từ bộ nhớ dùng chung

Trên thiết bị chạy Android 4.4 (API cấp 19) trở lên, ứng dụng của bạn có thể tương tác với nhà cung cấp tài liệu, bao gồm cả ổ đĩa bộ nhớ ngoài và bộ nhớ dựa trên đám mây, sử dụng Khung truy cập bộ nhớ (Storage Access Framework). Khung này cho phép người dùng tương tác với công cụ chọn của hệ thống để chọn nhà cung cấp tài liệu và chọn tài liệu cụ thể cũng như các tệp khác để ứng dụng của bạn tạo, mở hoặc sửa đổi.

Do người dùng tham gia vào việc chọn các tệp hoặc thư mục mà ứng dụng của bạn có thể truy cập nên cơ chế này không yêu cầu cấp bất kỳ quyền hệ thống nào, đồng thời quyền kiểm soát và quyền riêng tư của người dùng được nâng cao. Ngoài ra, các tệp này, được lưu trữ bên ngoài thư mục dành riêng cho ứng dụng và bên ngoài kho lưu trữ nội dung nghe nhìn, vẫn tồn tại trên thiết bị sau khi ứng dụng của bạn bị gỡ cài đặt.

Quá trình sử dụng khung này gồm các bước sau:

  1. Ứng dụng gọi một ý định chứa thao tác liên quan đến bộ nhớ. Thao tác này tương ứng với một trường hợp sử dụng cụ thể mà khung đó sẽ cung cấp.
  2. Người dùng xem công cụ chọn của hệ thống, cho phép họ duyệt qua nhà cung cấp tài liệu và chọn một vị trí hoặc tài liệu nơi diễn ra thao tác liên quan đến bộ nhớ.
  3. Ứng dụng có quyền đọc và ghi vào URI đại diện cho vị trí hoặc tài liệu do người dùng chọn. Bằng cách sử dụng URI này, ứng dụng có thể thực hiện các thao tác trên vị trí đã chọn.

Để hỗ trợ quyền truy cập vào tệp nội dung nghe nhìn trên các thiết bị chạy Android 9 (API cấp 28) trở xuống, hãy khai báo quyền READ_EXTERNAL_STORAGE và đặt maxSdkVersion thành 28.

Hướng dẫn này giải thích các trường hợp sử dụng mà khung hỗ trợ làm việc với các tệp và tài liệu khác. Hướng dẫn này cũng giải thích cách thực hiện các thao tác trên vị trí do người dùng chọn.

Các trường hợp sử dụng khi truy cập vào các tài liệu và tệp khác

Khung truy cập bộ nhớ hỗ trợ những trường hợp sử dụng sau đây khi truy cập vào các tệp và tài liệu khác.

Tạo tệp mới
Thao tác theo ý định ACTION_CREATE_DOCUMENT cho phép người dùng lưu tệp vào một vị trí cụ thể.
Mở một tài liệu hoặc tệp
Thao tác theo ý định ACTION_OPEN_DOCUMENT cho phép người dùng chọn một tài liệu hoặc tệp cụ thể cần mở.
Cấp quyền truy cập vào nội dung của một thư mục
Thao tác theo ý định ACTION_OPEN_DOCUMENT_TREE, có trên Android 5.0 (API cấp 21) trở lên, cho phép người dùng chọn một thư mục cụ thể, cấp quyền cho ứng dụng của bạn truy cập vào tất cả các tệp và thư mục con trong thư mục đó.

Các mục sau đây cung cấp hướng dẫn về cách định cấu hình từng trường hợp sử dụng.

Tạo tệp mới

Sử dụng thao tác theo ý định ACTION_CREATE_DOCUMENT để tải bộ chọn tệp của hệ thống và cho phép người dùng chọn vị trí ghi nội dung tệp. Quy trình này tương tự như quy trình được sử dụng trong hộp thoại "lưu dưới dạng" mà các hệ điều hành khác sử dụng.

Lưu ý: ACTION_CREATE_DOCUMENT không thể ghi đè tệp hiện có. Nếu ứng dụng của bạn cố gắng lưu một tệp có cùng tên, thì hệ thống sẽ thêm một số trong dấu ngoặc đơn vào cuối tên tệp.

Ví dụ: Nếu ứng dụng của bạn cố gắng lưu một tệp có tên là confirmation.pdf vào thư mục đã chứa tệp có tên đó, thì hệ thống sẽ lưu tệp mới bằng tên là confirmation(1).pdf.

Khi định cấu hình ý định, hãy chỉ định tên và loại MIME của tệp, đồng thời tuỳ ý chỉ định URI của tệp hoặc thư mục mà bộ chọn tệp phải hiển thị khi tải lần đầu bằng cách sử dụng ý định bổ sung EXTRA_INITIAL_URI.

Đoạn mã sau đây cho biết cách tạo và gọi ý định tạo tệp:

Kotlin

// Request code for creating a PDF document.
const val CREATE_FILE = 1

private fun createFile(pickerInitialUri: Uri) {
    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "application/pdf"
        putExtra(Intent.EXTRA_TITLE, "invoice.pdf")

        // Optionally, specify a URI for the directory that should be opened in
        // the system file picker before your app creates the document.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }
    startActivityForResult(intent, CREATE_FILE)
}

Java

// Request code for creating a PDF document.
private static final int CREATE_FILE = 1;

private void createFile(Uri pickerInitialUri) {
    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("application/pdf");
    intent.putExtra(Intent.EXTRA_TITLE, "invoice.pdf");

    // Optionally, specify a URI for the directory that should be opened in
    // the system file picker when your app creates the document.
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);

    startActivityForResult(intent, CREATE_FILE);
}

Mở tệp

Ứng dụng của bạn có thể sử dụng tài liệu làm đơn vị lưu trữ, trong đó người dùng nhập dữ liệu mà họ có thể muốn chia sẻ với các ứng dụng ngang hàng hoặc nhập vào các tài liệu khác. Một số ví dụ: Người dùng mở tài liệu năng suất hoặc mở một cuốn sách được lưu dưới dạng tệp EPUB.

Trong những trường hợp này, hãy cho phép người dùng chọn tệp muốn mở bằng cách gọi ý định ACTION_OPEN_DOCUMENT. Thao tác gọi này sẽ mở ứng dụng bộ chọn tệp của hệ thống. Để chỉ hiển thị các loại tệp mà ứng dụng của bạn hỗ trợ, hãy chỉ định loại MIME. Ngoài ra, bạn cũng có thể tuỳ ý chỉ định URI của tệp mà bộ chọn tệp sẽ hiển thị khi tải lần đầu bằng cách sử dụng ý định bổ sung EXTRA_INITIAL_URI.

Đoạn mã sau đây cho biết cách tạo và gọi ý định mở tài liệu PDF:

Kotlin

// Request code for selecting a PDF document.
const val PICK_PDF_FILE = 2

fun openFile(pickerInitialUri: Uri) {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "application/pdf"

        // Optionally, specify a URI for the file that should appear in the
        // system file picker when it loads.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }

    startActivityForResult(intent, PICK_PDF_FILE)
}

Java

// Request code for selecting a PDF document.
private static final int PICK_PDF_FILE = 2;

private void openFile(Uri pickerInitialUri) {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("application/pdf");

    // Optionally, specify a URI for the file that should appear in the
    // system file picker when it loads.
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);

    startActivityForResult(intent, PICK_PDF_FILE);
}

Hạn chế quyền truy cập

Trên Android 11 (API cấp 30) trở lên, bạn không thể sử dụng thao tác theo ý định ACTION_OPEN_DOCUMENT để yêu cầu người dùng chọn các tệp riêng lẻ từ những thư mục sau:

  • Thư mục Android/data/ và tất cả các thư mục con.
  • Thư mục Android/obb/ và tất cả các thư mục con.

Cấp quyền truy cập vào nội dung của một thư mục

Các ứng dụng quản lý tệp và tạo nội dung nghe nhìn thường quản lý các nhóm tệp trong hệ phân cấp của thư mục. Để cung cấp khả năng này trong ứng dụng của bạn, hãy sử dụng thao tác theo ý định ACTION_OPEN_DOCUMENT_TREE. Thao tác này cho phép người dùng cấp quyền truy cập vào toàn bộ cây thư mục, trừ một số trường hợp ngoại lệ bắt đầu trong Android 11 (API cấp 30). Sau đó, ứng dụng của bạn có thể truy cập mọi tệp trong thư mục đã chọn và bất kỳ thư mục con nào trong đó.

Khi sử dụng ACTION_OPEN_DOCUMENT_TREE, ứng dụng của bạn sẽ chỉ có quyền truy cập vào các tệp trong thư mục mà người dùng chọn. Bạn không có quyền truy cập vào các tệp của những ứng dụng khác nằm bên ngoài thư mục do người dùng chọn này. Quyền truy cập do người dùng kiểm soát này cho phép người dùng chọn chính xác nội dung mà họ có thể chia sẻ thoải mái với ứng dụng của bạn.

Nếu muốn, bạn có thể chỉ định URI của thư mục mà bộ chọn tệp sẽ hiển thị khi tải lần đầu bằng cách sử dụng ý định bổ sung EXTRA_INITIAL_URI.

Đoạn mã sau đây cho biết cách tạo và gọi ý định mở thư mục:

Kotlin

fun openDirectory(pickerInitialUri: Uri) {
    // Choose a directory using the system's file picker.
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
        // Optionally, specify a URI for the directory that should be opened in
        // the system file picker when it loads.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }

    startActivityForResult(intent, your-request-code)
}

Java

public void openDirectory(Uri uriToLoad) {
    // Choose a directory using the system's file picker.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);

    // Optionally, specify a URI for the directory that should be opened in
    // the system file picker when it loads.
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uriToLoad);

    startActivityForResult(intent, your-request-code);
}

Hạn chế quyền truy cập

Trên Android 11 (API cấp 30) trở lên, bạn không thể sử dụng thao tác theo ý định ACTION_OPEN_DOCUMENT_TREE để yêu cầu quyền truy cập vào các thư mục sau:

  • Thư mục gốc của phương tiện bộ nhớ trong.
  • Thư mục gốc của mỗi phương tiện ghi thẻ SD mà nhà sản xuất thiết bị cho là đáng tin cậy, bất kể thẻ đó là loại mô phỏng hay có thể tháo rời. Phương tiện ghi đáng tin cậy là phương tiện ghi mà hầu như lúc nào ứng dụng cũng có thể truy cập thành công.
  • Thư mục Download.

Hơn nữa, trên Android 11 (API cấp 30) trở lên, bạn không được sử dụng thao tác theo ý định ACTION_OPEN_DOCUMENT_TREE để yêu cầu người dùng chọn các tệp riêng lẻ từ những thư mục sau:

  • Thư mục Android/data/ và tất cả các thư mục con.
  • Thư mục Android/obb/ và tất cả các thư mục con.

Thực hiện các thao tác đối với vị trí đã chọn

Sau khi người dùng chọn một tệp hoặc thư mục bằng bộ chọn tệp của hệ thống, bạn có thể truy xuất URI của mục đã chọn bằng mã sau trong onActivityResult():

Kotlin

override fun onActivityResult(
        requestCode: Int, resultCode: Int, resultData: Intent?) {
    if (requestCode == your-request-code
            && resultCode == Activity.RESULT_OK) {
        // The result data contains a URI for the document or directory that
        // the user selected.
        resultData?.data?.also { uri ->
            // Perform operations on the document using its URI.
        }
    }
}

Java

@Override
public void onActivityResult(int requestCode, int resultCode,
        Intent resultData) {
    if (requestCode == your-request-code
            && resultCode == Activity.RESULT_OK) {
        // The result data contains a URI for the document or directory that
        // the user selected.
        Uri uri = null;
        if (resultData != null) {
            uri = resultData.getData();
            // Perform operations on the document using its URI.
        }
    }
}

Bằng cách tham chiếu đến URI của mục đã chọn, ứng dụng của bạn có thể thực hiện một số thao tác với mục đó. Ví dụ: Bạn có thể truy cập vào siêu dữ liệu của mục, chỉnh sửa mục tại chỗ và xoá mục.

Các phần sau đây cho biết cách hoàn tất thao tác trên các tệp mà người dùng chọn.

Xác định những thao tác mà nhà cung cấp hỗ trợ

Các nhà cung cấp nội dung khác nhau cho phép thực hiện các thao tác khác nhau trên tài liệu, chẳng hạn như sao chép tài liệu hoặc xem hình thu nhỏ của tài liệu. Để xác định những thao tác mà một nhà cung cấp cụ thể hỗ trợ, hãy kiểm tra giá trị của Document.COLUMN_FLAGS. Khi đó, giao diện người dùng của ứng dụng chỉ có thể hiển thị các tuỳ chọn được nhà cung cấp hỗ trợ.

Quyền có tác dụng lâu dài

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 quyền URI của tệp đó. Quyền này sẽ có hiệu lực cho đến khi thiết bị của người dùng khởi động lại. Tuy nhiên, giả sử rằng ứng dụng của bạn là ứng dụng chỉnh sửa hình ảnh và bạn muốn người dùng có thể truy cập vào 5 hình ảnh mà họ chỉnh sửa gần đây nhất, ngay trên ứ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 phải đưa người dùng quay lại bộ chọn của hệ thống để tìm tệp.

Để lưu giữ quyền truy cập vào các tệp trong các lần thiết bị khởi động lại và tạo trải nghiệm người dùng tốt hơn, ứng dụng của bạn có thể "lấy" (take) quyền URI lâu dài mà hệ thống cung cấp, như trong đoạn mã sau đây:

Kotlin

val contentResolver = applicationContext.contentResolver

val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
        Intent.FLAG_GRANT_WRITE_URI_PERMISSION
// Check for the freshest data.
contentResolver.takePersistableUriPermission(uri, takeFlags)

Java

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

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

Khi có URI cho một tài liệu, bạn sẽ có quyền truy cập vào siêu dữ liệu của tài liệu đó. Đoạn mã này lấy siêu dữ liệu cho tài liệu do URI chỉ định và ghi lại siêu dữ liệu đó:

Kotlin

val contentResolver = applicationContext.contentResolver

fun dumpImageMetaData(uri: Uri) {

    // The query, because it only applies to a single document, returns only
    // one row. There's no need to filter, sort, or select fields,
    // because we want all fields for one document.
    val cursor: Cursor? = contentResolver.query(
            uri, null, null, null, null, null)

    cursor?.use {
        // moveToFirst() returns false if the cursor has 0 rows. Very handy for
        // "if there's anything to look at, look at it" conditionals.
        if (it.moveToFirst()) {

            // Note it's called "Display Name". This is
            // provider-specific, and might not necessarily be the file name.
            val displayName: String =
                    it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
            Log.i(TAG, "Display Name: $displayName")

            val sizeIndex: Int = it.getColumnIndex(OpenableColumns.SIZE)
            // If the size is unknown, the value stored is null. But because an
            // int can't be null, the behavior is implementation-specific,
            // and 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.
            val size: String = if (!it.isNull(sizeIndex)) {
                // Technically the column stores an int, but cursor.getString()
                // will do the conversion automatically.
                it.getString(sizeIndex)
            } else {
                "Unknown"
            }
            Log.i(TAG, "Size: $size")
        }
    }
}

Java

public void dumpImageMetaData(Uri uri) {

    // The query, because it only applies to a single document, returns only
    // one row. There's no need to filter, sort, or select fields,
    // because 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 because an
            // int can't be null, the behavior is implementation-specific,
            // and 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ở tài liệu

Bằng cách tham chiếu đến URI của tài liệu, bạn có thể mở một tài liệu để xử lý thêm. Phần này trình bày các ví dụ về cách mở bitmap và luồng dữ liệu đầu vào.

Bitmap

Đoạn mã sau đây cho biết cách mở tệp Bitmap dựa trên URI:

Kotlin

val contentResolver = applicationContext.contentResolver

@Throws(IOException::class)
private fun getBitmapFromUri(uri: Uri): Bitmap {
    val parcelFileDescriptor: ParcelFileDescriptor =
            contentResolver.openFileDescriptor(uri, "r")
    val fileDescriptor: FileDescriptor = parcelFileDescriptor.fileDescriptor
    val image: Bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor)
    parcelFileDescriptor.close()
    return image
}

Java

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

Sau khi mở bitmap, bạn có thể hiển thị bitmap trong ImageView.

Luồng dữ liệu đầu vào

Đoạn mã sau đây cho biết cách mở đối tượng InputStream dựa trên URI. Trong đoạn mã này, các dòng của tệp sẽ được đọc thành chuỗi:

Kotlin

val contentResolver = applicationContext.contentResolver

@Throws(IOException::class)
private fun readTextFromUri(uri: Uri): String {
    val stringBuilder = StringBuilder()
    contentResolver.openInputStream(uri)?.use { inputStream ->
        BufferedReader(InputStreamReader(inputStream)).use { reader ->
            var line: String? = reader.readLine()
            while (line != null) {
                stringBuilder.append(line)
                line = reader.readLine()
            }
        }
    }
    return stringBuilder.toString()
}

Java

private String readTextFromUri(Uri uri) throws IOException {
    StringBuilder stringBuilder = new StringBuilder();
    try (InputStream inputStream =
            getContentResolver().openInputStream(uri);
            BufferedReader reader = new BufferedReader(
            new InputStreamReader(Objects.requireNonNull(inputStream)))) {
        String line;
        while ((line = reader.readLine()) != null) {
            stringBuilder.append(line);
        }
    }
    return stringBuilder.toString();
}

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

Bạn có thể sử dụng Khung truy cập bộ nhớ để chỉnh sửa tài liệu văn bản tại chỗ.

Đoạn mã sau đây sẽ ghi đè lên nội dung của tài liệu được trình bày bằng URI nhất định:

Kotlin

val contentResolver = applicationContext.contentResolver

private fun alterDocument(uri: Uri) {
    try {
        contentResolver.openFileDescriptor(uri, "w")?.use {
            FileOutputStream(it.fileDescriptor).use {
                it.write(
                    ("Overwritten at ${System.currentTimeMillis()}\n")
                        .toByteArray()
                )
            }
        }
    } catch (e: FileNotFoundException) {
        e.printStackTrace()
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

Java

private void alterDocument(Uri uri) {
    try {
        ParcelFileDescriptor pfd = getActivity().getContentResolver().
                openFileDescriptor(uri, "w");
        FileOutputStream fileOutputStream =
                new FileOutputStream(pfd.getFileDescriptor());
        fileOutputStream.write(("Overwritten 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();
    }
}

Xoá tài liệu

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

Kotlin

DocumentsContract.deleteDocument(applicationContext.contentResolver, uri)

Java

DocumentsContract.deleteDocument(applicationContext.contentResolver, uri);

Truy xuất URI phương tiện tương đương

Phương thức getMediaUri() cung cấp một URI kho phương tiện tương đương với URI nhất định của nhà cung cấp tài liệu. 2 URI này tham chiếu đến cùng một mục cơ bản. Khi sử dụng URI kho nội dung nghe nhìn, bạn có thể truy cập các tệp nội dung nghe nhìn từ bộ nhớ dùng chung dễ dàng hơn.

Phương thức getMediaUri() hỗ trợ các URI ExternalStorageProvider. Trên Android 12 (API cấp 31) trở lên, phương thức này cũng hỗ trợ URI MediaDocumentsProvider.

Mở tệp ảo

Trên Android 7.0 (API cấp 25) trở lên, ứng dụng của bạn có thể sử dụng các tệp ảo mà Khung truy cập bộ nhớ cung cấp. Mặc dù tệp ảo không chứa cách biểu diễn nhị phân, nhưng ứng dụng của bạn có thể mở nội dung trong đó bằng cách chuyển đổi những tệp ảo này thành loại tệp khác hoặc bằng cách xem các tệp đó thông qua thao tác theo ý định ACTION_VIEW.

Để mở các tệp ảo, ứng dụng máy khách của bạn cần có logic đặc biệt nhằm xử lý các tệp đó. Nếu muốn xem cách biểu diễn byte của tệp, chẳng hạn như xem trước tệp, bạn cần yêu cầu loại MIME thay thế từ nhà cung cấp tài liệu.

Sau khi người dùng lựa chọn, hãy sử dụng URI trong dữ liệu kết quả để xác định tệp có phải là tệp ảo hay không, như trong đoạn mã sau:

Kotlin

private fun isVirtualFile(uri: Uri): Boolean {
    if (!DocumentsContract.isDocumentUri(this, uri)) {
        return false
    }

    val cursor: Cursor? = contentResolver.query(
            uri,
            arrayOf(DocumentsContract.Document.COLUMN_FLAGS),
            null,
            null,
            null
    )

    val flags: Int = cursor?.use {
        if (cursor.moveToFirst()) {
            cursor.getInt(0)
        } else {
            0
        }
    } ?: 0

    return flags and DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT != 0
}

Java

private boolean isVirtualFile(Uri uri) {
    if (!DocumentsContract.isDocumentUri(this, uri)) {
        return false;
    }

    Cursor cursor = getContentResolver().query(
        uri,
        new String[] { DocumentsContract.Document.COLUMN_FLAGS },
        null, null, null);

    int flags = 0;
    if (cursor.moveToFirst()) {
        flags = cursor.getInt(0);
    }
    cursor.close();

    return (flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) != 0;
}

Sau khi xác minh rằng tài liệu là tệp ảo, bạn có thể chuyển đổi tệp đó thành một loại MIME thay thế, chẳng hạn như "image/png". Đoạn mã sau đây cho biết cách kiểm tra xem tệp ảo có thể được biểu thị dưới dạng hình ảnh hay không và nếu có, thì bạn sẽ nhận được luồng dữ liệu đầu vào từ tệp ảo:

Kotlin

@Throws(IOException::class)
private fun getInputStreamForVirtualFile(
        uri: Uri, mimeTypeFilter: String): InputStream {

    val openableMimeTypes: Array<String>? =
            contentResolver.getStreamTypes(uri, mimeTypeFilter)

    return if (openableMimeTypes?.isNotEmpty() == true) {
        contentResolver
                .openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null)
                .createInputStream()
    } else {
        throw FileNotFoundException()
    }
}

Java

private InputStream getInputStreamForVirtualFile(Uri uri, String mimeTypeFilter)
    throws IOException {

    ContentResolver resolver = getContentResolver();

    String[] openableMimeTypes = resolver.getStreamTypes(uri, mimeTypeFilter);

    if (openableMimeTypes == null ||
        openableMimeTypes.length < 1) {
        throw new FileNotFoundException();
    }

    return resolver
        .openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null)
        .createInputStream();
}

Tài nguyên khác

Để biết thêm thông tin về cách lưu trữ và truy cập tài liệu cũng như các tệp khác, hãy tham khảo các tài nguyên sau.

Mẫu

Video