Mengakses dokumen dan file lainnya dari penyimpanan bersama

Pada perangkat yang menjalankan Android 4.4 (API level 19) dan yang lebih tinggi, aplikasi Anda dapat berinteraksi dengan penyedia dokumen, termasuk volume penyimpanan eksternal dan penyimpanan berbasis cloud, menggunakan Storage Access Framework Framework ini memungkinkan pengguna berinteraksi dengan alat pilih sistem guna memilih penyedia dokumen serta memilih dokumen tertentu dan file lainnya untuk dibuat, dibuka, dan dimodifikasi aplikasi Anda.

Karena pengguna terlibat dalam pemilihan file atau direktori yang dapat diakses oleh aplikasi Anda, mekanisme ini tidak memerlukan izin sistem apa pun, serta privasi dan kontrol pengguna akan ditingkatkan. Selain itu, file ini, yang disimpan di luar direktori khusus aplikasi dan di luar penyimpanan media, akan tetap berada di perangkat setelah aplikasi Anda di-uninstal.

Penggunaan framework mencakup langkah-langkah berikut:

  1. Aplikasi memanggil intent yang berisi tindakan terkait penyimpanan. Tindakan ini sesuai dengan kasus penggunaan tertentu yang disediakan oleh framework.
  2. Pengguna akan melihat alat pilih sistem, sehingga dapat menjelajahi penyedia dokumen dan memilih lokasi atau dokumen tempat tindakan terkait penyimpanan dilakukan.
  3. Aplikasi mendapatkan akses baca dan tulis ke URI yang merepresentasikan lokasi atau dokumen yang dipilih pengguna. Dengan menggunakan URI ini, aplikasi dapat melakukan operasi pada lokasi yang dipilih.

Jika aplikasi menggunakan penyimpanan media, Anda harus meminta izin READ_EXTERNAL_STORAGE untuk mengakses file media aplikasi lain. Pada perangkat yang menjalankan Android 9 (API level 28) atau yang lebih rendah, aplikasi Anda harus meminta izin READ_EXTERNAL_STORAGE untuk mengakses file media apa pun, termasuk file media yang dibuat oleh aplikasi Anda.

Panduan ini menjelaskan berbagai kasus penggunaan yang didukung oleh framework untuk bekerja dengan file dan dokumen lainnya. Panduan ini juga menjelaskan cara melakukan operasi pada lokasi yang dipilih pengguna.

Kasus penggunaan untuk mengakses dokumen dan file lainnya

Storage Access Framework mendukung kasus penggunaan berikut untuk mengakses file dan dokumen lainnya.

Membuat file baru
Tindakan intent ACTION_CREATE_DOCUMENT memungkinkan pengguna untuk menyimpan file di lokasi tertentu.
Membuka dokumen atau file
Tindakan intent ACTION_OPEN_DOCUMENTmemungkinkan pengguna untuk memilih dokumen atau file tertentu yang akan dibuka.
Memberikan akses ke konten direktori
Tindakan intent ACTION_OPEN_DOCUMENT_TREE, yang tersedia di Android 5.0 (API level 21) dan yang lebih tinggi, memungkinkan pengguna untuk memilih direktori tertentu, yang memberikan aplikasi Anda akses ke semua file dan subdirektori dalam direktori tersebut.

Bagian berikut memberikan panduan tentang cara mengonfigurasi setiap kasus penggunaan.

Membuat file baru

Gunakan tindakan intent ACTION_CREATE_DOCUMENT untuk memuat alat pilih file sistem dan memungkinkan pengguna memilih lokasi untuk menulis isi file. Proses ini mirip dengan yang digunakan dalam dialog "simpan sebagai" yang digunakan oleh sistem operasi lain.

Catatan: ACTION_CREATE_DOCUMENT tidak dapat menimpa file yang sudah ada. Jika aplikasi Anda mencoba menyimpan file dengan nama yang sama, sistem akan menambahkan nomor dalam tanda kurung di akhir nama file.

