1page.title=Storage Access Framework
2@jd:body
3<div id="qv-wrapper">
4<div id="qv">
5
6<h2>Dalam dokumen ini
7 <a href="#" onclick="hideNestedItems('#toc44',this);return false;" class="header-toggle">
8        <span class="more">tampilkan maksimal</span>
9        <span class="less" style="display:none">tampilkan minimal</span></a></h2>
10<ol id="toc44" class="hide-nested">
11    <li>
12        <a href="#overview">Ikhtisar</a>
13    </li>
14    <li>
15        <a href="#flow">Arus Kontrol</a>
16    </li>
17    <li>
18        <a href="#client">Menulis Aplikasi Klien</a>
19        <ol>
20        <li><a href="#search">Mencari dokumen</a></li>
21        <li><a href="#process">Memproses hasil</a></li>
22        <li><a href="#metadata">Memeriksa metadata dokumen</a></li>
23        <li><a href="#open">Membuka dokumen</a></li>
24        <li><a href="#create">Membuat dokumen baru</a></li>
25        <li><a href="#delete">Menghapus dokumen</a></li>
26        <li><a href="#edit">Mengedit dokumen</a></li>
27        <li><a href="#permissions">Mempertahankan izin</a></li>
28        </ol>
29    </li>
30    <li><a href="#custom">Menulis Penyedia Dokumen Custom</a>
31        <ol>
32        <li><a href="#manifest">Manifes</a></li>
33        <li><a href="#contract">Kontrak</a></li>
34        <li><a href="#subclass">Subkelas DocumentsProvider</a></li>
35        <li><a href="#security">Keamanan</a></li>
36        </ol>
37    </li>
38
39</ol>
40<h2>Kelas-kelas utama</h2>
41<ol>
42    <li>{@link android.provider.DocumentsProvider}</li>
43    <li>{@link android.provider.DocumentsContract}</li>
44</ol>
45
46<h2>Video</h2>
47
48<ol>
49    <li><a href="http://www.youtube.com/watch?v=zxHVeXbK1P4">
50DevBytes: Android 4.4 Storage Access Framework: Penyedia</a></li>
51     <li><a href="http://www.youtube.com/watch?v=UFj9AEz0DHQ">
52DevBytes: Android 4.4 Storage Access Framework: Klien</a></li>
53</ol>
54
55
56<h2>Contoh Kode</h2>
57
58<ol>
59    <li><a href="{@docRoot}samples/StorageProvider/index.html">
60Penyedia Penyimpanan</a></li>
61     <li><a href="{@docRoot}samples/StorageClient/index.html">
62Klien Penyimpanan</a></li>
63</ol>
64
65<h2>Lihat Juga</h2>
66<ol>
67    <li>
68        <a href="{@docRoot}guide/topics/providers/content-provider-basics.html">
69        Dasar-Dasar Penyedia Konten
70        </a>
71    </li>
72</ol>
73
74</div>
75</div>
76
77
78<p>Android 4.4 (API level 19) memperkenalkan Storage Access Framework (SAF, Kerangka Kerja Akses Penyimpanan). SAF
79 memudahkan pengguna menyusuri dan membuka dokumen, gambar, dan file lainnya
80di semua penyedia penyimpanan dokumen pilihannya. UI standar yang mudah digunakan
81memungkinkan pengguna menyusuri file dan mengakses yang terbaru dengan cara konsisten di antara berbagai aplikasi dan penyedia.</p>
82
83<p>Layanan penyimpanan cloud atau lokal bisa dilibatkan dalam ekosistem ini dengan mengimplementasikan sebuah
84{@link android.provider.DocumentsProvider} yang membungkus layanannya. Aplikasi klien
85yang memerlukan akses ke dokumen sebuah penyedia bisa berintegrasi dengan SAF cukup dengan beberapa
86baris kode.</p>
87
88<p>SAF terdiri dari berikut ini:</p>
89
90<ul>
91<li><strong>Penyedia dokumen</strong>&mdash;Penyedia konten yang memungkinkan
92layanan penyimpanan (seperti Google Drive) untuk menampilkan file yang dikelolanya. Penyedia dokumen
93diimplementasikan sebagai subkelas dari kelas {@link android.provider.DocumentsProvider}.
94Skema penyedia dokumen berdasarkan hierarki file biasa,
95walaupun cara penyedia dokumen Anda secara fisik menyimpan data adalah terserah Anda.
96Platform Android terdiri dari beberapa penyedia dokumen bawaan, seperti
97Downloads, Images, dan Videos.</li>
98
99<li><strong>Aplikasi klien</strong>&mdash;Aplikasi custom yang memanggil intent
100{@link android.content.Intent#ACTION_OPEN_DOCUMENT} dan/atau
101{@link android.content.Intent#ACTION_CREATE_DOCUMENT} dan menerima
102file yang dihasilkan penyedia dokumen.</li>
103
104<li><strong>Picker</strong>&mdash;UI sistem yang memungkinkan pengguna mengakses dokumen dari semua
105penyedia dokumen yang memenuhi kriteria pencarian aplikasi klien.</li>
106</ul>
107
108<p>Beberapa fitur yang disediakan oleh SAF adalah sebagai berikut:</p>
109<ul>
110<li>Memungkinkan pengguna menyusuri konten dari semua penyedia dokumen, bukan hanya satu aplikasi.</li>
111<li>Memungkinkan aplikasi Anda memiliki akses jangka panjang dan tetap ke
112 dokumen yang dimiliki oleh penyedia dokumen. Melalui akses ini pengguna bisa menambah, mengedit,
113 menyimpan, dan menghapus file pada penyedia.</li>
114<li>Mendukung banyak akun pengguna dan akar jangka pendek seperti penyedia penyimpanan
115USB, yang hanya muncul jika drive itu dipasang. </li>
116</ul>
117
118<h2 id ="overview">Ikhtisar</h2>
119
120<p>SAF berpusat di seputar penyedia konten yang merupakan
121subkelas dari kelas {@link android.provider.DocumentsProvider}. Dalam <em>penyedia dokumen</em>, data
122distrukturkan sebagai hierarki file biasa:</p>
123<p><img src="{@docRoot}images/providers/storage_datamodel.png" alt="data model" /></p>
124<p class="img-caption"><strong>Gambar 1.</strong> Model data penyedia dokumen. Root menunjuk ke satu Document,
125yang nanti memulai pemekaran seluruh pohon.</p>
126
127<p>Perhatikan yang berikut ini:</p>
128<ul>
129
130<li>Setiap penyedia dokumen melaporkan satu atau beberapa
131"akar" yang merupakan titik awal penyusuran pohon dokumen.
132Masing-masing akar memiliki sebuah {@link android.provider.DocumentsContract.Root#COLUMN_ROOT_ID} yang unik,
133dan menunjuk ke satu dokumen (satu direktori)
134yang mewakili konten di bawah akar itu.
135Akar sengaja dibuat dinamis untuk mendukung kasus penggunaan seperti multiakun,
136perangkat penyimpanan USB jangka pendek, atau masuk/keluar pengguna.</li>
137
138<li>Di bawah tiap akar terdapat satu dokumen. Dokumen itu menunjuk ke dokumen-dokumen 1-ke-<em>N</em>,
139yang nanti masing-masing bisa menunjuk ke dokumen 1-ke-<em>N</em>. </li>
140
141<li>Tiap backend penyimpanan memunculkan
142masing-masing file dan direktori dengan mengacunya lewat sebuah
143{@link android.provider.DocumentsContract.Document#COLUMN_DOCUMENT_ID} yang unik.
144ID dokumen harus unik dan tidak berubah setelah dibuat, karena ID ini digunakan untuk
145URI persisten yang diberikan pada saat reboot perangkat.</li>
146
147
148<li>Dokumen bisa berupa file yang bisa dibuka (dengan tipe MIME tertentu), atau
149direktori yang berisi dokumen tambahan (dengan tipe MIME
150{@link android.provider.DocumentsContract.Document#MIME_TYPE_DIR}).</li>
151
152<li>Tiap dokumen bisa mempunyai kemampuan berbeda, sebagaimana yang dijelaskan oleh
153{@link android.provider.DocumentsContract.Document#COLUMN_FLAGS COLUMN_FLAGS}.
154Misalnya, {@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_WRITE},
155{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE}, dan
156{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_THUMBNAIL}.
157{@link android.provider.DocumentsContract.Document#COLUMN_DOCUMENT_ID} yang sama bisa
158dimasukkan dalam beberapa direktori.</li>
159</ul>
160
161<h2 id="flow">Arus Kontrol</h2>
162<p>Seperti dinyatakan di atas, model data penyedia dokumen dibuat berdasarkan hierarki file
163biasa. Akan tetapi, Anda bisa menyimpan secara fisik data dengan cara apa pun yang disukai,
164selama data bisa diakses melalui API {@link android.provider.DocumentsProvider}. Misalnya, Anda
165bisa menggunakan penyimpanan cloud berbasis tag untuk data Anda.</p>
166
167<p>Gambar 2 menampilkan contoh cara aplikasi foto bisa menggunakan SAF
168untuk mengakses data tersimpan:</p>
169<p><img src="{@docRoot}images/providers/storage_dataflow.png" alt="app" /></p>
170
171<p class="img-caption"><strong>Gambar 2.</strong> Arus Storage Access Framework</p>
172
173<p>Perhatikan yang berikut ini:</p>
174<ul>
175
176<li>Di SAF, penyedia dan klien tidak berinteraksi
177secara langsung. Klien meminta izin untuk berinteraksi
178dengan file (yakni, membaca, mengedit, membuat, atau menghapus file).</li>
179
180<li>Interaksi dimulai bila sebuah aplikasi (dalam contoh ini adalah aplikasi foto) mengeluarkan intent
181{@link android.content.Intent#ACTION_OPEN_DOCUMENT} atau {@link android.content.Intent#ACTION_CREATE_DOCUMENT}. Intent bisa berisi filter
182untuk mempersempit kriteria&mdash;misalnya, "beri saya semua file yang bisa dibuka
183yang memiliki tipe MIME 'gambar'".</li>
184
185<li>Setelah intent dibuat, picker sistem akan pergi ke setiap penyedia yang terdaftar
186dan menunjukkan kepada pengguna akar konten yang cocok.</li>
187
188<li>Picker memberi pengguna antarmuka standar untuk mengakses dokumen,
189walaupun penyedia dokumen dasar bisa sangat berbeda. Misalnya, gambar 2
190menunjukkan penyedia Google Drive, penyedia USB, dan penyedia cloud.</li>
191</ul>
192
193<p>Gambar 3 menunjukkan picker yang di digunakan pengguna mencari gambar telah memilih
194akun Google Drive:</p>
195
196<p><img src="{@docRoot}images/providers/storage_picker.png" width="340" alt="picker" style="border:2px solid #ddd" /></p>
197
198<p class="img-caption"><strong>Gambar 3.</strong> Picker</p>
199
200<p>Bila pengguna memilih Google Drive, gambar-gambar akan ditampilkan, seperti yang ditampilkan dalam
201gambar 4. Dari titik itu, pengguna bisa berinteraksi dengan gambar dengan cara apa pun
202yang didukung oleh penyedia dan aplikasi klien.
203
204<p><img src="{@docRoot}images/providers/storage_photos.png" width="340" alt="picker" style="border:2px solid #ddd" /></p>
205
206<p class="img-caption"><strong>Gambar 4.</strong> Gambar</p>
207
208<h2 id="client">Menulis Aplikasi Klien</h2>
209
210<p>Pada Android 4.3 dan yang lebih rendah, jika Anda ingin aplikasi mengambil file dari
211aplikasi lain, aplikasi Anda harus memanggil intent seperti {@link android.content.Intent#ACTION_PICK}
212atau {@link android.content.Intent#ACTION_GET_CONTENT}. Pengguna nanti harus memilih
213satu aplikasi yang akan digunakan untuk mengambil file dan aplikasi yang dipilih harus menyediakan antarmuka pengguna
214bagi untuk menyusuri dan mengambil dari file yang tersedia. </p>
215
216<p>Pada Android 4.4 dan yang lebih tinggi, Anda mempunyai opsi tambahan dalam menggunakan intent
217{@link android.content.Intent#ACTION_OPEN_DOCUMENT},
218yang menampilkan UI picker yang dikontrol oleh sistem yang memungkinkan pengguna
219menyusuri semua file yang disediakan aplikasi lain. Dari satu UI ini, pengguna
220bisa mengambil file dari aplikasi apa saja yang didukung.</p>
221
222<p>{@link android.content.Intent#ACTION_OPEN_DOCUMENT}
223tidak dimaksudkan untuk menjadi pengganti {@link android.content.Intent#ACTION_GET_CONTENT}.
224Yang harus Anda gunakan bergantung pada kebutuhan aplikasi:</p>
225
226<ul>
227<li>Gunakan {@link android.content.Intent#ACTION_GET_CONTENT} jika Anda ingin aplikasi
228cuma membaca/mengimpor data. Dengan pendekatan ini, aplikasi akan mengimpor salinan data,
229misalnya file gambar.</li>
230
231<li>Gunakan {@link android.content.Intent#ACTION_OPEN_DOCUMENT} jika Anda ingin aplikasi
232memiliki akses jangka panjang dan jangka pendek ke dokumen yang dimiliki oleh penyedia
233dokumen. Contohnya adalah aplikasi pengeditan foto yang memungkinkan pengguna mengedit
234gambar yang tersimpan dalam penyedia dokumen. </li>
235
236</ul>
237
238
239<p>Bagian ini menjelaskan cara menulis aplikasi klien berdasarkan
240{@link android.content.Intent#ACTION_OPEN_DOCUMENT} dan
241intent {@link android.content.Intent#ACTION_CREATE_DOCUMENT}.</p>
242
243
244<h3 id="search">Mencari dokumen</h3>
245
246<p>
247Cuplikan berikut menggunakan {@link android.content.Intent#ACTION_OPEN_DOCUMENT}
248untuk mencari penyedia dokumen yang
249berisi file gambar:</p>
250
251<pre>private static final int READ_REQUEST_CODE = 42;
252...
253/**
254 * Fires an intent to spin up the &quot;file chooser&quot; UI and select an image.
255 */
256public void performFileSearch() {
257
258    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
259    // browser.
260    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
261
262    // Filter to only show results that can be &quot;opened&quot;, such as a
263    // file (as opposed to a list of contacts or timezones)
264    intent.addCategory(Intent.CATEGORY_OPENABLE);
265
266    // Filter to show only images, using the image MIME data type.
267    // If one wanted to search for ogg vorbis files, the type would be &quot;audio/ogg&quot;.
268    // To search for all documents available via installed storage providers,
269    // it would be &quot;*/*&quot;.
270    intent.setType(&quot;image/*&quot;);
271
272    startActivityForResult(intent, READ_REQUEST_CODE);
273}</pre>
274
275<p>Perhatikan yang berikut ini:</p>
276<ul>
277<li>Saat aplikasi mengeluarkan intent {@link android.content.Intent#ACTION_OPEN_DOCUMENT}
278, aplikasi akan menjalankan picker yang menampilkan semua penyedia dokumen yang cocok.</li>
279
280<li>Menambahkan kategori {@link android.content.Intent#CATEGORY_OPENABLE} ke
281intent akan menyaring hasil agar hanya menampilkan dokumen yang bisa dibuka, seperti file gambar.</li>
282
283<li>Pernyataan {@code intent.setType("image/*")} menyaring lebih jauh agar hanya
284menampilkan dokumen yang memiliki tipe data MIME gambar.</li>
285</ul>
286
287<h3 id="results">Memproses Hasil</h3>
288
289<p>Setelah pengguna memilih dokumen di picker,
290{@link android.app.Activity#onActivityResult onActivityResult()} akan dipanggil.
291URI yang menunjuk ke dokumen yang dipilih dimasukkan dalam parameter {@code resultData}
292. Ekstrak URI dengan {@link android.content.Intent#getData getData()}.
293Setelah mendapatkannya, Anda bisa menggunakannya untuk mengambil dokumen yang diinginkan pengguna. Misalnya
294:</p>
295
296<pre>&#64;Override
297public void onActivityResult(int requestCode, int resultCode,
298        Intent resultData) {
299
300    // The ACTION_OPEN_DOCUMENT intent was sent with the request code
301    // READ_REQUEST_CODE. If the request code seen here doesn't match, it's the
302    // response to some other intent, and the code below shouldn't run at all.
303
304    if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
305        // The document selected by the user won't be returned in the intent.
306        // Instead, a URI to that document will be contained in the return intent
307        // provided to this method as a parameter.
308        // Pull that URI using resultData.getData().
309        Uri uri = null;
310        if (resultData != null) {
311            uri = resultData.getData();
312            Log.i(TAG, "Uri: " + uri.toString());
313            showImage(uri);
314        }
315    }
316}
317</pre>
318
319<h3 id="metadata">Memeriksa metadata dokumen</h3>
320
321<p>Setelah Anda memiliki URI untuk dokumen, Anda akan mendapatkan akses ke metadatanya. Cuplikan
322ini memegang metadata sebuah dokumen yang disebutkan oleh URI, dan mencatatnya:</p>
323
324<pre>public void dumpImageMetaData(Uri uri) {
325
326    // The query, since it only applies to a single document, will only return
327    // one row. There's no need to filter, sort, or select fields, since we want
328    // all fields for one document.
329    Cursor cursor = getActivity().getContentResolver()
330            .query(uri, null, null, null, null, null);
331
332    try {
333    // moveToFirst() returns false if the cursor has 0 rows.  Very handy for
334    // &quot;if there's anything to look at, look at it&quot; conditionals.
335        if (cursor != null &amp;&amp; cursor.moveToFirst()) {
336
337            // Note it's called &quot;Display Name&quot;.  This is
338            // provider-specific, and might not necessarily be the file name.
339            String displayName = cursor.getString(
340                    cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
341            Log.i(TAG, &quot;Display Name: &quot; + displayName);
342
343            int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
344            // If the size is unknown, the value stored is null.  But since an
345            // int can't be null in Java, the behavior is implementation-specific,
346            // which is just a fancy term for &quot;unpredictable&quot;.  So as
347            // a rule, check if it's null before assigning to an int.  This will
348            // happen often:  The storage API allows for remote files, whose
349            // size might not be locally known.
350            String size = null;
351            if (!cursor.isNull(sizeIndex)) {
352                // Technically the column stores an int, but cursor.getString()
353                // will do the conversion automatically.
354                size = cursor.getString(sizeIndex);
355            } else {
356                size = &quot;Unknown&quot;;
357            }
358            Log.i(TAG, &quot;Size: &quot; + size);
359        }
360    } finally {
361        cursor.close();
362    }
363}
364</pre>
365
366<h3 id="open-client">Membuka dokumen</h3>
367
368<p>Setelah mendapatkan URI dokumen, Anda bisa membuka dokumen atau melakukan apa saja
369yang diinginkan padanya.</p>
370
371<h4>Bitmap</h4>
372
373<p>Berikut ini adalah contoh cara membuka {@link android.graphics.Bitmap}:</p>
374
375<pre>private Bitmap getBitmapFromUri(Uri uri) throws IOException {
376    ParcelFileDescriptor parcelFileDescriptor =
377            getContentResolver().openFileDescriptor(uri, "r");
378    FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
379    Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
380    parcelFileDescriptor.close();
381    return image;
382}
383</pre>
384
385<p>Perhatikan bahwa Anda tidak boleh melakukan operasi ini pada thread UI. Lakukan hal ini di latar belakang
386, dengan menggunakan {@link android.os.AsyncTask}. Setelah membuka bitmap, Anda
387bisa menampilkannya dalam {@link android.widget.ImageView}.
388</p>
389
390<h4>Mendapatkan InputStream</h4>
391
392<p>Berikut ini adalah contoh cara mendapatkan {@link java.io.InputStream} dari URI. Dalam cuplikan ini
393, baris-baris file dibaca ke dalam sebuah string:</p>
394
395<pre>private String readTextFromUri(Uri uri) throws IOException {
396    InputStream inputStream = getContentResolver().openInputStream(uri);
397    BufferedReader reader = new BufferedReader(new InputStreamReader(
398            inputStream));
399    StringBuilder stringBuilder = new StringBuilder();
400    String line;
401    while ((line = reader.readLine()) != null) {
402        stringBuilder.append(line);
403    }
404    fileInputStream.close();
405    parcelFileDescriptor.close();
406    return stringBuilder.toString();
407}
408</pre>
409
410<h3 id="create">Membuat dokumen baru</h3>
411
412<p>Aplikasi Anda bisa membuat dokumen baru dalam penyedia dokumen dengan menggunakan intent
413{@link android.content.Intent#ACTION_CREATE_DOCUMENT}
414. Untuk membuat file, Anda memberikan satu tipe MIME dan satu nama file pada intent, dan
415menjalankannya dengan kode permintaan yang unik. Selebihnya akan diurus untuk Anda:</p>
416
417
418<pre>
419// Here are some examples of how you might call this method.
420// The first parameter is the MIME type, and the second parameter is the name
421// of the file you are creating:
422//
423// createFile("text/plain", "foobar.txt");
424// createFile("image/png", "mypicture.png");
425
426// Unique request code.
427private static final int WRITE_REQUEST_CODE = 43;
428...
429private void createFile(String mimeType, String fileName) {
430    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
431
432    // Filter to only show results that can be &quot;opened&quot;, such as
433    // a file (as opposed to a list of contacts or timezones).
434    intent.addCategory(Intent.CATEGORY_OPENABLE);
435
436    // Create a file with the requested MIME type.
437    intent.setType(mimeType);
438    intent.putExtra(Intent.EXTRA_TITLE, fileName);
439    startActivityForResult(intent, WRITE_REQUEST_CODE);
440}
441</pre>
442
443<p>Setelah membuat dokumen baru, Anda bisa mendapatkan URI-nya dalam
444{@link android.app.Activity#onActivityResult onActivityResult()}, sehingga Anda
445bisa terus menulis ke dokumen itu.</p>
446
447<h3 id="delete">Menghapus dokumen</h3>
448
449<p>Jika Anda memiliki URI dokumen dan
450{@link android.provider.DocumentsContract.Document#COLUMN_FLAGS Document.COLUMN_FLAGS}
451 dokumen berisi
452{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE SUPPORTS_DELETE},
453Anda bisa menghapus dokumen tersebut. Misalnya:</p>
454
455<pre>
456DocumentsContract.deleteDocument(getContentResolver(), uri);
457</pre>
458
459<h3 id="edit">Mengedit dokumen</h3>
460
461<p>Anda bisa menggunakan SAF untuk mengedit dokumen teks langsung di tempatnya.
462Cuplikan ini memicu
463intent {@link android.content.Intent#ACTION_OPEN_DOCUMENT} dan menggunakan
464kategori {@link android.content.Intent#CATEGORY_OPENABLE} untuk menampilkan
465dokumen yang bisa dibuka saja. Ini akan menyaring lebih jauh untuk menampilkan file teks saja:</p>
466
467<pre>
468private static final int EDIT_REQUEST_CODE = 44;
469/**
470 * Open a file for writing and append some text to it.
471 */
472 private void editDocument() {
473    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's
474    // file browser.
475    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
476
477    // Filter to only show results that can be &quot;opened&quot;, such as a
478    // file (as opposed to a list of contacts or timezones).
479    intent.addCategory(Intent.CATEGORY_OPENABLE);
480
481    // Filter to show only text files.
482    intent.setType(&quot;text/plain&quot;);
483
484    startActivityForResult(intent, EDIT_REQUEST_CODE);
485}
486</pre>
487
488<p>Berikutnya, dari {@link android.app.Activity#onActivityResult onActivityResult()}
489(lihat <a href="#results">Memproses hasil</a>) Anda bisa memanggil kode untuk mengedit.
490Cuplikan berikut mendapatkan {@link java.io.FileOutputStream}
491dari {@link android.content.ContentResolver}. Secara default, snipet menggunakan mode “tulis”.
492Inilah praktik terbaik untuk meminta jumlah akses minimum yang Anda perlukan, jadi jangan meminta
493baca/tulis jika yang Anda perlukan hanyalah tulis:</p>
494
495<pre>private void alterDocument(Uri uri) {
496    try {
497        ParcelFileDescriptor pfd = getActivity().getContentResolver().
498                openFileDescriptor(uri, "w");
499        FileOutputStream fileOutputStream =
500                new FileOutputStream(pfd.getFileDescriptor());
501        fileOutputStream.write(("Overwritten by MyCloud at " +
502                System.currentTimeMillis() + "\n").getBytes());
503        // Let the document provider know you're done by closing the stream.
504        fileOutputStream.close();
505        pfd.close();
506    } catch (FileNotFoundException e) {
507        e.printStackTrace();
508    } catch (IOException e) {
509        e.printStackTrace();
510    }
511}</pre>
512
513<h3 id="permissions">Mempertahankan izin</h3>
514
515<p>Bila aplikasi Anda membuka file untuk membaca atau menulis, sistem akan memberi
516aplikasi Anda izin URI untuk file itu. Pemberian ini berlaku hingga perangkat pengguna di-restart.
517Namun anggaplah aplikasi Anda adalah aplikasi pengeditan gambar, dan Anda ingin pengguna bisa
518mengakses 5 gambar terakhir yang dieditnya, langsung dari aplikasi Anda. Jika perangkat pengguna telah
519di-restart, maka Anda harus mengirim pengguna kembali ke picker sistem untuk menemukan
520file, hal ini jelas tidak ideal.</p>
521
522<p>Untuk mencegah terjadinya hal ini, Anda bisa mempertahankan izin yang diberikan
523sistem ke aplikasi Anda. Secara efektif, aplikasi Anda akan "mengambil" pemberian izin URI yang bisa dipertahankan
524yang ditawarkan oleh sistem. Hal ini memberi pengguna akses kontinu ke file
525melalui aplikasi Anda, sekalipun perangkat telah di-restart:</p>
526
527
528<pre>final int takeFlags = intent.getFlags()
529            &amp; (Intent.FLAG_GRANT_READ_URI_PERMISSION
530            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
531// Check for the freshest data.
532getContentResolver().takePersistableUriPermission(uri, takeFlags);</pre>
533
534<p>Ada satu langkah akhir. Anda mungkin telah menyimpan
535URI terbaru yang diakses aplikasi, namun URI itu mungkin tidak lagi valid,&mdash;aplikasi lain
536mungkin telah menghapus atau memodifikasi dokumen. Karena itu, Anda harus selalu memanggil
537{@code getContentResolver().takePersistableUriPermission()} untuk memeriksa
538data terbaru.</p>
539
540<h2 id="custom">Menulis Penyedia Dokumen Custom</h2>
541
542<p>
543Jika Anda sedang mengembangkan aplikasi yang menyediakan layanan penyimpanan untuk file (misalnya
544layanan penyimpanan cloud), Anda bisa menyediakan file melalui
545SAF dengan menulis penyedia dokumen custom.  Bagian ini menjelaskan
546caranya.</p>
547
548
549<h3 id="manifest">Manifes</h3>
550
551<p>Untuk mengimplementasikan penyedia dokumen custom, tambahkan yang berikut ini ke manifes aplikasi
552Anda:</p>
553<ul>
554
555<li>Target berupa API level 19 atau yang lebih tinggi.</li>
556
557<li>Elemen <code>&lt;provider&gt;</code> yang mendeklarasikan penyedia penyimpanan custom
558Anda. </li>
559
560<li>Nama penyedia Anda, yaitu nama kelasnya, termasuk nama paket.
561Misalnya: <code>com.example.android.storageprovider.MyCloudProvider</code>.</li>
562
563<li>Nama otoritas Anda, yaitu nama paket Anda (dalam contoh ini,
564<code>com.example.android.storageprovider</code>) plus tipe penyedia konten
565(<code>documents</code>). Misalnya, {@code com.example.android.storageprovider.documents}.</li>
566
567<li>Atribut <code>android:exported</code> yang diatur ke <code>&quot;true&quot;</code>.
568Anda harus mengekspor penyedia sehingga aplikasi lain bisa membacanya.</li>
569
570<li>Atribut <code>android:grantUriPermissions</code> yang diatur ke
571<code>&quot;true&quot;</code>. Pengaturan ini memungkinkan sistem memberi aplikasi lain akses
572ke konten dalam penyedia Anda. Untuk pembahasan cara mempertahankan pemberian bagi
573dokumen tertentu, lihat <a href="#permissions">Mempertahankan izin</a>.</li>
574
575<li>Izin {@code MANAGE_DOCUMENTS}. Secara default, penyedia tersedia
576bagi siapa saja. Menambahkan izin ini akan membatasi penyedia Anda pada sistem.
577Pembatasan ini penting untuk keamanan.</li>
578
579<li>Atribut {@code android:enabled} yang diatur ke nilai boolean didefinisikan dalam file
580sumber daya. Tujuan atribut ini adalah menonaktifkan penyedia pada perangkat yang menjalankan Android 4.3 atau yang lebih rendah.
581Misalnya, {@code android:enabled="@bool/atLeastKitKat"}. Selain
582memasukkan atribut ini dalam manifes, Anda perlu melakukan hal-hal berikut:
583<ul>
584<li>Dalam file sumber daya {@code bool.xml} Anda di bawah {@code res/values/}, tambahkan
585baris ini: <pre>&lt;bool name=&quot;atLeastKitKat&quot;&gt;false&lt;/bool&gt;</pre></li>
586
587<li>Dalam file sumber daya {@code bool.xml} Anda di bawah {@code res/values-v19/}, tambahkan
588baris ini: <pre>&lt;bool name=&quot;atLeastKitKat&quot;&gt;true&lt;/bool&gt;</pre></li>
589</ul></li>
590
591<li>Sebuah filter intent berisi tindakan
592{@code android.content.action.DOCUMENTS_PROVIDER}, agar penyedia Anda
593muncul dalam picker saat sistem mencari penyedia.</li>
594
595</ul>
596<p>Berikut ini adalah kutipan contoh manifes berisi penyedia yang:</p>
597
598<pre>&lt;manifest... &gt;
599    ...
600    &lt;uses-sdk
601        android:minSdkVersion=&quot;19&quot;
602        android:targetSdkVersion=&quot;19&quot; /&gt;
603        ....
604        &lt;provider
605            android:name=&quot;com.example.android.storageprovider.MyCloudProvider&quot;
606            android:authorities=&quot;com.example.android.storageprovider.documents&quot;
607            android:grantUriPermissions=&quot;true&quot;
608            android:exported=&quot;true&quot;
609            android:permission=&quot;android.permission.MANAGE_DOCUMENTS&quot;
610            android:enabled=&quot;&#64;bool/atLeastKitKat&quot;&gt;
611            &lt;intent-filter&gt;
612                &lt;action android:name=&quot;android.content.action.DOCUMENTS_PROVIDER&quot; /&gt;
613            &lt;/intent-filter&gt;
614        &lt;/provider&gt;
615    &lt;/application&gt;
616
617&lt;/manifest&gt;</pre>
618
619<h4 id="43">Mendukung perangkat yang menjalankan Android 4.3 dan yang lebih rendah</h4>
620
621<p>Intent
622{@link android.content.Intent#ACTION_OPEN_DOCUMENT} hanya tersedia
623pada perangkat yang menjalankan Android 4.4 dan yang lebih tinggi.
624Jika ingin aplikasi Anda mendukung {@link android.content.Intent#ACTION_GET_CONTENT}
625untuk mengakomodasi perangkat yang menjalankan Android 4.3 dan yang lebih rendah, Anda harus
626menonaktifkan filter inten {@link android.content.Intent#ACTION_GET_CONTENT} dalam
627manifes untuk perangkat yang menjalankan Android 4.4 atau yang lebih tinggi. Penyedia
628dokumen dan {@link android.content.Intent#ACTION_GET_CONTENT} harus dianggap
629saling eksklusif. Jika Anda mendukung keduanya sekaligus, aplikasi Anda akan
630muncul dua kali dalam UI picker sistem, yang menawarkan dua cara mengakses
631data tersimpan Anda. Hal ini akan membingungkan pengguna.</p>
632
633<p>Berikut ini adalah cara yang disarankan untuk menonaktifkan
634filter intent {@link android.content.Intent#ACTION_GET_CONTENT} untuk perangkat
635yang menjalankan Android versi 4.4 atau yang lebih tinggi:</p>
636
637<ol>
638<li>Dalam file sumber daya {@code bool.xml} Anda di bawah {@code res/values/}, tambahkan
639baris ini: <pre>&lt;bool name=&quot;atMostJellyBeanMR2&quot;&gt;true&lt;/bool&gt;</pre></li>
640
641<li>Dalam file sumber daya {@code bool.xml} Anda di bawah {@code res/values-v19/}, tambahkan
642baris ini: <pre>&lt;bool name=&quot;atMostJellyBeanMR2&quot;&gt;false&lt;/bool&gt;</pre></li>
643
644<li>Tambahkan
645<a href="{@docRoot}guide/topics/manifest/activity-alias-element.html">alias
646aktivitas</a> untuk menonaktifkan filter intent {@link android.content.Intent#ACTION_GET_CONTENT}
647bagi versi 4.4 (API level 19) dan yang lebih tinggi. Misalnya:
648
649<pre>
650&lt;!-- This activity alias is added so that GET_CONTENT intent-filter
651     can be disabled for builds on API level 19 and higher. --&gt;
652&lt;activity-alias android:name=&quot;com.android.example.app.MyPicker&quot;
653        android:targetActivity=&quot;com.android.example.app.MyActivity&quot;
654        ...
655        android:enabled=&quot;@bool/atMostJellyBeanMR2&quot;&gt;
656    &lt;intent-filter&gt;
657        &lt;action android:name=&quot;android.intent.action.GET_CONTENT&quot; /&gt;
658        &lt;category android:name=&quot;android.intent.category.OPENABLE&quot; /&gt;
659        &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; /&gt;
660        &lt;data android:mimeType=&quot;image/*&quot; /&gt;
661        &lt;data android:mimeType=&quot;video/*&quot; /&gt;
662    &lt;/intent-filter&gt;
663&lt;/activity-alias&gt;
664</pre>
665</li>
666</ol>
667<h3 id="contract">Kontrak</h3>
668
669<p>Biasanya bila Anda menulis penyedia konten custom, salah satu tugas adalah
670mengimplementasikan kelas kontrak, seperti dijelaskan dalam panduan pengembang
671<a href="{@docRoot}guide/topics/providers/content-provider-creating.html#ContractClass">
672Penyedia Konten</a>. Kelas kontrak adalah kelas {@code public final}
673yang berisi definisi konstanta untuk URI, nama kolom, tipe MIME, dan
674metadata lain yang berkenaan dengan penyedia. SAF
675menyediakan kelas-kelas kontrak ini untuk Anda, jadi Anda tidak perlu menulisnya
676sendiri:</p>
677
678<ul>
679   <li>{@link android.provider.DocumentsContract.Document}</li>
680   <li>{@link android.provider.DocumentsContract.Root}</li>
681</ul>
682
683<p>Misalnya, berikut ini adalah kolom-kolom yang bisa Anda hasilkan di kursor bila
684penyedia dokumen Anda membuat query dokumen atau akar:</p>
685
686<pre>private static final String[] DEFAULT_ROOT_PROJECTION =
687        new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES,
688        Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
689        Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
690        Root.COLUMN_AVAILABLE_BYTES,};
691private static final String[] DEFAULT_DOCUMENT_PROJECTION = new
692        String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
693        Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
694        Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};
695</pre>
696
697<h3 id="subclass">Subkelas DocumentsProvider</h3>
698
699<p>Langkah berikutnya dalam menulis penyedia dokumen custom adalah menjadikan
700kelas abstrak sebagai subkelas {@link android.provider.DocumentsProvider}. Setidaknya, Anda perlu
701 mengimplementasikan metode berikut:</p>
702
703<ul>
704<li>{@link android.provider.DocumentsProvider#queryRoots queryRoots()}</li>
705
706<li>{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}</li>
707
708<li>{@link android.provider.DocumentsProvider#queryDocument queryDocument()}</li>
709
710<li>{@link android.provider.DocumentsProvider#openDocument openDocument()}</li>
711</ul>
712
713<p>Hanya inilah metode yang diwajibkan kepada Anda secara ketat untuk diimplementasikan, namun ada
714banyak lagi yang mungkin Anda inginkan. Lihat {@link android.provider.DocumentsProvider}
715untuk detailnya.</p>
716
717<h4 id="queryRoots">Mengimplementasikan queryRoots</h4>
718
719<p>Implementasi {@link android.provider.DocumentsProvider#queryRoots
720queryRoots()} oleh Anda harus menghasilkan {@link android.database.Cursor} yang menunjuk ke semua
721direktori akar penyedia dokumen, dengan menggunakan kolom-kolom yang didefinisikan dalam
722{@link android.provider.DocumentsContract.Root}.</p>
723
724<p>Dalam cuplikan berikut, parameter {@code projection} mewakili bidang-bidang
725tertentu yang ingin didapatkan kembali oleh pemanggil. Cuplikan ini membuat kursor baru
726dan menambahkan satu baris ke satu akar&mdash; kursor, satu direktori level atas, seperti
727Downloads atau Images.  Kebanyakan penyedia hanya mempunyai satu akar. Anda bisa mempunyai lebih dari satu,
728misalnya, jika ada banyak akun pengguna. Dalam hal itu, cukup tambahkan sebuah
729baris kedua ke kursor.</p>
730
731<pre>
732&#64;Override
733public Cursor queryRoots(String[] projection) throws FileNotFoundException {
734
735    // Create a cursor with either the requested fields, or the default
736    // projection if "projection" is null.
737    final MatrixCursor result =
738            new MatrixCursor(resolveRootProjection(projection));
739
740    // If user is not logged in, return an empty root cursor.  This removes our
741    // provider from the list entirely.
742    if (!isUserLoggedIn()) {
743        return result;
744    }
745
746    // It's possible to have multiple roots (e.g. for multiple accounts in the
747    // same app) -- just add multiple cursor rows.
748    // Construct one row for a root called &quot;MyCloud&quot;.
749    final MatrixCursor.RowBuilder row = result.newRow();
750    row.add(Root.COLUMN_ROOT_ID, ROOT);
751    row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary));
752
753    // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
754    // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
755    // recently used documents will show up in the &quot;Recents&quot; category.
756    // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
757    // shares.
758    row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
759            Root.FLAG_SUPPORTS_RECENTS |
760            Root.FLAG_SUPPORTS_SEARCH);
761
762    // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
763    row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title));
764
765    // This document id cannot change once it's shared.
766    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir));
767
768    // The child MIME types are used to filter the roots and only present to the
769    //  user roots that contain the desired type somewhere in their file hierarchy.
770    row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(mBaseDir));
771    row.add(Root.COLUMN_AVAILABLE_BYTES, mBaseDir.getFreeSpace());
772    row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);
773
774    return result;
775}</pre>
776
777<h4 id="queryChildDocuments">Mengimplementasikan queryChildDocuments</h4>
778
779<p>Implementasi
780{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}
781oleh Anda harus menghasilkan {@link android.database.Cursor} yang menunjuk ke semua file dalam
782direktori yang ditentukan, dengan menggunakan kolom-kolom yang didefinisikan dalam
783{@link android.provider.DocumentsContract.Document}.</p>
784
785<p>Metode ini akan dipanggil bila Anda memilih akar aplikasi dalam picker UI.
786Metode mengambil dokumen anak dari direktori di bawah akar.  Metode ini bisa dipanggil pada level apa saja dalam
787hierarki file, bukan hanya akar. Cuplikan ini
788membuat kursor baru dengan kolom-kolom yang diminta, lalu menambahkan informasi tentang
789setiap anak langsung dalam direktori induk ke kursor.
790Satu anak bisa berupa gambar, direktori lain&mdash;file apa saja:</p>
791
792<pre>&#64;Override
793public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
794                              String sortOrder) throws FileNotFoundException {
795
796    final MatrixCursor result = new
797            MatrixCursor(resolveDocumentProjection(projection));
798    final File parent = getFileForDocId(parentDocumentId);
799    for (File file : parent.listFiles()) {
800        // Adds the file's display name, MIME type, size, and so on.
801        includeFile(result, null, file);
802    }
803    return result;
804}
805</pre>
806
807<h4 id="queryDocument">Mengimplementasikan queryDocument</h4>
808
809<p>Implementasi
810{@link android.provider.DocumentsProvider#queryDocument queryDocument()}
811oleh Anda harus menghasilkan {@link android.database.Cursor} yang menunjuk ke file yang disebutkan,
812dengan menggunakan kolom-kolom yang didefinisikan dalam {@link android.provider.DocumentsContract.Document}.
813</p>
814
815<p>Metode {@link android.provider.DocumentsProvider#queryDocument queryDocument()}
816menghasilkan informasi yang sama yang diteruskan dalam
817{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()},
818namun untuk file tertentu:</p>
819
820
821<pre>&#64;Override
822public Cursor queryDocument(String documentId, String[] projection) throws
823        FileNotFoundException {
824
825    // Create a cursor with the requested projection, or the default projection.
826    final MatrixCursor result = new
827            MatrixCursor(resolveDocumentProjection(projection));
828    includeFile(result, documentId, null);
829    return result;
830}
831</pre>
832
833<h4 id="openDocument">Mengimplementasikan openDocument</h4>
834
835<p>Anda harus mengimplementasikan {@link android.provider.DocumentsProvider#openDocument
836openDocument()} untuk menghasilkan {@link android.os.ParcelFileDescriptor} yang mewakili
837file yang disebutkan. Aplikasi lain bisa menggunakan {@link android.os.ParcelFileDescriptor}
838yang dihasilkan untuk mengalirkan data. Sistem memanggil metode ini setelah pengguna memilih file
839dan aplikasi klien meminta akses ke file itu dengan memanggil
840{@link android.content.ContentResolver#openFileDescriptor openFileDescriptor()}.
841Misalnya:</p>
842
843<pre>&#64;Override
844public ParcelFileDescriptor openDocument(final String documentId,
845                                         final String mode,
846                                         CancellationSignal signal) throws
847        FileNotFoundException {
848    Log.v(TAG, &quot;openDocument, mode: &quot; + mode);
849    // It's OK to do network operations in this method to download the document,
850    // as long as you periodically check the CancellationSignal. If you have an
851    // extremely large file to transfer from the network, a better solution may
852    // be pipes or sockets (see ParcelFileDescriptor for helper methods).
853
854    final File file = getFileForDocId(documentId);
855
856    final boolean isWrite = (mode.indexOf('w') != -1);
857    if(isWrite) {
858        // Attach a close listener if the document is opened in write mode.
859        try {
860            Handler handler = new Handler(getContext().getMainLooper());
861            return ParcelFileDescriptor.open(file, accessMode, handler,
862                        new ParcelFileDescriptor.OnCloseListener() {
863                &#64;Override
864                public void onClose(IOException e) {
865
866                    // Update the file with the cloud server. The client is done
867                    // writing.
868                    Log.i(TAG, &quot;A file with id &quot; +
869                    documentId + &quot; has been closed!
870                    Time to &quot; +
871                    &quot;update the server.&quot;);
872                }
873
874            });
875        } catch (IOException e) {
876            throw new FileNotFoundException(&quot;Failed to open document with id &quot;
877            + documentId + &quot; and mode &quot; + mode);
878        }
879    } else {
880        return ParcelFileDescriptor.open(file, accessMode);
881    }
882}
883</pre>
884
885<h3 id="security">Keamanan</h3>
886
887<p>Anggaplah penyedia dokumen Anda sebuah layanan penyimpanan cloud yang dilindungi kata sandi
888dan Anda ingin memastikan bahwa pengguna sudah login sebelum Anda mulai berbagi file mereka.
889Apakah yang harus dilakukan aplikasi Anda jika pengguna tidak login?  Solusinya adalah menghasilkan
890akar nol dalam implementasi {@link android.provider.DocumentsProvider#queryRoots
891queryRoots()} Anda. Yakni, sebuah kursor akar kosong:</p>
892
893<pre>
894public Cursor queryRoots(String[] projection) throws FileNotFoundException {
895...
896    // If user is not logged in, return an empty root cursor.  This removes our
897    // provider from the list entirely.
898    if (!isUserLoggedIn()) {
899        return result;
900}
901</pre>
902
903<p>Langkah lainnya adalah memanggil {@code getContentResolver().notifyChange()}.
904Ingat {@link android.provider.DocumentsContract}?  Kita menggunakannya untuk membuat
905URI ini. Cuplikan berikut memberi tahu sistem untuk membuat query akar penyedia dokumen Anda
906kapan saja status login pengguna berubah. Jika pengguna tidak
907login, panggilan ke {@link android.provider.DocumentsProvider#queryRoots queryRoots()} akan menghasilkan
908kursor kosong, seperti yang ditampilkan di atas. Cara ini akan memastikan bahwa dokumen penyedia hanya
909tersedia jika pengguna login ke penyedia itu.</p>
910
911<pre>private void onLoginButtonClick() {
912    loginOrLogout();
913    getContentResolver().notifyChange(DocumentsContract
914            .buildRootsUri(AUTHORITY), null);
915}
916</pre>