• 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">
50DevBytes:Android 4.4 存储访问框架:提供程序</a></li>
51     <li><a href="http://www.youtube.com/watch?v=UFj9AEz0DHQ">
52DevBytes:Android 4.4 存储访问框架:客户端</a></li>
53</ol>
54
55
56<h2>代码示例</h2>
57
58<ol>
59    <li><a href="{@docRoot}samples/StorageProvider/index.html">
60存储提供程序</a></li>
61     <li><a href="{@docRoot}samples/StorageClient/index.html">
62StorageClient</a></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 级)引入了存储访问框架 (SAF)。SAF
79让用户能够在其所有首选文档存储提供程序中方便地浏览并打开文档、图像以及其他文件。
80用户可以通过易用的标准 UI,以统一方式在所有应用和提供程序中浏览文件和访问最近使用的文件。
81</p>
82
83<p>云存储服务或本地存储服务可以通过实现封装其服务的
84{@link android.provider.DocumentsProvider} 参与此生态系统。只需几行代码,便可将需要访问提供程序文档的客户端应用与
85SAF
86集成。</p>
87
88<p>SAF 包括以下内容:</p>
89
90<ul>
91<li><strong>文档提供程序</strong>&mdash;一种内容提供程序,允许存储服务(如 Google
92云端硬盘)显示其管理的文件。文档提供程序作为
93{@link android.provider.DocumentsProvider}
94类的子类实现。文档提供程序的架构基于传统文件层次结构,但其实际数据存储方式由您决定。Android
95平台包括若干内置文档提供程序,如 Downloads、Images 和 Videos;
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}  Intent 并接收文档提供程序返回的文件;
102</li>
103
104<li><strong>选取器</strong>&mdash;一种系统
105UI,允许用户访问所有满足客户端应用搜索条件的文档提供程序内的文档。</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 围绕的内容提供程序是
121{@link android.provider.DocumentsProvider} 类的一个子类。在<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>每个文档提供程序都会报告一个或多个作为探索文档结构树起点的“根目录”。每个根目录都有一个唯一的
131{@link android.provider.DocumentsContract.Root#COLUMN_ROOT_ID},并且指向表示该根目录下内容的文档(目录)。根目录采用动态设计,以支持多个帐户、临时
132USB
133存储设备或用户登录/注销等用例;
134
135
136</li>
137
138<li>每个根目录下都有一个文档。该文档指向 1 至 <em>N</em>
139个文档,而其中每个文档又可指向 1 至 <em>N</em> 个文档; </li>
140
141<li>每个存储后端都会通过使用唯一的
142{@link android.provider.DocumentsContract.Document#COLUMN_DOCUMENT_ID} 引用各个文件和目录来显示它们。文档
143ID
144必须具有唯一性,一旦发放便不得更改,因为它们用于所有设备重启过程中的永久性
145URI 授权;</li>
146
147
148<li>文档可以是可打开的文件(具有特定 MIME
149类型)或包含附加文档的目录(具有
150{@link android.provider.DocumentsContract.Document#MIME_TYPE_DIR} MIME 类型);</li>
151
152<li>每个文档都可以具有不同的功能,如
153{@link android.provider.DocumentsContract.Document#COLUMN_FLAGS COLUMN_FLAGS} 所述。例如,{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_WRITE}、{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE}
154155{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_THUMBNAIL}。多个目录中可以包含相同的
156{@link android.provider.DocumentsContract.Document#COLUMN_DOCUMENT_ID}。
157
158</li>
159</ul>
160
161<h2 id="flow">控制流</h2>
162<p>如前文所述,文档提供程序数据模型基于传统文件层次结构。
163不过,只要可以通过
164{@link android.provider.DocumentsProvider} API
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>交互在应用(在本示例中为照片应用)触发 Intent
181{@link android.content.Intent#ACTION_OPEN_DOCUMENT} 或 {@link android.content.Intent#ACTION_CREATE_DOCUMENT} 后开始。Intent 可能包括进一步细化条件的过滤器&mdash;例如,“为我提供所有 MIME
182类型为‘图像’的可打开文件”;
183</li>
184
185<li>Intent 触发后,系统选取器将检索每个已注册的提供程序,并向用户显示匹配的内容根目录;
186</li>
187
188<li>选取器会为用户提供一个标准的文档访问界面,但底层文档提供程序可能与其差异很大。
189例如,图 2
190显示了一个 Google 云端硬盘提供程序、一个 USB 提供程序和一个云提供程序。</li>
191</ul>
192
193<p>图 3 显示了一个选取器,一位搜索图像的用户在其中选择了一个
194Google 云端硬盘帐户:</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
201云端硬盘时,系统会显示图像,如图 4 所示。从这时起,用户就可以通过提供程序和客户端应用支持的任何方式与它们进行交互。
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
211及更低版本,如果您想让应用从其他应用中检索文件,它必须调用 {@link android.content.Intent#ACTION_PICK}
212或 {@link android.content.Intent#ACTION_GET_CONTENT} 等 Intent。然后,用户必须选择一个要从中选取文件的应用,并且所选应用必须提供一个用户界面,以便用户浏览和选取可用文件。
213
214 </p>
215
216<p>对于 Android 4.4 及更高版本,您还可以选择使用
217{@link android.content.Intent#ACTION_OPEN_DOCUMENT}
218 Intent,后者会显示一个由系统控制的选取器
219UI,用户可以通过它浏览其他应用提供的所有文件。用户只需通过这一个 UI
220便可从任何受支持的应用中选取文件。</p>
221
222<p>{@link android.content.Intent#ACTION_OPEN_DOCUMENT}
223并非设计用于替代 {@link android.content.Intent#ACTION_GET_CONTENT}。应使用的 Intent 取决于应用的需要:
224</p>
225
226<ul>
227<li>如果您只想让应用读取/导入数据,请使用
228{@link android.content.Intent#ACTION_GET_CONTENT}。使用此方法时,应用会导入数据(如图像文件)的副本;
229</li>
230
231<li>如果您想让应用获得对文档提供程序所拥有文档的长期、持久性访问权限,请使用
232{@link android.content.Intent#ACTION_OPEN_DOCUMENT}。
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} Intent 的客户端应用。</p>
242
243
244<h3 id="search">搜索文档</h3>
245
246<p>
247以下代码段使用
248{@link android.content.Intent#ACTION_OPEN_DOCUMENT}
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 Intent 时,后者会启动一个选取器来显示所有匹配的文档提供程序</li>
279
280<li>在 Intent 中添加类别 {@link android.content.Intent#CATEGORY_OPENABLE}
281可对结果进行过滤,以仅显示可以打开的文档(如图像文件)</li>
282
283<li>语句 {@code intent.setType("image/*")} 可做进一步过滤,以仅显示
284MIME 数据类型为图像的文档</li>
285</ul>
286
287<h3 id="results">处理结果</h3>
288
289<p>用户在选取器中选择文档后,系统就会调用
290{@link android.app.Activity#onActivityResult onActivityResult()}。指向所选文档的 URI
291包含在 {@code resultData}
292参数中。使用 {@link android.content.Intent#getData getData()}
293提取 URI。获得 URI 后,即可使用它来检索用户想要的文档。例如:
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 线程上执行此操作。请使用
386{@link android.os.AsyncTask} 在后台执行此操作。打开位图后,即可在
387{@link android.widget.ImageView} 中显示它。
388</p>
389
390<h4>获取 InputStream</h4>
391
392<p>以下示例展示了如何从 URI 中获取
393{@link java.io.InputStream}。在此代码段中,系统将文件行读取到一个字符串中:</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 Intent 在文档提供程序中创建新文档。要想创建文件,请为您的 Intent 提供一个 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()} 中获取其
445URI,以便继续向其写入内容。</p>
446
447<h3 id="delete">删除文档</h3>
448
449<p>如果您获得了文档的
450URI,并且文档的
451{@link android.provider.DocumentsContract.Document#COLUMN_FLAGS Document.COLUMN_FLAGS}
452包含
453{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE SUPPORTS_DELETE},便可以删除该文档。例如:</p>
454
455<pre>
456DocumentsContract.deleteDocument(getContentResolver(), uri);
457</pre>
458
459<h3 id="edit">编辑文档</h3>
460
461<p>您可以使用 SAF
462就地编辑文本文档。以下代码段会触发
463{@link android.content.Intent#ACTION_OPEN_DOCUMENT}  Intent
464并使用类别 {@link android.content.Intent#CATEGORY_OPENABLE}
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>接下来,您可以从
489{@link android.app.Activity#onActivityResult onActivityResult()}(请参阅<a href="#results">处理结果</a>)调用代码以执行编辑。以下代码段可从
490{@link android.content.ContentResolver} 获取
491{@link java.io.FileOutputStream}。默认情况下,它使用“写入”模式。最佳做法是请求获得所需的最低限度访问权限,因此如果您只需要写入权限,就不要请求获得读取/写入权限。
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>当您的应用打开文件进行读取或写入时,系统会为您的应用提供针对该文件的
516URI 授权。该授权将一直持续到用户设备重启时。但假定您的应用是图像编辑应用,而且您希望用户能够直接从应用中访问他们编辑的最后
5175
518张图像。如果用户的设备已经重启,您就需要将用户转回系统选取器以查找这些文件,这显然不是理想的做法。
519
520</p>
521
522<p>为防止出现这种情况,您可以保留系统为您的应用授予的权限。您的应用实际上是“获取”了系统提供的持久
523URI
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>还有最后一个步骤。您可能已经保存了应用最近访问的
535URI,但它们可能不再有效&mdash;另一个应用可能已删除或修改了文档。
536因此,您应该始终调用
537{@code getContentResolver().takePersistableUriPermission()}
538以检查有无最新数据。</p>
539
540<h2 id="custom">编写自定义文档提供程序</h2>
541
542<p>
543如果您要开发为文件提供存储服务(如云保存服务)的应用,可以通过编写自定义文档提供程序,通过
544SAF
545提供您的文件。本节描述如何执行此操作。
546</p>
547
548
549<h3 id="manifest">清单文件</h3>
550
551<p>要想实现自定义文档提供程序,请将以下内容添加到您的应用的清单文件:
552</p>
553<ul>
554
555<li>一个 API 19 级或更高级别的目标;</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>android:exported</code> 设置为
568<code>&quot;true&quot;</code>。您必须导出提供程序,以便其他应用可以看到;</li>
569
570<li>属性 <code>android:grantUriPermissions</code> 设置为
571<code>&quot;true&quot;</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
581或更低版本的设备上禁用提供程序。例如,{@code android:enabled="@bool/atLeastKitKat"}。除了在清单文件中加入此属性外,您还需要执行以下操作;
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} 操作的 Intent
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>{@link android.content.Intent#ACTION_OPEN_DOCUMENT}
622 Intent 仅可用于运行
623Android 4.4 及更高版本的设备。如果您想让应用支持 {@link android.content.Intent#ACTION_GET_CONTENT}
624以适应运行 Android 4.3
625及更低版本的设备,则应在您的清单文件中为运行 Android 4.4
626或更高版本的设备禁用 {@link android.content.Intent#ACTION_GET_CONTENT}  Intent
627过滤器。应将文档提供程序和
628{@link android.content.Intent#ACTION_GET_CONTENT}
629视为具有互斥性。如果您同时支持这两者,您的应用将在系统选取器
630UI
631中出现两次,提供两种不同的方式来访问您存储的数据。这会给用户造成困惑。</p>
632
633<p>建议按照以下步骤为运行 Android 4.4 版或更高版本的设备禁用
634{@link android.content.Intent#ACTION_GET_CONTENT}  Intent
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>添加一个<a href="{@docRoot}guide/topics/manifest/activity-alias-element.html">Activity别名</a>,为
6454.4 版(API 19
646级)或更高版本禁用 {@link android.content.Intent#ACTION_GET_CONTENT}  Intent
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} 类,它包含对
673URI、列名称、MIME
674类型以及其他与提供程序有关的元数据的常量定义。SAF
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>编写自定义文档提供程序的下一步是为抽象类
700{@link android.provider.DocumentsProvider} 创建子类。您至少需要实现以下方法:
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详情请参阅
715{@link android.provider.DocumentsProvider}。</p>
716
717<h4 id="queryRoots">实现 queryRoots</h4>
718
719<p>您实现的
720{@link android.provider.DocumentsProvider#queryRoots
721queryRoots()} 必须使用在
722{@link android.provider.DocumentsContract.Root} 中定义的列返回一个指向文档提供程序所有根目录的 {@link android.database.Cursor}。</p>
723
724<p>在以下代码段中,{@code projection}
725参数表示调用方想要返回的特定字段。代码段会创建一个新游标,并为其添加一行&mdash;一个根目录,如
726Downloads 或 Images
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}
782中定义的列返回一个指向指定目录中所有文件的
783{@link android.database.Cursor}。</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()}
811必须使用在 {@link android.provider.DocumentsContract.Document} 中定义的列返回一个指向指定文件的
812{@link android.database.Cursor}。
813</p>
814
815<p>除了特定文件的信息外,{@link android.provider.DocumentsProvider#queryDocument queryDocument()}
816方法返回的信息与
817{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}
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">实现 queryDocument</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解决方案是在您实现的
890{@link android.provider.DocumentsProvider#queryRoots
891queryRoots()} 中返回零个根目录。也就是空的根目录游标:</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>另一个步骤是调用
904{@code getContentResolver().notifyChange()}。还记得 {@link android.provider.DocumentsContract} 吗?我们将使用它来创建此
905URI。以下代码段会在每次用户的登录状态发生变化时指示系统查询文档提供程序的根目录。
906如果用户未登录,则调用
907{@link android.provider.DocumentsProvider#queryRoots queryRoots()}
908会返回一个空游标,如上文所示。这可以确保只有在用户登录提供程序后其中的文档才可用。
909</p>
910
911<pre>private void onLoginButtonClick() {
912    loginOrLogout();
913    getContentResolver().notifyChange(DocumentsContract
914            .buildRootsUri(AUTHORITY), null);
915}
916</pre>