Misalnya, jika aplikasi Anda mencoba menyimpan file yang bernama confirmation.pdf dalam direktori yang sudah memiliki file dengan nama tersebut, sistem akan menyimpan file baru dengan nama confirmation.pdf (1).

Saat mengonfigurasi intent, tentukan nama file dan jenis MIME, serta tentukan URI file atau direktori secara opsional yang akan ditampilkan oleh alat pilih file saat file pertama kali dimuat menggunakan intent tambahan EXTRA_INITIAL_URI.

Cuplikan kode berikut menunjukkan cara membuat dan memanggil intent untuk membuat file:

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

Membuka file

Aplikasi Anda mungkin menggunakan dokumen sebagai unit penyimpanan tempat pengguna memasukkan data yang mungkin ingin mereka bagikan kepada rekan atau impor ke dokumen lain. Beberapa contohnya antara lain pengguna yang membuka dokumen produktivitas atau membuka buku yang disimpan sebagai file EPUB.

Dalam kasus ini, izinkan pengguna memilih file yang akan dibuka dengan memanggil intent ACTION_OPEN_DOCUMENT, yang membuka aplikasi alat pilih file sistem. Untuk hanya menampilkan jenis file yang didukung aplikasi Anda, tentukan jenis MIME. Selain itu, Anda juga dapat secara opsional menentukan URI file yang akan ditampilkan oleh alat pilih file saat file tersebut pertama kali dimuat menggunakan intent tambahan EXTRA_INITIAL_URI.

Cuplikan kode berikut menunjukkan cara membuat dan memanggil intent untuk membuka dokumen 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);
    }
    

Memberikan akses ke konten direktori

Aplikasi pengelolaan file dan pembuatan media biasanya mengelola kelompok-kelompok file dalam sebuah hierarki direktori. Untuk menyediakan kemampuan ini di aplikasi Anda, gunakan tindakan intent ACTION_OPEN_DOCUMENT_TREE, yang memungkinkan pengguna memberi akses ke seluruh hierarki direktori. Selanjutnya, aplikasi Anda dapat mengakses file apa pun di direktori yang dipilih dan subdirektorinya.

Saat menggunakan ACTION_OPEN_DOCUMENT_TREE, aplikasi Anda hanya mendapatkan akses ke file dalam direktori yang dipilih pengguna. Anda tidak memiliki akses ke file aplikasi lain yang berada di luar direktori pilihan pengguna ini. Akses yang dikontrol pengguna ini memungkinkan pengguna untuk memilih secara tepat konten yang ingin mereka bagikan kepada aplikasi Anda.

Selain itu, Anda dapat menentukan URI direktori yang akan ditampilkan oleh alat pilih file saat file dimuat pertama kali menggunakan intent tambahan EXTRA_INITIAL_URI.

Cuplikan kode berikut menunjukkan cara membuat dan memanggil intent untuk membuka direktori:

Kotlin

    fun openDirectory(pickerInitialUri: Uri) {
        // Choose a directory using the system's file picker.
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
            // Provide read access to files and sub-directories in the user-selected
            // directory.
            flags = Intent.FLAG_GRANT_READ_URI_PERMISSION

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

        // Provide read access to files and sub-directories in the user-selected
        // directory.
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

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

Melakukan operasi pada lokasi yang dipilih

Setelah pengguna memilih file atau direktori menggunakan alat pilih file sistem, Anda dapat mengambil URI item yang dipilih menggunakan kode berikut di 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.
            }
        }
    }
    

Dengan mendapatkan referensi ke URI item yang dipilih, aplikasi Anda dapat melakukan beberapa operasi pada item tersebut. Misalnya, Anda dapat mengakses metadata item, mengedit item di tempat, dan menghapus item.

Bagian berikut menunjukkan cara menyelesaikan tindakan pada file yang dipilih pengguna.

Menentukan operasi yang didukung oleh penyedia

Penyedia konten yang berbeda memungkinkan operasi yang berbeda dilakukan pada dokumen — seperti menyalin dokumen atau melihat thumbnail dokumen. Untuk menentukan operasi mana yang didukung oleh penyedia tertentu, periksa nilai Document.COLUMN_FLAGS. Selanjutnya, UI aplikasi Anda hanya dapat menampilkan opsi yang didukung oleh penyedia.

