• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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> &mdash; 可讓儲存服務 (例如 Google 雲端硬碟) 顯示所管理檔案的內容供應程式。
92文件供應程式是當作
93{@link android.provider.DocumentsProvider} 類別的子類別使用。文件供應程式結構定義是以傳統檔案階層為依據,不論您為文件供應程式設定的資料儲存方式為何。Android 平台內建數種文件供應程式,例如「下載」、「圖片」和「影片」。
94
95
96
97</li>
98
99<li><strong>用戶端應用程式</strong> &mdash; 可呼叫
100{@link android.content.Intent#ACTION_OPEN_DOCUMENT} 和 (或)
101{@link android.content.Intent#ACTION_CREATE_DOCUMENT} 意圖及接收文件供應程式所傳回檔案的自訂應用程式。
102</li>
103
104<li><strong>挑選器</strong> &mdash; 可讓使用者透過所有符合用戶端應用程式搜尋條件的文件供應程式存取文件的系統 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} 意圖後,互動程序便會開始。意圖可能包括用於縮小條件範圍的篩選器 &mdash; 例如「將所有內含 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 &quot;file chooser&quot; UI and select an image.
255 */
256public void performFileSearch() {
257
258    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
259    // browser.
260    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
261
262    // Filter to only show results that can be &quot;opened&quot;, such as a
263    // file (as opposed to a list of contacts or timezones)
264    intent.addCategory(Intent.CATEGORY_OPENABLE);
265
266    // Filter to show only images, using the image MIME data type.
267    // If one wanted to search for ogg vorbis files, the type would be &quot;audio/ogg&quot;.
268    // To search for all documents available via installed storage providers,
269    // it would be &quot;*/*&quot;.
270    intent.setType(&quot;image/*&quot;);
271
272    startActivityForResult(intent, READ_REQUEST_CODE);
273}</pre>
274
275<p>請注意下列事項:</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>&#64;Override
297public void onActivityResult(int requestCode, int resultCode,
298        Intent resultData) {
299
300    // The ACTION_OPEN_DOCUMENT intent was sent with the request code
301    // READ_REQUEST_CODE. If the request code seen here doesn't match, it's the
302    // response to some other intent, and the code below shouldn't run at all.
303
304    if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
305        // The document selected by the user won't be returned in the intent.
306        // Instead, a URI to that document will be contained in the return intent
307        // provided to this method as a parameter.
308        // Pull that URI using resultData.getData().
309        Uri uri = null;
310        if (resultData != null) {
311            uri = resultData.getData();
312            Log.i(TAG, "Uri: " + uri.toString());
313            showImage(uri);
314        }
315    }
316}
317</pre>
318
319<h3 id="metadata">檢查文件中繼資料</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    // &quot;if there's anything to look at, look at it&quot; conditionals.
335        if (cursor != null &amp;&amp; cursor.moveToFirst()) {
336
337            // Note it's called &quot;Display Name&quot;.  This is
338            // provider-specific, and might not necessarily be the file name.
339            String displayName = cursor.getString(
340                    cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
341            Log.i(TAG, &quot;Display Name: &quot; + displayName);
342
343            int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
344            // If the size is unknown, the value stored is null.  But since an
345            // int can't be null in Java, the behavior is implementation-specific,
346            // which is just a fancy term for &quot;unpredictable&quot;.  So as
347            // a rule, check if it's null before assigning to an int.  This will
348            // happen often:  The storage API allows for remote files, whose
349            // size might not be locally known.
350            String size = null;
351            if (!cursor.isNull(sizeIndex)) {
352                // Technically the column stores an int, but cursor.getString()
353                // will do the conversion automatically.
354                size = cursor.getString(sizeIndex);
355            } else {
356                size = &quot;Unknown&quot;;
357            }
358            Log.i(TAG, &quot;Size: &quot; + size);
359        }
360    } finally {
361        cursor.close();
362    }
363}
364</pre>
365
366<h3 id="open-client">開啟文件</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 &quot;opened&quot;, such as
433    // a file (as opposed to a list of contacts or timezones).
434    intent.addCategory(Intent.CATEGORY_OPENABLE);
435
436    // Create a file with the requested MIME type.
437    intent.setType(mimeType);
438    intent.putExtra(Intent.EXTRA_TITLE, fileName);
439    startActivityForResult(intent, WRITE_REQUEST_CODE);
440}
441</pre>
442
443<p>建立新文件後,您可在
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 &quot;opened&quot;, such as a
478    // file (as opposed to a list of contacts or timezones).
479    intent.addCategory(Intent.CATEGORY_OPENABLE);
480
481    // Filter to show only text files.
482    intent.setType(&quot;text/plain&quot;);
483
484    startActivityForResult(intent, EDIT_REQUEST_CODE);
485}
486</pre>
487
488<p>接著,您可以利用 {@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            &amp; (Intent.FLAG_GRANT_READ_URI_PERMISSION
530            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
531// Check for the freshest data.
532getContentResolver().takePersistableUriPermission(uri, takeFlags);</pre>
533
534<p>除了上述指示外,您還需要完成最後一個步驟。您儲存了您的應用程式最近存取的 URI,但這些 URI 有可能已失效 &mdash; 原因在於其他應用程式刪除或修改了文件。
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>&lt;provider&gt;</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>&quot;true&quot;</code> 的 <code>android:exported</code> 屬性。您必須將供應程式匯出,方便其他應用程式加以偵測。
568</li>
569
570<li>設為 <code>&quot;true&quot;</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>&lt;bool name=&quot;atLeastKitKat&quot;&gt;false&lt;/bool&gt;</pre></li>
586
587<li>在位於 {@code res/values-v19/} 的 {@code bool.xml} 資源檔案中,新增以下程式碼:
588 <pre>&lt;bool name=&quot;atLeastKitKat&quot;&gt;true&lt;/bool&gt;</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>&lt;manifest... &gt;
599    ...
600    &lt;uses-sdk
601        android:minSdkVersion=&quot;19&quot;
602        android:targetSdkVersion=&quot;19&quot; /&gt;
603        ....
604        &lt;provider
605            android:name=&quot;com.example.android.storageprovider.MyCloudProvider&quot;
606            android:authorities=&quot;com.example.android.storageprovider.documents&quot;
607            android:grantUriPermissions=&quot;true&quot;
608            android:exported=&quot;true&quot;
609            android:permission=&quot;android.permission.MANAGE_DOCUMENTS&quot;
610            android:enabled=&quot;&#64;bool/atLeastKitKat&quot;&gt;
611            &lt;intent-filter&gt;
612                &lt;action android:name=&quot;android.content.action.DOCUMENTS_PROVIDER&quot; /&gt;
613            &lt;/intent-filter&gt;
614        &lt;/provider&gt;
615    &lt;/application&gt;
616
617&lt;/manifest&gt;</pre>
618
619<h4 id="43">支援搭載 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>&lt;bool name=&quot;atMostJellyBeanMR2&quot;&gt;true&lt;/bool&gt;</pre></li>
640
641<li>在位於 {@code res/values-v19/} 的 {@code bool.xml} 資源檔案中,新增以下程式碼:
642 <pre>&lt;bool name=&quot;atMostJellyBeanMR2&quot;&gt;false&lt;/bool&gt;</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&lt;!-- This activity alias is added so that GET_CONTENT intent-filter
651     can be disabled for builds on API level 19 and higher. --&gt;
652&lt;activity-alias android:name=&quot;com.android.example.app.MyPicker&quot;
653        android:targetActivity=&quot;com.android.example.app.MyActivity&quot;
654        ...
655        android:enabled=&quot;@bool/atMostJellyBeanMR2&quot;&gt;
656    &lt;intent-filter&gt;
657        &lt;action android:name=&quot;android.intent.action.GET_CONTENT&quot; /&gt;
658        &lt;category android:name=&quot;android.intent.category.OPENABLE&quot; /&gt;
659        &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; /&gt;
660        &lt;data android:mimeType=&quot;image/*&quot; /&gt;
661        &lt;data android:mimeType=&quot;video/*&quot; /&gt;
662    &lt;/intent-filter&gt;
663&lt;/activity-alias&gt;
664</pre>
665</li>
666</ol>
667<h3 id="contract">合約</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這個程式碼片隊會建立新游標並在其中加入一列 &mdash; 也就是根目錄或頂層目錄 (例如「下載」或「圖片」)。
726
727大多數供應程式只有一個根目錄。而您可以有多個根目錄,例如擁有多個使用者帳戶的情況下。
728在這種情況下,只要在游標中加入第二列即可。
729</p>
730
731<pre>
732&#64;Override
733public Cursor queryRoots(String[] projection) throws FileNotFoundException {
734
735    // Create a cursor with either the requested fields, or the default
736    // projection if "projection" is null.
737    final MatrixCursor result =
738            new MatrixCursor(resolveRootProjection(projection));
739
740    // If user is not logged in, return an empty root cursor.  This removes our
741    // provider from the list entirely.
742    if (!isUserLoggedIn()) {
743        return result;
744    }
745
746    // It's possible to have multiple roots (e.g. for multiple accounts in the
747    // same app) -- just add multiple cursor rows.
748    // Construct one row for a root called &quot;MyCloud&quot;.
749    final MatrixCursor.RowBuilder row = result.newRow();
750    row.add(Root.COLUMN_ROOT_ID, ROOT);
751    row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary));
752
753    // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
754    // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
755    // recently used documents will show up in the &quot;Recents&quot; category.
756    // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
757    // shares.
758    row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
759            Root.FLAG_SUPPORTS_RECENTS |
760            Root.FLAG_SUPPORTS_SEARCH);
761
762    // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
763    row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title));
764
765    // This document id cannot change once it's shared.
766    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir));
767
768    // The child MIME types are used to filter the roots and only present to the
769    //  user roots that contain the desired type somewhere in their file hierarchy.
770    row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(mBaseDir));
771    row.add(Root.COLUMN_AVAILABLE_BYTES, mBaseDir.getFreeSpace());
772    row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);
773
774    return result;
775}</pre>
776
777<h4 id="queryChildDocuments">實作 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>&#64;Override
793public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
794                              String sortOrder) throws FileNotFoundException {
795
796    final MatrixCursor result = new
797            MatrixCursor(resolveDocumentProjection(projection));
798    final File parent = getFileForDocId(parentDocumentId);
799    for (File file : parent.listFiles()) {
800        // Adds the file's display name, MIME type, size, and so on.
801        includeFile(result, null, file);
802    }
803    return result;
804}
805</pre>
806
807<h4 id="queryDocument">實作 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>&#64;Override
822public Cursor queryDocument(String documentId, String[] projection) throws
823        FileNotFoundException {
824
825    // Create a cursor with the requested projection, or the default projection.
826    final MatrixCursor result = new
827            MatrixCursor(resolveDocumentProjection(projection));
828    includeFile(result, documentId, null);
829    return result;
830}
831</pre>
832
833<h4 id="openDocument">實作 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>&#64;Override
844public ParcelFileDescriptor openDocument(final String documentId,
845                                         final String mode,
846                                         CancellationSignal signal) throws
847        FileNotFoundException {
848    Log.v(TAG, &quot;openDocument, mode: &quot; + mode);
849    // It's OK to do network operations in this method to download the document,
850    // as long as you periodically check the CancellationSignal. If you have an
851    // extremely large file to transfer from the network, a better solution may
852    // be pipes or sockets (see ParcelFileDescriptor for helper methods).
853
854    final File file = getFileForDocId(documentId);
855
856    final boolean isWrite = (mode.indexOf('w') != -1);
857    if(isWrite) {
858        // Attach a close listener if the document is opened in write mode.
859        try {
860            Handler handler = new Handler(getContext().getMainLooper());
861            return ParcelFileDescriptor.open(file, accessMode, handler,
862                        new ParcelFileDescriptor.OnCloseListener() {
863                &#64;Override
864                public void onClose(IOException e) {
865
866                    // Update the file with the cloud server. The client is done
867                    // writing.
868                    Log.i(TAG, &quot;A file with id &quot; +
869                    documentId + &quot; has been closed!
870                    Time to &quot; +
871                    &quot;update the server.&quot;);
872                }
873
874            });
875        } catch (IOException e) {
876            throw new FileNotFoundException(&quot;Failed to open document with id &quot;
877            + documentId + &quot; and mode &quot; + mode);
878        }
879    } else {
880        return ParcelFileDescriptor.open(file, accessMode);
881    }
882}
883</pre>
884
885<h3 id="security">安全性</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>