Storage Access Framework

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 menjelajah file dan mengakses yang terbaru dengan cara konsisten di antara berbagai aplikasi dan penyedia.

Layanan storage di awan atau lokal bisa dilibatkan dalam ekosistem ini dengan mengimplementasikan sebuah DocumentsProvider yang membungkus 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 storage (seperti Google Drive) untuk menampilkan file yang dikelolanya. Penyedia dokumen diimplementasikan sebagai subkelas dari kelas DocumentsProvider. Skema penyedia dokumen berdasarkan hierarki file biasa, walaupun cara penyedia dokumen Anda secara fisik menyimpan data adalah terserah Anda. Platform Android terdiri dari beberapa penyedia dokumen bawaan, seperti Downloads, Images, dan Videos.
  • Aplikasi klien—Aplikasi khusus yang memanggil maksud 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 itu dipasang.

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 atau beberapa "akar" yang merupakan titik awal penyusuran pohon dokumen. Masing-masing akar memiliki sebuah COLUMN_ROOT_ID yang unik, dan menunjuk ke satu dokumen (satu direktori) yang mewakili materi di bawah akar itu. Akar sengaja dibuat dinamis untuk mendukung kasus penggunaan seperti multiakun, perangkat penyimpanan USB jangka pendek, atau proses 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 storage 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).
  • Tiap dokumen bisa mempunyai kemampuan berbeda, sebagaimana yang dijelaskan oleh COLUMN_FLAGS. Misalnya, FLAG_SUPPORTS_WRITE, FLAG_SUPPORTS_DELETE, dan FLAG_SUPPORTS_THUMBNAIL. COLUMN_DOCUMENT_ID yang sama bisa dimasukkan dalam beberapa direktori.

Alur Kontrol

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

Gambar 2 menampilkan contoh 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 bila sebuah aplikasi (dalam contoh ini adalah aplikasi foto) mengeluarkan maksud ACTION_OPEN_DOCUMENT atau ACTION_CREATE_DOCUMENT. Maksud bisa berisi filter untuk menyaring kriteria—misalnya, "beri saya semua file yang bisa dibuka yang memiliki tipe MIME 'gambar'".
  • Setelah maksud dibuat, picker sistem akan pergi ke setiap penyedia yang terdaftar dan menunjukkan kepada pengguna akar 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 awan.

Gambar 3 menunjukkan picker yang di digunakan pengguna menelusuri gambar telah memilih akun Google Drive:

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 maksud 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 untuk menjelajah dan mengambil dari file yang tersedia.

Pada Android 4.4 dan yang lebih tinggi, Anda mempunyai opsi tambahan dalam menggunakan maksud ACTION_OPEN_DOCUMENT, yang menampilkan UI picker yang dikontrol oleh sistem yang memungkinkan pengguna 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, misalnya 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 maksud ACTION_CREATE_DOCUMENT.

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

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 mengeluarkan maksud ACTION_OPEN_DOCUMENT, aplikasi akan menjalankan picker yang menampilkan semua penyedia dokumen yang cocok.
  • Menambahkan kategori CATEGORY_OPENABLE ke maksud 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.

Memproses Hasil

Setelah pengguna memilih dokumen di picker, onActivityResult() akan dipanggil. URI yang menunjuk ke dokumen yang dipilih dimasukkan dalam parameter resultData . Ekstrak URI dengan getData(). Setelah mendapatkannya, Anda bisa menggunakannya untuk mengambil dokumen yang diinginkan pengguna. Misalnya :

@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 memegang metadata sebuah dokumen yang disebutkan oleh URI, dan mencatatnya:

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:

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 adalah contoh cara mengambil InputStream dari URI. Dalam cuplikan ini, baris-baris file dibaca ke dalam sebuah string:

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 baru

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

// 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. Misalnya:

DocumentsContract.deleteDocument(getContentResolver(), uri);

Mengedit dokumen

Anda bisa menggunakan SAF untuk mengedit dokumen teks langsung di tempatnya. Cuplikan ini memicu maksud ACTION_OPEN_DOCUMENT dan menggunakan kategori CATEGORY_OPENABLE untuk menampilkan dokumen yang bisa dibuka saja. Ini akan menyaring lebih jauh untuk menampilkan file teks saja:

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, snipet 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:

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. Ini 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:

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. Anda mungkin telah menyimpan URI terbaru yang diakses aplikasi, namun URI itu mungkin tidak lagi valid,—aplikasi lain mungkin telah menghapus atau memodifikasi dokumen. Karena itu, Anda harus selalu memanggil getContentResolver().takePersistableUriPermission() untuk memeriksa data terbaru.