Mempertahankan izin

Saat aplikasi Anda membuka file untuk membaca atau menulis, sistem memberi aplikasi Anda izin URI untuk file tersebut, yang akan berlaku hingga perangkat pengguna memulai ulang. Anggaplah aplikasi Anda adalah aplikasi pengeditan gambar, dan Anda ingin pengguna dapat mengakses 5 gambar yang baru saja diedit, langsung dari aplikasi Anda. Jika perangkat pengguna dimulai ulang, Anda harus mengembalikan pengguna ke alat pilih sistem untuk menemukan file tersebut.

Untuk mempertahankan akses ke file saat perangkat dimulai ulang dan menciptakan pengalaman pengguna yang lebih baik, aplikasi Anda dapat "menerima" pemberian izin URI persistable yang ditawarkan oleh sistem, seperti yang ditunjukkan dalam cuplikan kode berikut:

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

Memeriksa metadata dokumen

Setelah Anda memiliki URI untuk dokumen, Anda akan mendapatkan akses ke metadatanya. Cuplikan ini mengambil metadata untuk dokumen yang ditentukan oleh URI, dan memasukkannya dalam log:

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

Membuka dokumen

Dengan memiliki referensi ke URI dokumen, Anda dapat membuka dokumen untuk diproses lebih lanjut. Bagian ini menunjukkan contoh untuk membuka bitmap dan stream input.

Bitmap

Cuplikan kode berikut menunjukkan cara membuka file Bitmap mengingat URI-nya:

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

Setelah membuka bitmap, Anda dapat menampilkannya dalam bentuk ImageView.

Stream input

Cuplikan kode berikut menunjukkan cara membuka objek InputStream mengingat URI-nya. Dalam cuplikan ini, baris-baris file dibaca ke dalam sebuah string:

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

Mengedit dokumen

Anda dapat menggunakan Storage Access Framework untuk mengedit teks dokumen di tempat

Cuplikan kode berikut menimpa isi dokumen yang direpresentasikan oleh URI tertentu:

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

Menghapus dokumen

Jika memiliki URI untuk dokumen dan Document.COLUMN_FLAGS dokumen berisi SUPPORTS_DELETE, Anda dapat menghapus dokumen tersebut. Contoh:

Kotlin

    DocumentsContract.deleteDocument(applicationContext.contentResolver, uri)
    

Java

    DocumentsContract.deleteDocument(applicationContext.contentResolver, uri);
    

Membuka file virtual

Pada Android 7.0 (API level 25) dan yang lebih tinggi, aplikasi Anda dapat menggunakan file virtual yang disediakan oleh Storage Access Framework. Meskipun file virtual tidak memiliki representasi biner, aplikasi Anda dapat membuka isi file tersebut dengan memaksanya masuk ke dalam jenis file yang berbeda atau dengan menampilkan file tersebut menggunakan tindakan intent ACTION_VIEW.

Untuk membuka file virtual, aplikasi klien Anda perlu menyertakan logika khusus untuk menangani file tersebut. Jika ingin mendapatkan representasi byte dari file—misalnya, untuk melihat pratinjau file—Anda harus meminta jenis MIME alternatif dari penyedia dokumen.

Setelah pengguna membuat pilihan, gunakan URI dalam data hasil untuk menentukan apakah file tersebut adalah virtual, seperti yang ditunjukkan dalam 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 memastikan bahwa dokumen tersebut adalah file virtual, Anda kemudian dapat memaksa file tersebut ke dalam jenis MIME alternatif, seperti "image/png". Cuplikan kode berikut menunjukkan cara memeriksa apakah file virtual dapat direpresentasikan sebagai gambar, dan jika demikian, mendapatkan stream 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 < 1) {
            throw new FileNotFoundException();
        }

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

Referensi lainnya

Untuk informasi selengkapnya tentang cara menyimpan dan mengakses dokumen dan file lainnya, lihat referensi berikut.

Contoh

Video