Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

Buka file menggunakan framework akses penyimpanan

Android 4.4 (API level 19) memperkenalkan Storage Access Framework (SAF-Kerangka Kerja Akses Penyimpanan). SAF memudahkan pengguna menjelajah dan membuka dokumen, gambar, dan file lainnya di semua penyedia penyimpanan dokumen pilihannya. UI standar yang mudah digunakan memungkinkan pengguna menjelajahi file dan mengakses yang terbaru dengan cara konsisten di antara berbagai aplikasi dan penyedia.

Layanan penyimpanan cloud atau lokal dapat dilibatkan dalam ekosistem ini dengan melibatkan DocumentsProvider yang merangkum layanannya. Aplikasi klien yang memerlukan akses ke dokumen sebuah penyedia bisa berintegrasi dengan SAF cukup dengan beberapa baris kode.

SAF terdiri dari berikut ini:

  • Penyedia dokumen—Penyedia materi yang memungkinkan layanan penyimpanan (seperti Google Drive) untuk menampilkan file yang dikelolanya. Penyedia dokumen diimplementasikan sebagai subkelas dari kelas DocumentsProvider. Skema penyedia dokumen didasarkan pada hierarki file biasa, meskipun bagaimana penyedia dokumen secara fisik menyimpan data. Platform Android menyertakan beberapa penyedia dokumen bawaan, seperti Unduhan, Gambar, dan Video.
  • Aplikasi klien—Aplikasi kustom yang memanggil intent ACTION_OPEN_DOCUMENT dan/atau ACTION_CREATE_DOCUMENT dan menerima file yang dikembalikan penyedia dokumen.
  • Picker—UI sistem yang memungkinkan pengguna mengakses dokumen dari semua penyedia dokumen yang memenuhi kriteria penelusuran aplikasi klien.

Beberapa fitur yang disediakan oleh SAF adalah sebagai berikut:

  • Memungkinkan pengguna menjelajah materi dari semua penyedia dokumen, bukan hanya satu aplikasi.
  • Memungkinkan aplikasi Anda memiliki akses jangka panjang dan tetap ke dokumen yang dimiliki oleh penyedia dokumen. Melalui akses ini pengguna bisa menambah, mengedit, menyimpan, dan menghapus file pada penyedia.
  • Mendukung banyak akun pengguna dan akar jangka pendek seperti penyedia penyimpanan USB, yang hanya muncul jika drive terpasang.

Ringkasan

SAF berpusat di seputar penyedia materi yang merupakan subkelas dari kelas DocumentsProvider. Dalam penyedia dokumen, data distrukturkan sebagai hierarki file biasa:

model data

Gambar 1. Model data penyedia dokumen. Root menunjuk ke satu Document, yang nanti memulai pemekaran seluruh pohon.

Perhatikan yang berikut ini:

  • Setiap penyedia dokumen melaporkan satu 'root' atau lebih, yang merupakan titik awal penyusuran pohon dokumen. Masing-masing root memiliki COLUMN_ROOT_ID yang unik, dan itu menunjuk ke dokumen (direktori) yang mewakili materi di bawah root tersebut. Roots sengaja dibuat dinamis untuk mendukung kasus penggunaan seperti multiakun, perangkat penyimpanan USB jangka pendek, atau masuk/keluar pengguna.
  • Di bawah tiap akar terdapat satu dokumen. Dokumen itu menunjuk ke dokumen-dokumen 1-ke-N, yang nanti masing-masing bisa menunjuk ke dokumen 1-ke-N.
  • Tiap backend penyimpanan memunculkan masing-masing file dan direktori dengan mengacunya lewat sebuah COLUMN_DOCUMENT_ID yang unik. ID dokumen harus unik dan tidak berubah setelah dibuat, karena ID ini digunakan untuk URI persisten yang diberikan pada saat boot ulang perangkat.
  • Dokumen bisa berupa file yang bisa dibuka (dengan tipe MIME tertentu), atau direktori yang berisi dokumen tambahan (dengan tipe MIME MIME_TYPE_DIR).
  • Setiap dokumen bisa memiliki kemampuan yang berbeda, seperti yang dijelaskan oleh COLUMN_FLAGS. Misalnya, FLAG_SUPPORTS_WRITE, FLAG_SUPPORTS_DELETE, dan FLAG_SUPPORTS_THUMBNAIL. COLUMN_DOCUMENT_ID yang sama dapat disertakan di beberapa direktori.

Alur kontrol