Menulis Penyedia Dokumen Khusus

Jika Anda sedang mengembangkan aplikasi yang menyediakan layanan storage untuk file (misalnya layanan storage di awan), Anda bisa menyediakan file melalui SAF dengan menulis penyedia dokumen khusus. Bagian ini menjelaskan caranya.

Manifes

Untuk mengimplementasikan penyedia dokumen khusus, tambahkan yang berikut ini ke manifes aplikasi Anda:

  • Target berupa API level 19 atau yang lebih tinggi.
  • Elemen <provider> yang mendeklarasikan penyedia storage khusus Anda.
  • Nama penyedia Anda, yaitu nama kelasnya, termasuk nama paket. Misalnya: com.example.android.storageprovider.MyCloudProvider.
  • Nama otoritas Anda, yaitu nama paket Anda (dalam contoh ini, com.example.android.storageprovider) plus tipe penyedia materi (documents). Misalnya, com.example.android.storageprovider.documents.
  • Atribut android:exported yang diatur ke "true". Anda harus mengekspor penyedia sehingga aplikasi lain bisa membacanya.
  • Atribut android:grantUriPermissions yang diatur ke "true". Setelan ini memungkinkan sistem memberi aplikasi lain akses ke materi dalam penyedia Anda. Untuk pembahasan cara mempertahankan pemberian bagi dokumen tertentu, lihat Mempertahankan izin.
  • Izin MANAGE_DOCUMENTS. Secara default, penyedia tersedia bagi siapa saja. Menambahkan izin ini akan membatasi penyedia Anda pada sistem. Pembatasan ini penting untuk keamanan.
  • Atribut android:enabled yang disetel ke nilai boolean didefinisikan dalam file sumber daya. Tujuan atribut ini adalah menonaktifkan penyedia pada perangkat yang menjalankan Android 4.3 atau yang lebih rendah. Misalnya, android:enabled="@bool/atLeastKitKat". Selain memasukkan atribut ini dalam manifes, Anda perlu melakukan hal-hal berikut:
    • Dalam file sumber daya bool.xml Anda di bawah res/values/, tambahkan baris ini:
      <bool name="atLeastKitKat">false</bool>
    • Dalam file sumber daya bool.xml Anda di bawah res/values-v19/, tambahkan baris ini:
      <bool name="atLeastKitKat">true</bool>
  • Sebuah filter maksud berisi aksi android.content.action.DOCUMENTS_PROVIDER, agar penyedia Anda muncul dalam picker saat sistem menelusuri penyedia.

Berikut ini adalah kutipan contoh manifes berisi penyedia yang:

<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"
            android:enabled="@bool/atLeastKitKat">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>
    </application>

</manifest>

Mendukung perangkat yang menjalankan Android 4.3 dan yang lebih rendah

Maksud ACTION_OPEN_DOCUMENT hanya tersedia pada perangkat yang menjalankan Android 4.4 dan yang lebih tinggi. Jika ingin aplikasi Anda mendukung ACTION_GET_CONTENT untuk mengakomodasi perangkat yang menjalankan Android 4.3 dan yang lebih rendah, Anda harus menonaktifkan filter maksud ACTION_GET_CONTENT dalam manifes untuk perangkat yang menjalankan Android 4.4 atau yang lebih tinggi. Penyedia dokumen dan ACTION_GET_CONTENT harus dianggap saling eksklusif. Jika Anda mendukung keduanya sekaligus, aplikasi Anda akan muncul dua kali dalam UI picker sistem, yang menawarkan dua cara mengakses data tersimpan Anda. Hal ini akan membingungkan pengguna.

Berikut ini adalah cara yang disarankan untuk menonaktifkan filter maksud ACTION_GET_CONTENT untuk perangkat yang menjalankan Android versi 4.4 atau yang lebih tinggi:

  1. Dalam file sumber daya bool.xml Anda di bawah res/values/, tambahkan baris ini:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. Dalam file sumber daya bool.xml Anda di bawah res/values-v19/, tambahkan baris ini:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. Tambahkan alias aktivitas untuk menonaktifkan filter maksud ACTION_GET_CONTENT bagi versi 4.4 (API level 19) dan yang lebih tinggi. Misalnya:
    <!-- 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>
    

Kontrak

Biasanya bila Anda menulis penyedia materi khusus, salah satu tugas adalah mengimplementasikan kelas kontrak, seperti dijelaskan dalam panduan developer Penyedia Materi. Kelas kontrak adalah kelas public final yang berisi definisi konstanta untuk URI, nama kolom, tipe MIME, dan metadata lain yang berkenaan dengan penyedia. SAF menyediakan kelas-kelas kontrak ini untuk Anda, jadi Anda tidak perlu menulisnya sendiri:

