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>—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>—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>—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—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 "file chooser" 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 "opened", 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 "audio/ogg". 268 // To search for all documents available via installed storage providers, 269 // it would be "*/*". 270 intent.setType("image/*"); 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>@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 // "if there's anything to look at, look at it" conditionals. 335 if (cursor != null && cursor.moveToFirst()) { 336 337 // Note it's called "Display Name". 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, "Display Name: " + 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 "unpredictable". 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 = "Unknown"; 357 } 358 Log.i(TAG, "Size: " + 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 "opened", 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 "opened", 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("text/plain"); 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 & (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,—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><provider></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>"true"</code>. 568Anda harus mengekspor penyedia sehingga aplikasi lain bisa membacanya.</li> 569 570<li>Atribut <code>android:grantUriPermissions</code> yang diatur ke 571<code>"true"</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><bool name="atLeastKitKat">false</bool></pre></li> 586 587<li>Dalam file sumber daya {@code bool.xml} Anda di bawah {@code res/values-v19/}, tambahkan 588baris ini: <pre><bool name="atLeastKitKat">true</bool></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><manifest... > 599 ... 600 <uses-sdk 601 android:minSdkVersion="19" 602 android:targetSdkVersion="19" /> 603 .... 604 <provider 605 android:name="com.example.android.storageprovider.MyCloudProvider" 606 android:authorities="com.example.android.storageprovider.documents" 607 android:grantUriPermissions="true" 608 android:exported="true" 609 android:permission="android.permission.MANAGE_DOCUMENTS" 610 android:enabled="@bool/atLeastKitKat"> 611 <intent-filter> 612 <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> 613 </intent-filter> 614 </provider> 615 </application> 616 617</manifest></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><bool name="atMostJellyBeanMR2">true</bool></pre></li> 640 641<li>Dalam file sumber daya {@code bool.xml} Anda di bawah {@code res/values-v19/}, tambahkan 642baris ini: <pre><bool name="atMostJellyBeanMR2">false</bool></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<!-- This activity alias is added so that GET_CONTENT intent-filter 651 can be disabled for builds on API level 19 and higher. --> 652<activity-alias android:name="com.android.example.app.MyPicker" 653 android:targetActivity="com.android.example.app.MyActivity" 654 ... 655 android:enabled="@bool/atMostJellyBeanMR2"> 656 <intent-filter> 657 <action android:name="android.intent.action.GET_CONTENT" /> 658 <category android:name="android.intent.category.OPENABLE" /> 659 <category android:name="android.intent.category.DEFAULT" /> 660 <data android:mimeType="image/*" /> 661 <data android:mimeType="video/*" /> 662 </intent-filter> 663</activity-alias> 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— 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@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 "MyCloud". 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 "Recents" 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—file apa saja:</p> 791 792<pre>@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>@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>@Override 844public ParcelFileDescriptor openDocument(final String documentId, 845 final String mode, 846 CancellationSignal signal) throws 847 FileNotFoundException { 848 Log.v(TAG, "openDocument, mode: " + 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 @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, "A file with id " + 869 documentId + " has been closed! 870 Time to " + 871 "update the server."); 872 } 873 874 }); 875 } catch (IOException e) { 876 throw new FileNotFoundException("Failed to open document with id " 877 + documentId + " and mode " + 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>