Seperti dinyatakan di atas, model data penyedia dokumen berdasarkan pada hierarki file biasa. Akan tetapi, Anda bisa secara fisik menyimpan data dengan cara apa pun yang disukai, selama Anda bisa mengaksesnya dengan menggunakan API DocumentsProvider. Misalnya, Anda bisa menggunakan penyimpanan cloud berbasis tag untuk data Anda.

Gambar 2 menampilkan cara aplikasi foto bisa menggunakan SAF untuk mengakses data tersimpan:

aplikasi

Gambar 2. Alur Storage Access Framework

Perhatikan yang berikut ini:

  • Di SAF, penyedia dan klien tidak berinteraksi secara langsung. Klien meminta izin untuk berinteraksi dengan file (yakni, membaca, mengedit, membuat, atau menghapus file).
  • Interaksi dimulai jika sebuah aplikasi (dalam contoh ini adalah aplikasi foto) mengeluarkan intent ACTION_OPEN_DOCUMENT atau ACTION_CREATE_DOCUMENT. Intent bisa berisi filter untuk menyaring kriteria—misalnya, "beri saya semua file yang bisa dibuka yang memiliki tipe MIME 'gambar'".
  • Setelah intent dibuat, picker sistem akan pergi ke setiap penyedia yang terdaftar dan menunjukkan kepada pengguna root materi yang cocok.
  • Picker memberi pengguna antarmuka standar untuk mengakses dokumen, walaupun penyedia dokumen dasar bisa sangat berbeda. Misalnya, gambar 2 menunjukkan penyedia Google Drive, penyedia USB, dan penyedia cloud.

Gambar 3 menunjukkan picker yang digunakan pengguna menelusuri gambar telah memilih akun Google Drive. Gambar tersebut juga menunjukkan semua root yang tersedia untuk aplikasi klien.

picker

Gambar 3. Picker

Ketika pengguna memilih Google Drive maka gambar akan ditampilkan, seperti yang ditunjukkan pada gambar 4. Dari titik itu, pengguna bisa berinteraksi dengan gambar dengan cara apa pun yang didukung oleh penyedia dan aplikasi klien.

picker

Gambar 4. Gambar

Menulis aplikasi klien

Pada Android 4.3 dan yang lebih rendah, jika Anda ingin aplikasi mengambil file dari aplikasi lain, aplikasi Anda harus memanggil intent seperti ACTION_PICK atau ACTION_GET_CONTENT. Pengguna nanti harus memilih satu aplikasi yang akan digunakan untuk mengambil file dan aplikasi yang dipilih harus menyediakan antarmuka pengguna bagi pengguna untuk menjelajah dan mengambil dari file yang tersedia.

Pada Android 4.4 dan yang lebih tinggi, Anda mempunyai opsi tambahan dalam menggunakan intent ACTION_OPEN_DOCUMENT, yang menampilkan UI picker yang dikontrol oleh sistem sehingga pengguna bisa menjelajah semua file yang disediakan aplikasi lain. Dari satu UI ini, pengguna bisa mengambil file dari aplikasi apa saja yang didukung.

ACTION_OPEN_DOCUMENT tidak dimaksudkan untuk menjadi pengganti ACTION_GET_CONTENT. Yang harus Anda gunakan bergantung pada kebutuhan aplikasi:

  • Gunakan ACTION_GET_CONTENT jika Anda ingin aplikasi cuma membaca/mengimpor data. Dengan pendekatan ini, aplikasi akan mengimpor salinan data, seperti file gambar.
  • Gunakan ACTION_OPEN_DOCUMENT jika Anda ingin aplikasi memiliki akses jangka panjang dan jangka pendek ke dokumen yang dimiliki oleh penyedia dokumen. Contohnya adalah aplikasi pengeditan foto yang memungkinkan pengguna mengedit gambar yang tersimpan dalam penyedia dokumen.

Bagian ini menjelaskan cara menulis aplikasi klien berdasarkan ACTION_OPEN_DOCUMENT dan intent ACTION_CREATE_DOCUMENT.

Cuplikan berikut menggunakan ACTION_OPEN_DOCUMENT untuk menelusuri penyedia dokumen yang berisi file gambar:

Kotlin

private const val READ_REQUEST_CODE: Int = 42
...
/**
 * Fires an intent to spin up the "file chooser" UI and select an image.
 */
