1page.title=儲存空間存取架構 2@jd:body 3<div id="qv-wrapper"> 4<div id="qv"> 5 6<h2>本文件內容 7<a href="#" onclick="hideNestedItems('#toc44',this);return false;" class="header-toggle"> 8 <span class="more">顯示更多</span> 9 <span class="less" style="display:none">顯示較少</span></a></h2> 10<ol id="toc44" class="hide-nested"> 11 <li> 12 <a href="#overview">總覽</a> 13 </li> 14 <li> 15 <a href="#flow">控管流程</a> 16 </li> 17 <li> 18 <a href="#client">編寫用戶端應用程式</a> 19 <ol> 20 <li><a href="#search">搜尋文件</a></li> 21 <li><a href="#process">處理結果</a></li> 22 <li><a href="#metadata">檢查文件中繼資料</a></li> 23 <li><a href="#open">開啟文件</a></li> 24 <li><a href="#create">建立新文件</a></li> 25 <li><a href="#delete">刪除文件</a></li> 26 <li><a href="#edit">編輯文件</a></li> 27 <li><a href="#permissions">保留權限</a></li> 28 </ol> 29 </li> 30 <li><a href="#custom">編寫自訂文件供應程式</a> 31 <ol> 32 <li><a href="#manifest">宣示說明</a></li> 33 <li><a href="#contract">合約</a></li> 34 <li><a href="#subclass">將 DocumentsProvider 設為子類別</a></li> 35 <li><a href="#security">安全性</a></li> 36 </ol> 37 </li> 38 39</ol> 40<h2>重要類別</h2> 41<ol> 42 <li>{@link android.provider.DocumentsProvider}</li> 43 <li>{@link android.provider.DocumentsContract}</li> 44</ol> 45 46<h2>相關影片</h2> 47 48<ol> 49 <li><a href="http://www.youtube.com/watch?v=zxHVeXbK1P4">DevBytes: 50Android 4.4 儲存空間存取架構:供應程式</a></li> 51 <li><a href="http://www.youtube.com/watch?v=UFj9AEz0DHQ">DevBytes: 52Android 4.4 儲存空間存取架構:用戶端</a></li> 53</ol> 54 55 56<h2>程式碼範例</h2> 57 58<ol> 59 <li><a href="{@docRoot}samples/StorageProvider/index.html">儲存空間供應程式</a> 60</li> 61 <li><a href="{@docRoot}samples/StorageClient/index.html">StorageClient</a> 62</li> 63</ol> 64 65<h2>另請參閱</h2> 66<ol> 67 <li> 68 <a href="{@docRoot}guide/topics/providers/content-provider-basics.html"> 69 內容供應程式基本概念 70 </a> 71 </li> 72</ol> 73 74</div> 75</div> 76 77 78<p>Android 4.4 (API 級別 19) 導入了「儲存空間存取架構」(Storage Access Framework (SAF)),SAF 可方便使用者透過偏好的文件儲存空間供應程式開啟文件、圖片等其他檔案。 79 80提供簡單易用的標準 UI 可讓使用者在各種應用程式和供應程式中,以相同的方式瀏覽檔案及存取近期開啟的檔案。 81</p> 82 83<p>雲端或本機儲存服務可實作會封裝服務本身的 84{@link android.provider.DocumentsProvider},藉此加入這個生態系統。您只需編寫幾行程式碼,即可將需要存取供應程式文件的用戶端應用程式與 SAF 整合。 85 86</p> 87 88<p>SAF 內含下列項目:</p> 89 90<ul> 91<li><strong>文件供應程式</strong> — 可讓儲存服務 (例如 Google 雲端硬碟) 顯示所管理檔案的內容供應程式。 92文件供應程式是當作 93{@link android.provider.DocumentsProvider} 類別的子類別使用。文件供應程式結構定義是以傳統檔案階層為依據,不論您為文件供應程式設定的資料儲存方式為何。Android 平台內建數種文件供應程式,例如「下載」、「圖片」和「影片」。 94 95 96 97</li> 98 99<li><strong>用戶端應用程式</strong> — 可呼叫 100{@link android.content.Intent#ACTION_OPEN_DOCUMENT} 和 (或) 101{@link android.content.Intent#ACTION_CREATE_DOCUMENT} 意圖及接收文件供應程式所傳回檔案的自訂應用程式。 102</li> 103 104<li><strong>挑選器</strong> — 可讓使用者透過所有符合用戶端應用程式搜尋條件的文件供應程式存取文件的系統 UI。 105</li> 106</ul> 107 108<p>以下是 SAF 提供的部分功能:</p> 109<ul> 110<li>可讓使用者透過所有文件供應程式 (而非單一應用程式) 瀏覽內容。</li> 111<li>可將文件供應程式所擁有文件的存取權永久授予您的應用程式, 112方便使用者透過相關供應程式新增、編輯、儲存及刪除檔案。 113</li> 114<li>支援多個使用者帳戶和暫時性的根目錄,例如只在插入電腦時才會顯示的 USB 儲存空間供應程式。 115 </li> 116</ul> 117 118<h2 id ="overview">總覽</h2> 119 120<p>SAF 是以內容供應程式 ({@link android.provider.DocumentsProvider} 類別的子類別) 為基礎。 121「文件供應程式」<em></em>中的資料結構採用如下所示的傳統檔案階層: 122</p> 123<p><img src="{@docRoot}images/providers/storage_datamodel.png" alt="data model" /></p> 124<p class="img-caption"><strong>圖 1.</strong>文件供應程式資料模型。「根目錄」會指向單一「文件」,接著該文件會展開成樹狀結構的分支。請注意下列事項: 125</p> 126 127<p></p> 128<ul> 129 130<li>每個文件供應程式都會回報一或多個「根目錄」(也就是文件樹狀結構的起始點)。每個根目錄都有專屬的 {@link android.provider.DocumentsContract.Root#COLUMN_ROOT_ID},可導向至代表該根目錄所含內容的某份文件 (某個目錄)。根目錄的設計架構是動態的,能夠支援多重帳戶、暫時性 USB 儲存裝置或使用者登入/登出等使用狀況。 131 132 133 134 135 136</li> 137 138<li>每個根目錄都內含一份文件,而該文件會指向 N<em></em> 份文件的 1,每份文件又可指向另外 N<em></em> 份文件的 1。 139 </li> 140 141<li>每個儲存空間後端都會透過唯一的 142{@link android.provider.DocumentsContract.Document#COLUMN_DOCUMENT_ID} 來參照個別檔案,藉此顯示這些檔案及目錄。文件 ID 不得重複而且一旦核發便不得更改,原因在於裝置重新啟動時會將這些 ID 用於永久 URI 授權。 143 144 145</li> 146 147 148<li>文件可以是可開啟的檔案 (類型為 MIME) 或內含其他文件的目錄 (類型為 149{@link android.provider.DocumentsContract.Document#MIME_TYPE_DIR} MIME )。 150</li> 151 152<li>每份文件的功能會視 153{@link android.provider.DocumentsContract.Document#COLUMN_FLAGS COLUMN_FLAGS} 而有所不同,例如 {@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_WRITE}、 154{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE} 和 155{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_THUMBNAIL}。相同的 {@link android.provider.DocumentsContract.Document#COLUMN_DOCUMENT_ID} 可以納入多個目錄中。 156 157 158</li> 159</ul> 160 161<h2 id="flow">控管流程</h2> 162<p>如上所述,文件供應程式資料模型是以傳統檔案階層為基礎。 163不過,您可以自己偏好的方式儲存您的資料,只要所儲存資料可透過 {@link android.provider.DocumentsProvider} API 存取即可。例如,您可以將資料存放在標籤式的雲端儲存空間。 164 165</p> 166 167<p>圖 2 是相片應用程式如何使用 SAF 存取已儲存資料的說明範例: 168</p> 169<p><img src="{@docRoot}images/providers/storage_dataflow.png" alt="app" /></p> 170 171<p class="img-caption"><strong>圖 2.</strong>儲存空間存取架構</p> 172 173<p>請注意下列事項:</p> 174<ul> 175 176<li>在 SAF 中,供應程式與用戶端無法直接進行互動。 177用戶端必須取得相關權限才能與檔案進行互動 (也就是讀取、編輯、建立或刪除檔案)。 178</li> 179 180<li>應用程式 (在此範例中為相片應用程式) 觸發 181{@link android.content.Intent#ACTION_OPEN_DOCUMENT} 或 {@link android.content.Intent#ACTION_CREATE_DOCUMENT} 意圖後,互動程序便會開始。意圖可能包括用於縮小條件範圍的篩選器 — 例如「將所有內含 MIME 類型『圖片』的可開啟檔案提供給我」。 182 183</li> 184 185<li>一旦觸發意圖,系統挑選器就會前往所有已註冊的供應程式,並且向使用者顯示相符的內容根目錄。 186</li> 187 188<li>即便底層文件供應程式可能不盡相同,挑選器仍會提供使用者可用於存取文件的標準介面。 189例如圖 2 中的 Google 雲端硬碟供應程式、USB 供應程式和雲端供應程式。 190</li> 191</ul> 192 193<p>圖 3 顯示的是使用者搜尋指定 Google 雲端硬碟帳戶中的圖片時所用的挑選器: 194</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>圖 3.</strong>挑選器</p> 199 200<p>使用者選取 Google 雲端硬碟後,系統就會顯示相關圖片 (如圖 4 所示)。 201此時,使用者即可與這些圖片進行供應程式和用戶端應用程式支援的互動。 202 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>圖 4.</strong>相關圖片</p> 207 208<h2 id="client">編寫用戶端應用程式</h2> 209 210<p>如果您想讓應用程式在搭載 Android 4.3 以下版本的裝置上從其他應用程式擷取檔案,您的應用程式就必須呼叫 {@link android.content.Intent#ACTION_PICK}或 {@link android.content.Intent#ACTION_GET_CONTENT} 意圖。 211 212接著,使用者必須選取某款應用程式來選取檔案,而且選定的應用程式必須提供使用者介面,讓使用者瀏覽及挑選可用的檔案。 213 214 </p> 215 216<p>針對搭載 Android 4.4 以上版本的裝置,您的應用程式還可以呼叫 217{@link android.content.Intent#ACTION_OPEN_DOCUMENT} 意圖,以顯示系統所控管的挑選器 UI,方便使用者瀏覽其他應用程式提供的所有檔案。 218 219透過這個單一 UI,使用者可以從任何受支援的應用程式挑選檔案。 220</p> 221 222<p>{@link android.content.Intent#ACTION_OPEN_DOCUMENT} 並不是 {@link android.content.Intent#ACTION_GET_CONTENT} 的替代意圖,實際上應呼叫的意圖取決於您應用程式的需求。 223 224</p> 225 226<ul> 227<li>如果您只想讓應用程式讀取/匯入資料,請呼叫 {@link android.content.Intent#ACTION_GET_CONTENT}, 228以便應用程式匯入資料 (例如圖片檔) 的複本。 229</li> 230 231<li>如果您想將文件供應程式所擁有文件的存取權永久授予您的應用程式,請呼叫 {@link android.content.Intent#ACTION_OPEN_DOCUMENT}。 232 233例如可讓使用者編輯文件供應程式中所儲存圖片的相片編輯應用程式。 234 </li> 235 236</ul> 237 238 239<p>本節說明如何根據 240{@link android.content.Intent#ACTION_OPEN_DOCUMENT} 和 241{@link android.content.Intent#ACTION_CREATE_DOCUMENT} 意圖編寫用戶端應用程式。</p> 242 243 244<h3 id="search">搜尋文件</h3> 245 246<p> 247以下程式碼片段採用 {@link android.content.Intent#ACTION_OPEN_DOCUMENT},可搜尋內含圖片檔的文件供應程式: 248 249</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>請注意下列事項:</p> 276<ul> 277<li>當應用程式觸發 {@link android.content.Intent#ACTION_OPEN_DOCUMENT} 意圖時,挑選器便會啟動並顯示所有相符的文件供應程式。 278</li> 279 280<li>將 {@link android.content.Intent#CATEGORY_OPENABLE} 這個類別加入意圖中可篩選搜尋結果,限定系統只顯示可開啟的文件 (例如圖片檔)。 281</li> 282 283<li>使用 {@code intent.setType("image/*")} 陳述式可進一步篩選搜尋結果,顯示系統只顯示內含 MIME 類型圖片的文件。 284</li> 285</ul> 286 287<h3 id="results">處理結果</h3> 288 289<p>使用者在挑選器中選取某份文件後,便會呼叫 290{@link android.app.Activity#onActivityResult onActivityResult()}。指向所選文件的 URI 包含在 {@code resultData} 參數中。 291 292請使用 {@link android.content.Intent#getData getData()} 擷取 URI,然後使用該 URI 擷取使用者所需的文件。 293例如: 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">檢查文件中繼資料</h3> 320 321<p>取得文件的 URI 後,您就可以存取該文件的中繼資料。以下程式碼片段會擷取 URI 所指定文件的中繼資料並且加以記錄: 322</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">開啟文件</h3> 367 368<p>取得文件的 URI 後,您就可以開啟該文件或是對該文件執行任何所需操作。 369</p> 370 371<h4>點陣圖</h4> 372 373<p>以下範例可開啟 {@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>請注意,請不要針對 UI 執行緒進行這項作業,請在背景中使用 {@link android.os.AsyncTask} 進行。 386開啟點陣圖後,您就可以在 {@link android.widget.ImageView} 中顯示該點陣圖。 387 388</p> 389 390<h4>取得 InputStream</h4> 391 392<p>以下範例可從 URI 中取得 {@link java.io.InputStream}。在這個程式碼片段中,系統會將每行檔案解讀為單一字串: 393</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">建立新文件</h3> 411 412<p>應用程式可在文件供應程式中使用 413{@link android.content.Intent#ACTION_CREATE_DOCUMENT} 意圖建立新文件。 414如要建立新檔案,請將 MIME 類型和檔案名稱提供給意圖,然後使用專屬的要求程式碼執行該意圖。 415系統會為您完成其餘的作業:</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>建立新文件後,您可在 444{@link android.app.Activity#onActivityResult onActivityResult()} 中取得該文件的 URI, 445以便繼續在其中編寫程式碼。</p> 446 447<h3 id="delete">刪除文件</h3> 448 449<p>如果您已取得文件的 URI,而且文件的 450{@link android.provider.DocumentsContract.Document#COLUMN_FLAGS Document.COLUMN_FLAGS} 含有 451{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE SUPPORTS_DELETE},您就可以刪除該文件。 452 453例如:</p> 454 455<pre> 456DocumentsContract.deleteDocument(getContentResolver(), uri); 457</pre> 458 459<h3 id="edit">編輯文件</h3> 460 461<p>您可以使用 SAF 即時編輯文字文件。以下程式碼片段會觸發 {@link android.content.Intent#ACTION_OPEN_DOCUMENT} 意圖並使用 {@link android.content.Intent#CATEGORY_OPENABLE} 類別限制系統只顯示可開啟的文件。 462 463 464 465此外,這個程式碼還會進一步篩選搜尋結果,讓系統只顯示文字檔:</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>接著,您可以利用 {@link android.app.Activity#onActivityResult onActivityResult()} (詳情請參閱<a href="#results">處理結果</a>) 呼叫程式碼執行編輯動作。以下程式碼片段會利用 {@link android.content.ContentResolver} 取得 {@link java.io.FileOutputStream}。 489 490 491在預設情況下,這個程式碼片段會使用「寫入」模式。這種方法可索取最少量的所需存取權,因此如果您只需要寫入存取權,請勿要求讀取/寫入: 492 493</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">保留權限</h3> 514 515<p>應用程式開啟要讀取或寫入的檔案後,系統會將該檔案的 URI 權限授予您的應用程式。 516除非使用者重新啟動裝置,否則這項權限會持續保持有效狀態。不過,假如您的應用程式為圖片編輯應用程式,而您希望使用者可直接透過您的應用程式存取他們最近編輯的 5 張圖片。如果使用者重新啟動的裝置,就您必須將使用者傳回系統挑選器來搜尋所需檔案,而這並非最佳做法。 517 518 519 520</p> 521 522<p>為了避免這種情況發生,您可以保留系統授予您應用程式的權限。實際上,您的應用程式會「取得」系統授予的永久性 URI 權限。 523 524這種權限可讓使用者持續透過您的應用程式存取檔案,即使其裝置重新啟動也無妨: 525</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>除了上述指示外,您還需要完成最後一個步驟。您儲存了您的應用程式最近存取的 URI,但這些 URI 有可能已失效 — 原因在於其他應用程式刪除或修改了文件。 535 536因此,建議您一律呼叫 537{@code getContentResolver().takePersistableUriPermission()} 檢查最新資料。 538</p> 539 540<h2 id="custom">編寫自訂文件供應程式</h2> 541 542<p> 543如果您想開發可提供檔案儲存服務 (例如雲端儲存服務) 的應用程式,可以編寫自訂文件供應程式透過 SAF 提供您的檔案。 544 545本節說明如何編寫這類程式。 546</p> 547 548 549<h3 id="manifest">宣示說明</h3> 550 551<p>如要實作自訂文件供應程式,請將以下項目加入應用程式的宣示說明: 552</p> 553<ul> 554 555<li>19 以上的 API 級別目標。</li> 556 557<li>宣告自訂儲存空間供應程式的 558<code><provider></code> 元素。 </li> 559 560<li>供應程式的名稱 (也就是供應程式的類別名稱),包括套件名稱。範例:<code>com.example.android.storageprovider.MyCloudProvider</code>。 561</li> 562 563<li>授權的名稱 (也就是套件的名稱;在此範例中為 564<code>com.example.android.storageprovider</code>) 以及內容供應程式的類型 565(<code>documents</code>)。範例:{@code com.example.android.storageprovider.documents}。</li> 566 567<li>設為 <code>"true"</code> 的 <code>android:exported</code> 屬性。您必須將供應程式匯出,方便其他應用程式加以偵測。 568</li> 569 570<li>設為 <code>"true"</code> 的 571<code>android:grantUriPermissions</code> 屬性。這項設定可讓系統將供應程式內容的存取權授予其他應用程式。 572如果想瞭解如何保留特定文件的權限,請參閱<a href="#permissions">保留權限</a>。 573</li> 574 575<li>{@code MANAGE_DOCUMENTS} 權限。在預設情況下,所有人都可使用供應程式。 576加入這項權限可針對系統設定供應程式限制,藉此提高其安全性。 577</li> 578 579<li>設定資源檔案所定義布林值的 {@code android:enabled} 屬性。 580這項屬性可用於針對搭載 Android 4.3 以下版本的裝置停用供應程式。範例:{@code android:enabled="@bool/atLeastKitKat"}。 581除了在宣示說明中加入這項屬性以外,您還必須執行下列操作: 582 583<ul> 584<li>在位於 {@code res/values/} 的 {@code bool.xml} 資源檔案中,新增以下程式碼: 585 <pre><bool name="atLeastKitKat">false</bool></pre></li> 586 587<li>在位於 {@code res/values-v19/} 的 {@code bool.xml} 資源檔案中,新增以下程式碼: 588 <pre><bool name="atLeastKitKat">true</bool></pre></li> 589</ul></li> 590 591<li>內含 592{@code android.content.action.DOCUMENTS_PROVIDER} 動作的意圖篩選器,讓您的供應程式能夠在系統搜尋供應程式時顯示在挑選器中。 593</li> 594 595</ul> 596<p>以下是內含供應程式的範例宣示說明例外狀況:</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">支援搭載 Android 4.3 以下版本的裝置</h4> 620 621<p>只有搭載 Android 4.4 以上版本的裝置可使用 622{@link android.content.Intent#ACTION_OPEN_DOCUMENT} 意圖。如果您想讓應用程式支援 {@link android.content.Intent#ACTION_GET_CONTENT} 以便與搭載 Android 4.3 以下版本的裝置相容,請針對搭載 Android 4.4 以上版本的裝置停用宣示說明中的 {@link android.content.Intent#ACTION_GET_CONTENT} 意圖篩選器。 623 624 625 626 627文件供應器和 {@link android.content.Intent#ACTION_GET_CONTENT} 是完全不同的項目。 628 629如果您同時支援這兩個項目,您的應用程式就會重複出現在系統挑選器 UI 中,讓使用者可透過兩種不同方式存取您儲存的資料, 630 631而這樣會造成混淆。</p> 632 633<p>以下提供針對搭載 Android 4.4 以上版本的裝置停用 634{@link android.content.Intent#ACTION_GET_CONTENT} 意圖篩選器的建議做法: 635</p> 636 637<ol> 638<li>在位於 {@code res/values/} 的 {@code bool.xml} 資源檔案中,新增以下程式碼: 639 <pre><bool name="atMostJellyBeanMR2">true</bool></pre></li> 640 641<li>在位於 {@code res/values-v19/} 的 {@code bool.xml} 資源檔案中,新增以下程式碼: 642 <pre><bool name="atMostJellyBeanMR2">false</bool></pre></li> 643 644<li>新增 645<a href="{@docRoot}guide/topics/manifest/activity-alias-element.html">Activity 別名</a>來針對搭載 Android 4.4 (API 級別 19) 以上版本的裝置停用 {@link android.content.Intent#ACTION_GET_CONTENT} 意圖篩選器。 646 647例如: 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">合約</h3> 668 669<p>一般來說,當您編寫自訂內容供應程式時,需要完成的其中一項工作為實作合約類別 (詳情請參閱<a href="{@docRoot}guide/topics/providers/content-provider-creating.html#ContractClass">內容供應程式</a>開發人員指南)。 670 671 672合約類別是 {@code public final} 類別,內含以下項目的固定不變定義:URI、欄名稱、MIME 類型以及供應程式擁有的其他中繼資料。 673 674SAF 可為您提供以下合約類別,因此您不必自行編 寫合約: 675 676</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>例如,以下是在文件供應程式查詢文件或根目錄時可能會傳回的資料欄: 684</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">將 DocumentsProvider 設為子類別</h3> 698 699<p>編寫自動文件供應程式的下一個步驟是,將抽象類別 {@link android.provider.DocumentsProvider} 設為子類別。 700您至少必須實作下列方法: 701</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>以上是您必須實作的方法,不過您可能會視需要實作其他方法。 714詳情請參閱 {@link android.provider.DocumentsProvider}。 715</p> 716 717<h4 id="queryRoots">實作 queryRoots</h4> 718 719<p>實作 {@link android.provider.DocumentsProvider#queryRoots 720queryRoots()} 後系統會使用 {@link android.provider.DocumentsContract.Root} 中定義的資料欄,傳回指向文件供應程式所有根目錄的 {@link android.database.Cursor}。 721 722</p> 723 724<p>在以下程式碼片段中,{@code projection} 參數代表呼叫者想返回的特定欄位。 725這個程式碼片隊會建立新游標並在其中加入一列 — 也就是根目錄或頂層目錄 (例如「下載」或「圖片」)。 726 727大多數供應程式只有一個根目錄。而您可以有多個根目錄,例如擁有多個使用者帳戶的情況下。 728在這種情況下,只要在游標中加入第二列即可。 729</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">實作 queryChildDocuments</h4> 778 779<p>實作 780{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()} 後系統會使用 781{@link android.provider.DocumentsContract.Document} 中定義的資料欄,傳回指向特定目錄中所有檔案的 {@link android.database.Cursor}。 782 783</p> 784 785<p>當您在挑選器 UI 中選擇應用程式的根目錄後,就會呼叫這個方法,藉此取得根目錄內某個目錄中的下層文件。 786您可以在檔案階層的任何層級中呼叫這個方法,而不單單只能在根目錄中呼叫。 787以下程式碼片段會使用要求的資料欄建立新游標,然後加入該游標中上層目錄的任何下層物件相關資訊。下層物件可以是圖片、其他目錄等任何檔案: 788 789 790</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">實作 queryDocument</h4> 808 809<p>實作 810{@link android.provider.DocumentsProvider#queryDocument queryDocument()} 後系統會使用 {@link android.provider.DocumentsContract.Document} 中定義的資料欄,傳回指向特定檔案的 {@link android.database.Cursor}。 811 812 813</p> 814 815<p>{@link android.provider.DocumentsProvider#queryDocument queryDocument()}方法會針對特定檔案傳回 816{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()} 所傳送的相同資訊: 817 818</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">實作 openDocument</h4> 834 835<p>您必須實作 {@link android.provider.DocumentsProvider#openDocument 836openDocument()} 來傳回代表特定檔案的 837{@link android.os.ParcelFileDescriptor}。其他應用程式可利用傳回的 {@link android.os.ParcelFileDescriptor} 傳輸資料。 838使用者選取檔案而且用戶端應用程式呼叫 839{@link android.content.ContentResolver#openFileDescriptor openFileDescriptor()} 要求存取該檔案後,系統就會呼叫這個方法。範例: 840 841</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">安全性</h3> 886 887<p>假如您的文件供應程式為受密碼保護的雲端儲存服務,而您先想確認使用者都已登入,然後再開始分享其檔案。那麼在使用者未登入的情況下,您的應用程式應採取什麼行動? 888 889解決方案是不要讓文件供應程式在您實作 {@link android.provider.DocumentsProvider#queryRoots 890queryRoots()} 後傳回任何根目錄。 891換句話說,就是讓供應程式傳回空的根目錄游標:</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>另一個需採取的步驟是呼叫 {@code getContentResolver().notifyChange()}。還記得 {@link android.provider.DocumentsContract} 嗎? 904我們會使用該類別建立 URI。以下程式碼片段會指示系統在使用者的登入狀態變更時,查詢文件供應程式的根目錄。 905 906如果使用者未登入,呼叫 {@link android.provider.DocumentsProvider#queryRoots queryRoots()} 就會如上所述傳回空的游標。 907 908這樣可確保只有登入供應程式的使用者可存取其中的文件。 909</p> 910 911<pre>private void onLoginButtonClick() { 912 loginOrLogout(); 913 getContentResolver().notifyChange(DocumentsContract 914 .buildRootsUri(AUTHORITY), null); 915} 916</pre>