Misalnya, berikut ini adalah kolom-kolom yang bisa Anda hasilkan di kursor bila penyedia dokumen Anda membuat kueri dokumen atau akar:

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

Subkelas DocumentsProvider

Langkah berikutnya dalam menulis penyedia dokumen khusus adalah menjadikan kelas abstrak sebagai subkelas DocumentsProvider. Setidaknya, Anda perlu mengimplementasikan metode berikut:

Hanya inilah metode yang diwajibkan kepada Anda secara ketat untuk diimplementasikan, namun ada banyak lagi yang mungkin Anda inginkan. Lihat DocumentsProvider untuk detailnya.

Mengimplementasikan queryRoots

Implementasi queryRoots() oleh Anda harus mengembalikan Cursor yang menunjuk ke semua direktori akar penyedia dokumen, dengan menggunakan kolom-kolom yang didefinisikan dalam DocumentsContract.Root.

Dalam cuplikan berikut, parameter projection mewakili bidang-bidang tertentu yang ingin didapatkan kembali oleh pemanggil. Cuplikan ini membuat kursor baru dan menambahkan satu baris ke satu akar— kursor, satu direktori level atas, seperti Downloads atau Images. Kebanyakan penyedia hanya mempunyai satu akar. Anda bisa mempunyai lebih dari satu, misalnya, jika ada banyak akun pengguna. Dalam hal itu, cukup tambahkan sebuah baris kedua ke kursor.

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

    // Create 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.
    // Construct one row for a root called "MyCloud".
    final MatrixCursor.RowBuilder row = result.newRow();
    row.add(Root.COLUMN_ROOT_ID, ROOT);
    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 once it's shared.
    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir));

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

    return result;
}

Mengimplementasikan queryChildDocuments

Implementasi queryChildDocuments() oleh Anda harus mengembalikan Cursor yang menunjuk ke semua file dalam direktori yang ditentukan, dengan menggunakan kolom-kolom yang didefinisikan dalam DocumentsContract.Document.

Metode ini akan dipanggil bila Anda memilih akar aplikasi dalam picker UI. Metode mengambil dokumen anak dari direktori di bawah akar. Metode ini bisa dipanggil pada level apa saja dalam hierarki file, bukan hanya akar. Cuplikan ini membuat kursor baru dengan kolom-kolom yang diminta, lalu menambahkan informasi tentang setiap anak langsung dalam direktori induk ke kursor. Satu anak bisa berupa gambar, direktori lain—file apa saja:

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

Mengimplementasikan queryDocument

Implementasi queryDocument() oleh Anda harus mengembalikan Cursor yang menunjuk ke file yang disebutkan, dengan menggunakan kolom-kolom yang didefinisikan dalam DocumentsContract.Document.

Metode queryDocument() mengembalikan informasi yang sama yang diteruskan dalam queryChildDocuments(), namun untuk file tertentu:

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

Mengimplementasikan openDocument

Anda harus mengimplementasikan openDocument() untuk mengembalikan ParcelFileDescriptor yang mewakili file yang disebutkan. Aplikasi lain bisa menggunakan ParcelFileDescriptor yang dikembalikan untuk mengalirkan data. Sistem memanggil metode ini setelah pengguna memilih file dan aplikasi klien meminta akses ke file itu dengan memanggil openFileDescriptor(). Misalnya:

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

Keamanan

Anggaplah penyedia dokumen Anda sebuah layanan storage awan yang dilindungi kata sandi dan Anda ingin memastikan bahwa pengguna sudah login sebelum Anda mulai berbagi file mereka. Apakah yang harus dilakukan aplikasi Anda jika pengguna tidak login? Solusinya adalah mengembalikan akar nol dalam implementasi queryRoots() Anda. Yakni, sebuah kursor akar kosong:

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

Langkah lainnya adalah memanggil getContentResolver().notifyChange(). Ingat DocumentsContract? Kita menggunakannya untuk membuat URI ini. Cuplikan berikut memberi tahu sistem untuk melakukan kueri akar penyedia dokumen Anda kapan saja status proses masuk pengguna berubah. Jika pengguna tidak login, panggilan ke queryRoots() akan mengembalikan kursor kosong, seperti yang ditampilkan di atas. Cara ini akan memastikan bahwa dokumen penyedia hanya tersedia jika pengguna login ke penyedia itu.

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