fun performFileSearch() {

    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
    // browser.
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        // Filter to only show results that can be "opened", such as a
        // file (as opposed to a list of contacts or timezones)
        addCategory(Intent.CATEGORY_OPENABLE)

        // Filter to show only images, using the image MIME data type.
        // If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
        // To search for all documents available via installed storage providers,
        // it would be "*/*".
        type = "image/*"
    }

    startActivityForResult(intent, READ_REQUEST_CODE)
}

Java

private static final int READ_REQUEST_CODE = 42;
...
/**
 * Fires an intent to spin up the "file chooser" UI and select an image.
 */
public void performFileSearch() {

    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
    // browser.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

    // Filter to only show results that can be "opened", such as a
    // file (as opposed to a list of contacts or timezones)
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Filter to show only images, using the image MIME data type.
    // If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
    // To search for all documents available via installed storage providers,
    // it would be "*/*".
    intent.setType("image/*");

    startActivityForResult(intent, READ_REQUEST_CODE);
}

Perhatikan yang berikut ini:

  • Saat aplikasi memicu intent ACTION_OPEN_DOCUMENT, aplikasi akan menjalankan picker yang menampilkan semua penyedia dokumen yang cocok.
  • Menambahkan kategori CATEGORY_OPENABLE ke intent akan menyaring hasil agar hanya menampilkan dokumen yang bisa dibuka, seperti file gambar.
  • Pernyataan intent.setType("image/*") memfilter lebih jauh agar hanya menampilkan dokumen yang memiliki tipe data MIME gambar.

Proses hasil

Setelah pengguna memilih dokumen di picker, onActivityResult() dipanggil. Parameter resultData berisi URI yang menunjuk ke dokumen yang dipilih. Ekstrak URI dengan getData(). Setelah mendapatkannya, Anda bisa menggunakannya untuk mengambil dokumen yang diinginkan pengguna. Sebagai contoh:

Kotlin

override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {

    // The ACTION_OPEN_DOCUMENT intent was sent with the request code
    // READ_REQUEST_CODE. If the request code seen here doesn't match, it's the
    // response to some other intent, and the code below shouldn't run at all.

    if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
        // The document selected by the user won't be returned in the intent.
        // Instead, a URI to that document will be contained in the return intent
        // provided to this method as a parameter.
        // Pull that URI using resultData.getData().
        resultData?.data?.also { uri ->
            Log.i(TAG, "Uri: $uri")
            showImage(uri)
        }
    }
}

Java

@Override
public void onActivityResult(int requestCode, int resultCode,
        Intent resultData) {

    // The ACTION_OPEN_DOCUMENT intent was sent with the request code
    // READ_REQUEST_CODE. If the request code seen here doesn't match, it's the
    // response to some other intent, and the code below shouldn't run at all.

    if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
        // The document selected by the user won't be returned in the intent.
        // Instead, a URI to that document will be contained in the return intent
        // provided to this method as a parameter.
        // Pull that URI using resultData.getData().
        Uri uri = null;
        if (resultData != null) {
            uri = resultData.getData();
            Log.i(TAG, "Uri: " + uri.toString());
            showImage(uri);
        }
    }
}

Memeriksa metadata dokumen

Setelah Anda memiliki URI untuk dokumen, Anda akan mendapatkan akses ke metadatanya. Cuplikan ini mengambil metadata sebuah dokumen yang disebutkan oleh URI, dan mencatatnya:

Kotlin

fun dumpImageMetaData(uri: Uri) {

    // The query, since it only applies to a single document, will only return
    // one row. There's no need to filter, sort, or select fields, since we want
    // all fields for one document.
    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 since an
            // int can't be null in Java, the behavior is implementation-specific,
            // which is just a fancy term for "unpredictable".  So as
            // a rule, check if it's null before assigning to an int.  This will
            // happen often:  The storage API allows for remote files, whose
            // size might not be locally known.
            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, since it only applies to a single document, will only return
    // one row. There's no need to filter, sort, or select fields, since we want
    // all fields for one document.
    Cursor cursor = getActivity().getContentResolver()
            .query(uri, null, null, null, null, null);

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

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

            int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
            // If the size is unknown, the value stored is null.  But since an
            // int can't be null in Java, the behavior is implementation-specific,
            // which is just a fancy term for "unpredictable".  So as
            // a rule, check if it's null before assigning to an int.  This will
            // happen often:  The storage API allows for remote files, whose
            // size might not be locally known.
            String size = null;
            if (!cursor.isNull(sizeIndex)) {
                // Technically the column stores an int, but cursor.getString()
                // will do the conversion automatically.
                size = cursor.getString(sizeIndex);
            } else {
                size = "Unknown";
            }
            Log.i(TAG, "Size: " + size);
        }
    } finally {
        cursor.close();
    }
}

