1 page.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"> 50 DevBytes: Android 4.4 Storage Access Framework: Penyedia</a></li> 51 <li><a href="http://www.youtube.com/watch?v=UFj9AEz0DHQ"> 52 DevBytes: 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"> 60 Penyedia Penyimpanan</a></li> 61 <li><a href="{@docRoot}samples/StorageClient/index.html"> 62 Klien 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 80 di semua penyedia penyimpanan dokumen pilihannya. UI standar yang mudah digunakan 81 memungkinkan 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 85 yang memerlukan akses ke dokumen sebuah penyedia bisa berintegrasi dengan SAF cukup dengan beberapa 86 baris kode.</p> 87 88 <p>SAF terdiri dari berikut ini:</p> 89 90 <ul> 91 <li><strong>Penyedia dokumen</strong>—Penyedia konten yang memungkinkan 92 layanan penyimpanan (seperti Google Drive) untuk menampilkan file yang dikelolanya. Penyedia dokumen 93 diimplementasikan sebagai subkelas dari kelas {@link android.provider.DocumentsProvider}. 94 Skema penyedia dokumen berdasarkan hierarki file biasa, 95 walaupun cara penyedia dokumen Anda secara fisik menyimpan data adalah terserah Anda. 96 Platform Android terdiri dari beberapa penyedia dokumen bawaan, seperti 97 Downloads, 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 102 file yang dihasilkan penyedia dokumen.</li> 103 104 <li><strong>Picker</strong>—UI sistem yang memungkinkan pengguna mengakses dokumen dari semua 105 penyedia 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 115 USB, 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 121 subkelas dari kelas {@link android.provider.DocumentsProvider}. Dalam <em>penyedia dokumen</em>, data 122 distrukturkan 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, 125 yang 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. 132 Masing-masing akar memiliki sebuah {@link android.provider.DocumentsContract.Root#COLUMN_ROOT_ID} yang unik, 133 dan menunjuk ke satu dokumen (satu direktori) 134 yang mewakili konten di bawah akar itu. 135 Akar sengaja dibuat dinamis untuk mendukung kasus penggunaan seperti multiakun, 136 perangkat 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>, 139 yang nanti masing-masing bisa menunjuk ke dokumen 1-ke-<em>N</em>. </li> 140 141 <li>Tiap backend penyimpanan memunculkan 142 masing-masing file dan direktori dengan mengacunya lewat sebuah 143 {@link android.provider.DocumentsContract.Document#COLUMN_DOCUMENT_ID} yang unik. 144 ID dokumen harus unik dan tidak berubah setelah dibuat, karena ID ini digunakan untuk 145 URI persisten yang diberikan pada saat reboot perangkat.</li> 146 147 148 <li>Dokumen bisa berupa file yang bisa dibuka (dengan tipe MIME tertentu), atau 149 direktori 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}. 154 Misalnya, {@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 158 dimasukkan 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 163 biasa. Akan tetapi, Anda bisa menyimpan secara fisik data dengan cara apa pun yang disukai, 164 selama data bisa diakses melalui API {@link android.provider.DocumentsProvider}. Misalnya, Anda 165 bisa menggunakan penyimpanan cloud berbasis tag untuk data Anda.</p> 166 167 <p>Gambar 2 menampilkan contoh cara aplikasi foto bisa menggunakan SAF 168 untuk 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 177 secara langsung. Klien meminta izin untuk berinteraksi 178 dengan 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 182 untuk mempersempit kriteria—misalnya, "beri saya semua file yang bisa dibuka 183 yang memiliki tipe MIME 'gambar'".</li> 184 185 <li>Setelah intent dibuat, picker sistem akan pergi ke setiap penyedia yang terdaftar 186 dan menunjukkan kepada pengguna akar konten yang cocok.</li> 187 188 <li>Picker memberi pengguna antarmuka standar untuk mengakses dokumen, 189 walaupun penyedia dokumen dasar bisa sangat berbeda. Misalnya, gambar 2 190 menunjukkan 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 194 akun 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 201 gambar 4. Dari titik itu, pengguna bisa berinteraksi dengan gambar dengan cara apa pun 202 yang 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 211 aplikasi lain, aplikasi Anda harus memanggil intent seperti {@link android.content.Intent#ACTION_PICK} 212 atau {@link android.content.Intent#ACTION_GET_CONTENT}. Pengguna nanti harus memilih 213 satu aplikasi yang akan digunakan untuk mengambil file dan aplikasi yang dipilih harus menyediakan antarmuka pengguna 214 bagi 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}, 218 yang menampilkan UI picker yang dikontrol oleh sistem yang memungkinkan pengguna 219 menyusuri semua file yang disediakan aplikasi lain. Dari satu UI ini, pengguna 220 bisa mengambil file dari aplikasi apa saja yang didukung.</p> 221 222 <p>{@link android.content.Intent#ACTION_OPEN_DOCUMENT} 223 tidak dimaksudkan untuk menjadi pengganti {@link android.content.Intent#ACTION_GET_CONTENT}. 224 Yang 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 228 cuma membaca/mengimpor data. Dengan pendekatan ini, aplikasi akan mengimpor salinan data, 229 misalnya file gambar.</li> 230 231 <li>Gunakan {@link android.content.Intent#ACTION_OPEN_DOCUMENT} jika Anda ingin aplikasi 232 memiliki akses jangka panjang dan jangka pendek ke dokumen yang dimiliki oleh penyedia 233 dokumen. Contohnya adalah aplikasi pengeditan foto yang memungkinkan pengguna mengedit 234 gambar 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 241 intent {@link android.content.Intent#ACTION_CREATE_DOCUMENT}.</p> 242 243 244 <h3 id="search">Mencari dokumen</h3> 245 246 <p> 247 Cuplikan berikut menggunakan {@link android.content.Intent#ACTION_OPEN_DOCUMENT} 248 untuk mencari penyedia dokumen yang 249 berisi 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 */ 256 public 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 281 intent 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 284 menampilkan 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. 291 URI yang menunjuk ke dokumen yang dipilih dimasukkan dalam parameter {@code resultData} 292 . Ekstrak URI dengan {@link android.content.Intent#getData getData()}. 293 Setelah mendapatkannya, Anda bisa menggunakannya untuk mengambil dokumen yang diinginkan pengguna. Misalnya 294 :</p> 295 296 <pre>@Override 297 public 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 322 ini 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 369 yang 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 387 bisa 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 415 menjalankannya 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. 427 private static final int WRITE_REQUEST_CODE = 43; 428 ... 429 private 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 445 bisa 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}, 453 Anda bisa menghapus dokumen tersebut. Misalnya:</p> 454 455 <pre> 456 DocumentsContract.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. 462 Cuplikan ini memicu 463 intent {@link android.content.Intent#ACTION_OPEN_DOCUMENT} dan menggunakan 464 kategori {@link android.content.Intent#CATEGORY_OPENABLE} untuk menampilkan 465 dokumen yang bisa dibuka saja. Ini akan menyaring lebih jauh untuk menampilkan file teks saja:</p> 466 467 <pre> 468 private 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. 490 Cuplikan berikut mendapatkan {@link java.io.FileOutputStream} 491 dari {@link android.content.ContentResolver}. Secara default, snipet menggunakan mode tulis. 492 Inilah praktik terbaik untuk meminta jumlah akses minimum yang Anda perlukan, jadi jangan meminta 493 baca/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 516 aplikasi Anda izin URI untuk file itu. Pemberian ini berlaku hingga perangkat pengguna di-restart. 517 Namun anggaplah aplikasi Anda adalah aplikasi pengeditan gambar, dan Anda ingin pengguna bisa 518 mengakses 5 gambar terakhir yang dieditnya, langsung dari aplikasi Anda. Jika perangkat pengguna telah 519 di-restart, maka Anda harus mengirim pengguna kembali ke picker sistem untuk menemukan 520 file, hal ini jelas tidak ideal.</p> 521 522 <p>Untuk mencegah terjadinya hal ini, Anda bisa mempertahankan izin yang diberikan 523 sistem ke aplikasi Anda. Secara efektif, aplikasi Anda akan "mengambil" pemberian izin URI yang bisa dipertahankan 524 yang ditawarkan oleh sistem. Hal ini memberi pengguna akses kontinu ke file 525 melalui 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. 532 getContentResolver().takePersistableUriPermission(uri, takeFlags);</pre> 533 534 <p>Ada satu langkah akhir. Anda mungkin telah menyimpan 535 URI terbaru yang diakses aplikasi, namun URI itu mungkin tidak lagi valid,—aplikasi lain 536 mungkin telah menghapus atau memodifikasi dokumen. Karena itu, Anda harus selalu memanggil 537 {@code getContentResolver().takePersistableUriPermission()} untuk memeriksa 538 data terbaru.</p> 539 540 <h2 id="custom">Menulis Penyedia Dokumen Custom</h2> 541 542 <p> 543 Jika Anda sedang mengembangkan aplikasi yang menyediakan layanan penyimpanan untuk file (misalnya 544 layanan penyimpanan cloud), Anda bisa menyediakan file melalui 545 SAF dengan menulis penyedia dokumen custom. Bagian ini menjelaskan 546 caranya.</p> 547 548 549 <h3 id="manifest">Manifes</h3> 550 551 <p>Untuk mengimplementasikan penyedia dokumen custom, tambahkan yang berikut ini ke manifes aplikasi 552 Anda:</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 558 Anda. </li> 559 560 <li>Nama penyedia Anda, yaitu nama kelasnya, termasuk nama paket. 561 Misalnya: <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>. 568 Anda 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 572 ke konten dalam penyedia Anda. Untuk pembahasan cara mempertahankan pemberian bagi 573 dokumen tertentu, lihat <a href="#permissions">Mempertahankan izin</a>.</li> 574 575 <li>Izin {@code MANAGE_DOCUMENTS}. Secara default, penyedia tersedia 576 bagi siapa saja. Menambahkan izin ini akan membatasi penyedia Anda pada sistem. 577 Pembatasan ini penting untuk keamanan.</li> 578 579 <li>Atribut {@code android:enabled} yang diatur ke nilai boolean didefinisikan dalam file 580 sumber daya. Tujuan atribut ini adalah menonaktifkan penyedia pada perangkat yang menjalankan Android 4.3 atau yang lebih rendah. 581 Misalnya, {@code android:enabled="@bool/atLeastKitKat"}. Selain 582 memasukkan 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 585 baris 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 588 baris 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 593 muncul 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 623 pada perangkat yang menjalankan Android 4.4 dan yang lebih tinggi. 624 Jika ingin aplikasi Anda mendukung {@link android.content.Intent#ACTION_GET_CONTENT} 625 untuk mengakomodasi perangkat yang menjalankan Android 4.3 dan yang lebih rendah, Anda harus 626 menonaktifkan filter inten {@link android.content.Intent#ACTION_GET_CONTENT} dalam 627 manifes untuk perangkat yang menjalankan Android 4.4 atau yang lebih tinggi. Penyedia 628 dokumen dan {@link android.content.Intent#ACTION_GET_CONTENT} harus dianggap 629 saling eksklusif. Jika Anda mendukung keduanya sekaligus, aplikasi Anda akan 630 muncul dua kali dalam UI picker sistem, yang menawarkan dua cara mengakses 631 data tersimpan Anda. Hal ini akan membingungkan pengguna.</p> 632 633 <p>Berikut ini adalah cara yang disarankan untuk menonaktifkan 634 filter intent {@link android.content.Intent#ACTION_GET_CONTENT} untuk perangkat 635 yang 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 639 baris 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 642 baris 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 646 aktivitas</a> untuk menonaktifkan filter intent {@link android.content.Intent#ACTION_GET_CONTENT} 647 bagi 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 670 mengimplementasikan kelas kontrak, seperti dijelaskan dalam panduan pengembang 671 <a href="{@docRoot}guide/topics/providers/content-provider-creating.html#ContractClass"> 672 Penyedia Konten</a>. Kelas kontrak adalah kelas {@code public final} 673 yang berisi definisi konstanta untuk URI, nama kolom, tipe MIME, dan 674 metadata lain yang berkenaan dengan penyedia. SAF 675 menyediakan kelas-kelas kontrak ini untuk Anda, jadi Anda tidak perlu menulisnya 676 sendiri:</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 684 penyedia 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,}; 691 private 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 700 kelas 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 714 banyak lagi yang mungkin Anda inginkan. Lihat {@link android.provider.DocumentsProvider} 715 untuk detailnya.</p> 716 717 <h4 id="queryRoots">Mengimplementasikan queryRoots</h4> 718 719 <p>Implementasi {@link android.provider.DocumentsProvider#queryRoots 720 queryRoots()} oleh Anda harus menghasilkan {@link android.database.Cursor} yang menunjuk ke semua 721 direktori 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 725 tertentu yang ingin didapatkan kembali oleh pemanggil. Cuplikan ini membuat kursor baru 726 dan menambahkan satu baris ke satu akar— kursor, satu direktori level atas, seperti 727 Downloads atau Images. Kebanyakan penyedia hanya mempunyai satu akar. Anda bisa mempunyai lebih dari satu, 728 misalnya, jika ada banyak akun pengguna. Dalam hal itu, cukup tambahkan sebuah 729 baris kedua ke kursor.</p> 730 731 <pre> 732 @Override 733 public 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()} 781 olehAnda harus menghasilkan {@link android.database.Cursor} yang menunjuk ke semua file dalam 782 direktori 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. 786 Metode mengambil dokumen anak dari direktori di bawah akar. Metode ini bisa dipanggil pada level apa saja dalam 787 hierarki file, bukan hanya akar. Cuplikan ini 788 membuat kursor baru dengan kolom-kolom yang diminta, lalu menambahkan informasi tentang 789 setiap anak langsung dalam direktori induk ke kursor. 790 Satu anak bisa berupa gambar, direktori lain—file apa saja:</p> 791 792 <pre>@Override 793 public 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()} 811 oleh Anda harus menghasilkan {@link android.database.Cursor} yang menunjuk ke file yang disebutkan, 812 dengan menggunakan kolom-kolom yang didefinisikan dalam {@link android.provider.DocumentsContract.Document}. 813 </p> 814 815 <p>Metode {@link android.provider.DocumentsProvider#queryDocument queryDocument()} 816 menghasilkan informasi yang sama yang diteruskan dalam 817 {@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}, 818 namun untuk file tertentu:</p> 819 820 821 <pre>@Override 822 public 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 836 openDocument()} untuk menghasilkan {@link android.os.ParcelFileDescriptor} yang mewakili 837 file yang disebutkan. Aplikasi lain bisa menggunakan {@link android.os.ParcelFileDescriptor} 838 yang dihasilkan untuk mengalirkan data. Sistem memanggil metode ini setelah pengguna memilih file 839 dan aplikasi klien meminta akses ke file itu dengan memanggil 840 {@link android.content.ContentResolver#openFileDescriptor openFileDescriptor()}. 841 Misalnya:</p> 842 843 <pre>@Override 844 public 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 888 dan Anda ingin memastikan bahwa pengguna sudah login sebelum Anda mulai berbagi file mereka. 889 Apakah yang harus dilakukan aplikasi Anda jika pengguna tidak login? Solusinya adalah menghasilkan 890 akar nol dalam implementasi {@link android.provider.DocumentsProvider#queryRoots 891 queryRoots()} Anda. Yakni, sebuah kursor akar kosong:</p> 892 893 <pre> 894 public 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()}. 904 Ingat {@link android.provider.DocumentsContract}? Kita menggunakannya untuk membuat 905 URI ini. Cuplikan berikut memberi tahu sistem untuk membuat query akar penyedia dokumen Anda 906 kapan saja status login pengguna berubah. Jika pengguna tidak 907 login, panggilan ke {@link android.provider.DocumentsProvider#queryRoots queryRoots()} akan menghasilkan 908 kursor kosong, seperti yang ditampilkan di atas. Cara ini akan memastikan bahwa dokumen penyedia hanya 909 tersedia 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>