Membuka dokumen

Setelah mendapatkan URI dokumen, Anda bisa membuka dokumen atau melakukan apa saja yang diinginkan padanya.

Bitmap

Berikut ini adalah contoh cara membuka Bitmap:

Kotlin

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

Perhatikan bahwa Anda tidak boleh melakukan operasi ini pada thread UI. Lakukan hal ini di latar belakang, dengan menggunakan AsyncTask. Setelah membuka bitmap, Anda bisa menampilkannya dalam ImageView.

Mengambil InputStream

Berikut ini contoh cara mengambil InputStream dari URI. Dalam cuplikan ini, baris-baris file dibaca ke dalam sebuah string:

Kotlin

@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 {
    InputStream inputStream = getContentResolver().openInputStream(uri);
    BufferedReader reader = new BufferedReader(new InputStreamReader(
            inputStream));
    StringBuilder stringBuilder = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        stringBuilder.append(line);
    }
    fileInputStream.close();
    parcelFileDescriptor.close();
    return stringBuilder.toString();
}

Membuat dokumen

Aplikasi Anda bisa membuat dokumen baru dalam penyedia dokumen dengan menggunakan intent ACTION_CREATE_DOCUMENT. Untuk membuat file, Anda memberikan satu tipe MIME dan satu nama file, dan menjalankannya dengan kode permintaan yang unik. Selebihnya akan diurus untuk Anda:

Kotlin

// Here are some examples of how you might call this method.
// The first parameter is the MIME type, and the second parameter is the name
// of the file you are creating:
//
// createFile("text/plain", "foobar.txt");
// createFile("image/png", "mypicture.png");

// Unique request code.
private const val WRITE_REQUEST_CODE: Int = 43
...
private fun createFile(mimeType: String, fileName: String) {
    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
        // Filter to only show results that can be "opened", such as
        // a file (as opposed to a list of contacts or timezones).
        addCategory(Intent.CATEGORY_OPENABLE)

        // Create a file with the requested MIME type.
        type = mimeType
        putExtra(Intent.EXTRA_TITLE, fileName)
    }

    startActivityForResult(intent, WRITE_REQUEST_CODE)
}

Java

// Here are some examples of how you might call this method.
// The first parameter is the MIME type, and the second parameter is the name
// of the file you are creating:
//
// createFile("text/plain", "foobar.txt");
// createFile("image/png", "mypicture.png");

// Unique request code.
private static final int WRITE_REQUEST_CODE = 43;
...
private void createFile(String mimeType, String fileName) {
    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);

    // Filter to only show results that can be "opened", such as
    // a file (as opposed to a list of contacts or timezones).
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Create a file with the requested MIME type.
    intent.setType(mimeType);
    intent.putExtra(Intent.EXTRA_TITLE, fileName);
    startActivityForResult(intent, WRITE_REQUEST_CODE);
}

Setelah membuat dokumen baru, Anda bisa mendapatkan URI-nya dalam onActivityResult(), sehingga Anda bisa terus menulis ke dokumen itu.

Menghapus dokumen

Jika Anda memiliki URI dokumen dan Document.COLUMN_FLAGS dokumen berisi SUPPORTS_DELETE, Anda bisa menghapus dokumen tersebut. Sebagai contoh:

Kotlin

DocumentsContract.deleteDocument(contentResolver, uri)

Java

DocumentsContract.deleteDocument(getContentResolver(), uri);

Mengedit dokumen

Anda bisa menggunalan SAF untuk mengedit dokumen teks di tempat. Cuplikan memicu intent ACTION_OPEN_DOCUMENT dan menggunakan CATEGORY_OPENABLE kategori agar hanya menampilkan dokumen yang bisa dibuka. Ini akan menyaring lebih jauh untuk menampilkan file teks saja:

Kotlin

private const val EDIT_REQUEST_CODE: Int = 44
/**
 * Open a file for writing and append some text to it.
 */
private fun editDocument() {
    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's
    // file browser.
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        // Filter to only show results that can be "opened", such as a
        // file (as opposed to a list of contacts or timezones).
        addCategory(Intent.CATEGORY_OPENABLE)

        // Filter to show only text files.
        type = "text/plain"
    }

    startActivityForResult(intent, EDIT_REQUEST_CODE)
}

Java

private static final int EDIT_REQUEST_CODE = 44;
/**
 * Open a file for writing and append some text to it.
 */
 private void editDocument() {
    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's
    // file browser.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

    // Filter to only show results that can be "opened", such as a
    // file (as opposed to a list of contacts or timezones).
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Filter to show only text files.
    intent.setType("text/plain");

    startActivityForResult(intent, EDIT_REQUEST_CODE);
}

Berikutnya, dari onActivityResult() (lihat Memproses hasil) Anda bisa memanggil kode untuk mengedit. Cuplikan berikut mendapatkan FileOutputStream dari ContentResolver. Secara default cuplikan menggunakan mode tulis. Inilah praktik terbaik untuk meminta jumlah akses minimum yang Anda perlukan, jadi jangan meminta baca/tulis jika yang Anda perlukan hanyalah tulis:

Kotlin

private fun alterDocument(uri: Uri) {
    try {
        contentResolver.openFileDescriptor(uri, "w")?.use {
            // use{} lets the document provider know you're done by automatically closing the stream
            FileOutputStream(it.fileDescriptor).use {
                it.write(
                    ("Overwritten by MyCloud 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 by MyCloud at " +
                System.currentTimeMillis() + "\n").getBytes());
        // Let the document provider know you're done by closing the stream.
        fileOutputStream.close();
        pfd.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Mempertahankan izin

Bila aplikasi Anda membuka file untuk membaca atau menulis, sistem akan memberi aplikasi Anda izin URI untuk file itu, yang akan berlaku hingga perangkat pengguna dimulai ulang. Namun anggaplah aplikasi Anda adalah aplikasi pengeditan gambar, dan Anda ingin pengguna bisa mengakses 5 gambar terakhir yang dieditnya, langsung dari aplikasi Anda. Jika perangkat pengguna telah dimulai ulang, maka Anda harus mengirim pengguna kembali ke picker sistem untuk menemukan file, hal ini jelas tidak ideal.

Untuk mencegah terjadinya hal ini, Anda bisa mempertahankan izin yang diberikan sistem ke aplikasi Anda. Secara efektif, aplikasi Anda akan "mengambil" pemberian izin URI yang bisa dipertahankan, yang ditawarkan oleh sistem. Hal ini memberi pengguna akses kontinu ke file melalui aplikasi Anda, sekalipun perangkat telah dimulai ulang:

Kotlin

val takeFlags: Int = intent.flags and
        (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);

Ada satu langkah akhir. URI terbaru yang diakses aplikasi Anda mungkin tidak lagi valid,—aplikasi lain mungkin telah menghapus atau memodifikasi dokumen. Karena itu, Anda harus selalu memanggil getContentResolver().takePersistableUriPermission() untuk memeriksa data terbaru.

Membuka file virtual

Android 7.0 menambahkan konsep file virtual pada Storage Access Framework. Walaupun file virtual tidak memiliki representasi biner, aplikasi klien Anda bisa membuka materi dengan menekannya ke tipe file yang berbeda atau dengan melihat file tersebut dengan menggunakan intent ACTION_VIEW.

Untuk membuka file virtual, aplikasi klien Anda perlu menyertakan logika khusus untuk melakukannya. Jika Anda ingin mendapatkan representasi byte dari file—untuk meninjau file, misalnya—Anda perlu meminta tipe MIME alternatif dari penyedia dokumen.

Untuk mendapatkan URI sebuah dokumen virtual di aplikasi Anda, terlebih dahulu buatlah Intent untuk membuka UI picker file, seperti kode yang ditampilkan sebelumnya di Cari dokumen.

Setelah pengguna membuat pilihan, sistem memanggil metode onActivityResult(), seperti yang ditampilkan sebelumnya di Hasil proses. Aplikasi Anda dapat mengambil URI file lalu menentukan apakah file tersebut virtual menggunakan metode yang mirip dengan cuplikan kode berikut:

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

Setelah Anda memverifikasi bahwa file tersebut virtual, Anda bisa menekan file ke tipe MIME alternatif seperti file gambar. Cuplikan kode berikut menunjukkan cara memeriksa apakah file virtual bisa direpresentasikan sebagai gambar, dan jika demikian, mendapatkan aliran input dari file virtual.

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 &lt; 1) {
        throw new FileNotFoundException();
    }

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

Untuk informasi selengkapnya tentang file virtual dan cara menanganinya dalam aplikasi klien Storage Access Framework, tonton video File virtual di framework akses penyimpanan.

Untuk kode contoh yang terkait dengan halaman ini, lihat:

Untuk video yang terkait dengan halaman ini, lihat:

Untuk informasi terkait tambahan, lihat: