1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
|
page.title=Storage Access Framework
@jd:body
<div id="qv-wrapper">
<div id="qv">
<h2>In this document</h2>
<ol>
<li>
<a href="#overview">Overview</a>
</li>
<li>
<a href="#flow">Control Flow</a>
</li>
<li>
<a href="#client">Writing a Client App</a>
<ol>
<li><a href="#search">Search for documents</a></li>
<li><a href="#process">Process results</a></li>
<li><a href="#metadata">Examine document metadata</a></li>
<li><a href="#open">Open a document</a></li>
<li><a href="#create">Create a new document</a></li>
<li><a href="#delete">Delete a document</a></li>
<li><a href="#edit">Edit a document</a></li>
<li><a href="#permissions">Persist permissions</a></li>
</ol>
</li>
<li><a href="#custom">Writing a Custom Document Provider</a>
<ol>
<li><a href="#manifest">Manifest</a></li>
<li><a href="#contract">Contracts</a></li>
<li><a href="#subclass">Subclass DocumentsProvider</a></li>
<li><a href="#security">Security</a></li>
</ol>
</li>
</ol>
<h2>Key classes</h2>
<ol>
<li>{@link android.provider.DocumentsProvider}</li>
<li>{@link android.provider.DocumentsContract}</li>
<li>{@link android.provider.DocumentsContract.Document}</li>
<li>{@link android.provider.DocumentsContract.Root}</li>
</ol>
<h2>See Also</h2>
<ol>
<li>
<a href="{@docRoot}guide/topics/providers/content-provider-basics.html">
Content Provider Basics
</a>
</li>
</ol>
</div>
</div>
<p>Android 4.4 (API level 19) introduces the Storage Access Framework. The
Storage Access Framework encapsulates capabilities in the Android platform that
allow apps to request files from file storage services. The Storage Access
Framework includes the following:</p>
<ul>
<li><strong>Document provider</strong>—A content provider that allows a
storage service (such as Google Drive) to reveal the files it manages. This is
implemented as a subclass of the {@link android.provider.DocumentsProvider} class.
The document provider schema is based on a traditional file hierarchy,
though how your document provider physically stores data is up to you.
The Android platform includes several built-in document providers, such as
Downloads, Images, and Videos.</li>
<li><strong>Client app</strong>—A custom app that invokes the
{@link android.content.Intent#ACTION_OPEN_DOCUMENT} and/or
{@link android.content.Intent#ACTION_CREATE_DOCUMENT} intent and receives the
files returned by document providers.</li>
<li><strong>Picker</strong>—A system UI that lets users access documents from all
document providers that satisfy the client app's search criteria.</li>
</ul>
<p>Some of the features offered by the Storage Access Framework are as follows:</p>
<ul>
<li>Lets users browse content from all document providers, not just a single app.</li>
<li>Makes it possible for your app to have long term, persistent access to
documents owned by a document provider. Through this access users can add, edit,
save, and delete files on the provider.</li>
<li>Supports multiple user accounts and transient roots such as USB storage
providers, which only appear if the drive is plugged in. </li>
</ul>
<h2 id ="overview">Overview</h2>
<p>The Storage Access Framework centers around a content provider that is a
subclass of the {@link android.provider.DocumentsProvider} class. Within a <em>document provider</em>, data is
structured as a traditional file hierarchy:</p>
<p><img src="{@docRoot}images/providers/storage_datamodel.png" alt="data model" /></p>
<p class="img-caption"><strong>Figure 1.</strong> Document provider data model. A Root points to a single Document,
which then starts the fan-out of the entire tree.</p>
<p>Note the following:</p>
<ul>
<li>Each document provider reports one or more
"roots" which are starting points into exploring a tree of documents.
Each root has a unique {@link android.provider.DocumentsContract.Root#COLUMN_ROOT_ID},
and it points to a document (a directory)
representing the contents under that root.
Roots are dynamic by design to support use cases like multiple accounts,
transient USB storage devices, or user login/log out.</li>
<li>Under each root is a single document. That document points to 1 to <em>N</em> documents,
each of which in turn can point to 1 to <em>N</em> documents. </li>
<li>Each storage backend surfaces
individual files and directories by referencing them with a unique
{@link android.provider.DocumentsContract.Document#COLUMN_DOCUMENT_ID}.
Document IDs must be unique and not change once issued, since they are used for persistent
URI grants across device reboots.</li>
<li>Documents can be either an openable file (with a specific MIME type), or a
directory containing additional documents (with the
{@link android.provider.DocumentsContract.Document#MIME_TYPE_DIR} MIME type).</li>
<li>Each document can have different capabilities, as described by
{@link android.provider.DocumentsContract.Document#COLUMN_FLAGS COLUMN_FLAGS}.
For example, {@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_WRITE},
{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE}, and
{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_THUMBNAIL}.
The same {@link android.provider.DocumentsContract.Document#COLUMN_DOCUMENT_ID} can be
included in multiple directories.</li>
</ul>
<h2 id="flow">Control Flow</h2>
<p>As stated above, the document provider data model is based on a traditional
file hierarchy. However, you can physically store your data however you like, as
long as it can be accessed through the {@link android.provider.DocumentsProvider} API. For example, you
could use tag-based cloud storage for your data.</p>
<p>Figure 2 shows an example of how a photo app might use the Storage Access Framework
to access stored data:</p>
<p><img src="{@docRoot}images/providers/storage_dataflow.png" alt="app" /></p>
<p class="img-caption"><strong>Figure 2.</strong> Storage Access Framework Flow</p>
<p>Note the following:</p>
<ul>
<li>In the Storage Access Framework, providers and clients don't interact
directly. A client requests permission to interact
with files (that is, to read, edit, create, or delete files).</li>
<li>The interaction starts when an application (in this example, a photo app) fires the intent
{@link android.content.Intent#ACTION_OPEN_DOCUMENT} or {@link android.content.Intent#ACTION_CREATE_DOCUMENT}. The intent may include filters
to further refine the criteria—for example, "give me all openable files
that have the 'image' MIME type."</li>
<li>Once the intent fires, the system picker goes to each registered provider
and shows the user the matching content roots.</li>
<li>The picker gives users a standard interface for accessing documents, even
though the underlying document providers may be very different. For example, figure 2
shows a Google Drive provider, a USB provider, and a cloud provider.</li>
</ul>
<p>Figure 3 shows a picker in which a user searching for images has selected a
Google Drive account:</p>
<p><img src="{@docRoot}images/providers/storage_picker.png" width="340"
alt="picker" style="border:2px solid #ddd"/></p>
<p class="img-caption"><strong>Figure 3.</strong> Picker</p>
<p>When the user selects Google Drive the images are displayed, as shown in
figure 4. From that point on, the user can interact with them in whatever ways
are supported by the provider and client app.
<p><img src="{@docRoot}images/providers/storage_photos.png" width="340"
alt="picker" style="border:2px solid #ddd"/></p>
<p class="img-caption"><strong>Figure 4.</strong> Images</p>
<h2 id="client">Writing a Client App</h2>
<p>On Android 4.3 and lower, if you want your app to retrieve a file from another
app, it must invoke an intent such as {@link android.content.Intent#ACTION_PICK}
or {@link android.content.Intent#ACTION_GET_CONTENT}. The user must then select
a single app from which to pick a file and the selected app must provide a user
interface for the user to browse and pick from the available files. </p>
<p>On Android 4.4 and higher, you have the additional option of using the
{@link android.content.Intent#ACTION_OPEN_DOCUMENT} intent,
which displays a picker UI controlled by the system that allows the user to
browse all files that other apps have made available. From this single UI, the
user can pick a file from any of the supported apps.</p>
<p>{@link android.content.Intent#ACTION_OPEN_DOCUMENT} is
not intended to be a replacement for {@link android.content.Intent#ACTION_GET_CONTENT}.
The one you should use depends on the needs of your app:</p>
<ul>
<li>Use {@link android.content.Intent#ACTION_GET_CONTENT} if you want your app
to simply read/import data. With this approach, the app imports a copy of the data,
such as an image file.</li>
<li>Use {@link android.content.Intent#ACTION_OPEN_DOCUMENT} if you want your
app to have long term, persistent access to documents owned by a document
provider. An example would be a photo-editing app that lets users edit
images stored in a document provider. </li>
</ul>
<p>This section describes how to write client apps based on the
{@link android.content.Intent#ACTION_OPEN_DOCUMENT} and
{@link android.content.Intent#ACTION_CREATE_DOCUMENT} intents.</p>
<h3 id="search">Search for documents</h3>
<p>
The following snippet uses {@link android.content.Intent#ACTION_OPEN_DOCUMENT}
to search for document providers that
contain image files:</p>
<pre>private static final int READ_REQUEST_CODE = 42;
...
/**
* Fires an intent to spin up the "file chooser" UI and select an image.
*/
public void performFileSearch() {
// ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
// browser.
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
// Filter to only show results that can be "opened", such as a
// file (as opposed to a list of contacts or timezones)
intent.addCategory(Intent.CATEGORY_OPENABLE);
// Filter to show only images, using the image MIME data type.
// If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
// To search for all documents available via installed storage providers,
// it would be "*/*".
intent.setType("image/*");
startActivityForResult(intent, READ_REQUEST_CODE);
}</pre>
<p>Note the following:</p>
<ul>
<li>When the app fires the {@link android.content.Intent#ACTION_OPEN_DOCUMENT}
intent, it launches a picker that displays all matching document providers.</li>
<li>Adding the category {@link android.content.Intent#CATEGORY_OPENABLE} to the
intent filters the results to display only documents that can be opened, such as image files.</li>
<li>The statement {@code intent.setType("image/*")} further filters to
display only documents that have the image MIME data type.</li>
</ul>
<h3 id="results">Process Results</h3>
<p>Once the user selects a document in the picker,
{@link android.app.Activity#onActivityResult onActivityResult()} gets called.
The URI that points to the selected document is contained in the {@code resultData}
parameter. Extract the URI using {@link android.content.Intent#getData getData()}.
Once you have it, you can use it to retrieve the document the user wants. For
example:</p>
<pre>@Override
public void onActivityResult(int requestCode, int resultCode,
Intent resultData) {
// The ACTION_OPEN_DOCUMENT intent was sent with the request code
// READ_REQUEST_CODE. If the request code seen here doesn't match, it's the
// response to some other intent, and the code below shouldn't run at all.
if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
// The document selected by the user won't be returned in the intent.
// Instead, a URI to that document will be contained in the return intent
// provided to this method as a parameter.
// Pull that URI using resultData.getData().
Uri uri = null;
if (resultData != null) {
uri = resultData.getData();
Log.i(TAG, "Uri: " + uri.toString());
showImage(uri);
}
}
}
</pre>
<h3 id="metadata">Examine document metadata</h3>
<p>Once you have the URI for a document, you gain access to its metadata. This
snippet grabs the metadata for a document specified by the URI, and logs it:</p>
<pre>public void dumpImageMetaData(Uri uri) {
// The query, since it only applies to a single document, will only return
// one row. There's no need to filter, sort, or select fields, since we want
// all fields for one document.
Cursor cursor = getActivity().getContentResolver()
.query(uri, null, null, null, null, null);
try {
// moveToFirst() returns false if the cursor has 0 rows. Very handy for
// "if there's anything to look at, look at it" conditionals.
if (cursor != null && cursor.moveToFirst()) {
// Note it's called "Display Name". This is
// provider-specific, and might not necessarily be the file name.
String displayName = cursor.getString(
cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
Log.i(TAG, "Display Name: " + displayName);
int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
// If the size is unknown, the value stored is null. But since an
// int can't be null in Java, the behavior is implementation-specific,
// which is just a fancy term for "unpredictable". So as
// a rule, check if it's null before assigning to an int. This will
// happen often: The storage API allows for remote files, whose
// size might not be locally known.
String size = null;
if (!cursor.isNull(sizeIndex)) {
// Technically the column stores an int, but cursor.getString()
// will do the conversion automatically.
size = cursor.getString(sizeIndex);
} else {
size = "Unknown";
}
Log.i(TAG, "Size: " + size);
}
} finally {
cursor.close();
}
}
</pre>
<h3 id="open-client">Open a document</h3>
<p>Once you have the URI for a document, you can open it or do whatever else
you want to do with it.</p>
<h4>Bitmap</h4>
<p>Here is an example of how you might open a {@link android.graphics.Bitmap}:</p>
<pre>private Bitmap getBitmapFromUri(Uri uri) throws IOException {
ParcelFileDescriptor parcelFileDescriptor =
getContentResolver().openFileDescriptor(uri, "r");
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
parcelFileDescriptor.close();
return image;
}
</pre>
<p>Note that you should not do this operation on the UI thread. Do it in the
background, using {@link android.os.AsyncTask}. Once you open the bitmap, you
can display it in an {@link android.widget.ImageView}.
</p>
<h4>Get an InputStream</h4>
<p>Here is an example of how you can get an {@link java.io.InputStream} from the URI. In this
snippet, the lines of the file are being read into a string:</p>
<pre>private String readTextFromUri(Uri uri) throws IOException {
InputStream inputStream = getContentResolver().openInputStream(uri);
BufferedReader reader = new BufferedReader(new InputStreamReader(
inputStream));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
fileInputStream.close();
parcelFileDescriptor.close();
return stringBuilder.toString();
}
</pre>
<h3 id="create">Create a new document</h3>
<p>Your app can create a new document in a document provider using the
{@link android.content.Intent#ACTION_CREATE_DOCUMENT}
intent. To create a file you give your intent a MIME type and a file name, and
launch it with a unique request code. The rest is taken care of for you:</p>
<pre>
// Here are some examples of how you might call this method.
// The first parameter is the MIME type, and the second parameter is the name
// of the file you are creating:
//
// createFile("text/plain", "foobar.txt");
// createFile("image/png", "mypicture.png");
// Unique request code.
private static final int WRITE_REQUEST_CODE = 43;
...
private void createFile(String mimeType, String fileName) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
// Filter to only show results that can be "opened", such as
// a file (as opposed to a list of contacts or timezones).
intent.addCategory(Intent.CATEGORY_OPENABLE);
// Create a file with the requested MIME type.
intent.setType(mimeType);
intent.putExtra(Intent.EXTRA_TITLE, fileName);
startActivityForResult(intent, WRITE_REQUEST_CODE);
}
</pre>
<p>Once you create a new document you can get its URI in
{@link android.app.Activity#onActivityResult onActivityResult()}, so that you
can continue to write to it.</p>
<h3 id="delete">Delete a document</h3>
<p>If you have the URI for a document and the document's
{@link android.provider.DocumentsContract.Document#COLUMN_FLAGS Document.COLUMN_FLAGS}
contains
{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE SUPPORTS_DELETE},
you can delete the document. For example:</p>
<pre>
DocumentsContract.deleteDocument(getContentResolver(), uri);
</pre>
<h3 id="edit">Edit a document</h3>
<p>You can use the Storage Access Framework to edit a text document in place.
This snippet fires
the {@link android.content.Intent#ACTION_OPEN_DOCUMENT} intent and uses the
category {@link android.content.Intent#CATEGORY_OPENABLE} to to display only
documents that can be opened. It further filters to show only text files:</p>
<pre>
private static final int EDIT_REQUEST_CODE = 44;
/**
* Open a file for writing and append some text to it.
*/
private void editDocument() {
// ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's
// file browser.
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
// Filter to only show results that can be "opened", such as a
// file (as opposed to a list of contacts or timezones).
intent.addCategory(Intent.CATEGORY_OPENABLE);
// Filter to show only text files.
intent.setType("text/plain");
startActivityForResult(intent, EDIT_REQUEST_CODE);
}
</pre>
<p>Next, from {@link android.app.Activity#onActivityResult onActivityResult()}
(see <a href="#results">Process results</a>) you can call code to perform the edit.
The following snippet gets a {@link java.io.FileOutputStream}
from the {@link android.content.ContentResolver}. By default it uses “write” mode.
It's best practice to ask for the least amount of access you need, so don’t ask
for read/write if all you need is write:</p>
<pre>private void alterDocument(Uri uri) {
try {
ParcelFileDescriptor pfd = getActivity().getContentResolver().
openFileDescriptor(uri, "w");
FileOutputStream fileOutputStream =
new FileOutputStream(pfd.getFileDescriptor());
fileOutputStream.write(("Overwritten by MyCloud at " +
System.currentTimeMillis() + "\n").getBytes());
// Let the document provider know you're done by closing the stream.
fileOutputStream.close();
pfd.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}</pre>
<h3 id="permissions">Persist permissions</h3>
<p>When your app opens a file for reading or writing, the system gives your
app a URI permission grant for that file. It lasts until the user's device restarts.
But suppose your app is an image-editing app, and you want users to be able to
access the last 5 images they edited, directly from your app. If the user's device has
restarted, you'd have to send the user back to the system picker to find the
files, which is obviously not ideal.</p>
<p>To prevent this from happening, you can persist the permissions the system
gives your app. Effectively, your app "takes" the persistable URI permission grant
that the system is offering. This gives the user continued access to the files
through your app, even if the device has been restarted:</p>
<pre>final int takeFlags = intent.getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Check for the freshest data.
getContentResolver().takePersistableUriPermission(uri, takeFlags);</pre>
<p>There is one final step. You may have saved the most
recent URIs your app accessed, but they may no longer be valid—another app
may have deleted or modified a document. Thus, you should always call
{@code getContentResolver().takePersistableUriPermission()} to check for the
freshest data.</p>
<h2 id="custom">Writing a Custom Document Provider</h2>
<p>
If you're developing an app that provides storage services for files (such as
a cloud save service), you can make your files available through the Storage
Access Framework by writing a custom document provider. This section describes
how to do this.</p>
<h3 id="manifest">Manifest</h3>
<p>To implement a custom document provider, add the following to your application's
manifest:</p>
<ul>
<li>A target of API level 19 or higher.</li>
<li>A <code><provider></code> element that declares your custom storage
provider. </li>
<li>The name of your provider, which is its class name, including package name.
For example: <code>com.example.android.storageprovider.MyCloudProvider</code>.</li>
<li>The name of your authority, which is your package name (in this example,
<code>com.example.android.storageprovider</code>) plus the type of content provider
(<code>documents</code>). For example, {@code com.example.android.storageprovider.documents}.</li>
<li>The attribute <code>android:exported</code> set to <code>"true"</code>.
You must export your provider so that other apps can see it.</li>
<li>The attribute <code>android:grantUriPermissions</code> set to
<code>"true"</code>. This allows the system to grant other apps access
to content in your provider. For a discussion of how to persist a grant for
a particular document, see <a href="#permissions">Persist permissions</a>.</li>
<li>The {@code MANAGE_DOCUMENTS} permission. By default a provider is available
to everyone. Adding this permission restricts your provider to the system,
which is important for security. </li>
<li>An intent filter that includes the
{@code android.content.action.DOCUMENTS_PROVIDER} action, so that your provider
appears in the picker when the system searches for providers.</li>
</ul>
<p>Here are excerpts from a sample manifest that includes a provider:</p>
<pre><manifest... >
...
<uses-sdk
android:minSdkVersion="19"
android:targetSdkVersion="19" />
....
<provider
android:name="com.example.android.storageprovider.MyCloudProvider"
android:authorities="com.example.android.storageprovider.documents"
android:grantUriPermissions="true"
android:exported="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
</application>
</manifest></pre>
<h4>Supporting devices running Android 4.3 and lower</h4>
<p>The
{@link android.content.Intent#ACTION_OPEN_DOCUMENT} intent is only available
on devices running Android 4.4 and higher.
If you want your application to support {@link android.content.Intent#ACTION_GET_CONTENT}
to accommodate devices that are running Android 4.3 and lower, you should
disable the {@link android.content.Intent#ACTION_GET_CONTENT} intent filter in
your manifest if a device is running Android 4.4 or higher. A
document provider and {@link android.content.Intent#ACTION_GET_CONTENT} should be considered
mutually exclusive. If you support both of them simultaneously, your app will
appear twice in the system picker UI, offering two different ways of accessing
your stored data. This would be confusing for users.</p>
<p>Here is the recommended way of disabling the
{@link android.content.Intent#ACTION_GET_CONTENT} intent filter for devices
running Android version 4.4 or higher:</p>
<ol>
<li>In your {@code bool.xml} resources file under {@code res/values/}, add
this line: <pre><bool name="atMostJellyBeanMR2">true</bool></pre></li>
<li>In your {@code bool.xml} resources file under {@code res/values-v19/}, add
this line: <pre><bool name="atMostJellyBeanMR2">false</bool></pre></li>
<li>Add an
<a href="{@docRoot}guide/topics/manifest/activity-alias-element.html">activity
alias</a> to disable the {@link android.content.Intent#ACTION_GET_CONTENT} intent
filter for versions 4.4 (API level 19) and higher. For example:
<pre>
<!-- This activity alias is added so that GET_CONTENT intent-filter
can be disabled for builds on API level 19 and higher. -->
<activity-alias android:name="com.android.example.app.MyPicker"
android:targetActivity="com.android.example.app.MyActivity"
...
android:enabled="@bool/atMostJellyBeanMR2">
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.OPENABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
</activity-alias>
</pre>
</li>
</ol>
<h3 id="contract">Contracts</h3>
<p>Usually when you write a custom content provider, one of the tasks is
implementing contract classes, as described in the
<a href="{@docRoot}guide/topics/providers/content-provider-creating.html#ContractClass">
Content Providers</a> developers guide. A contract class is a {@code public final} class
that contains constant definitions for the URIs, column names, MIME types, and
other metadata that pertain to the provider. The Storage Access Framework
provides these contract classes for you, so you don't need to write your
own:</p>
<ul>
<li>{@link android.provider.DocumentsContract.Document}</li>
<li>{@link android.provider.DocumentsContract.Root}</li>
</ul>
<p>For example, here are the columns you might return in a cursor when
your document provider is queried for documents or the root:</p>
<pre>private static final String[] DEFAULT_ROOT_PROJECTION =
new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES,
Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_AVAILABLE_BYTES,};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new
String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};
</pre>
<h3 id="subclass">Subclass DocumentsProvider</h3>
<p>The next step in writing a custom document provider is to subclass the
abstract class {@link android.provider.DocumentsProvider}. At minimum, you need
to implement the following methods:</p>
<ul>
<li>{@link android.provider.DocumentsProvider#queryRoots queryRoots()}</li>
<li>{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}</li>
<li>{@link android.provider.DocumentsProvider#queryDocument queryDocument()}</li>
<li>{@link android.provider.DocumentsProvider#openDocument openDocument()}</li>
</ul>
<p>These are the only methods you are strictly required to implement, but there
are many more you might want to. See {@link android.provider.DocumentsProvider}
for details.</p>
<h4 id="queryRoots">Implement queryRoots</h4>
<p>Your implementation of {@link android.provider.DocumentsProvider#queryRoots
queryRoots()} must return a {@link android.database.Cursor} pointing to all the
root directories of your document providers, using columns defined in
{@link android.provider.DocumentsContract.Root}.</p>
<p>In the following snippet, the {@code projection} parameter represents the
specific fields the caller wants to get back. The snippet creates a new cursor
and adds one row to it—one root, a top level directory, like
Downloads or Images. Most providers only have one root. You might have more than one,
for example, in the case of multiple user accounts. In that case, just add a
second row to the cursor.</p>
<pre>
@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
// Create a cursor with either the requested fields, or the default
// projection if "projection" is null.
final MatrixCursor result =
new MatrixCursor(resolveRootProjection(projection));
// If user is not logged in, return an empty root cursor. This removes our
// provider from the list entirely.
if (!isUserLoggedIn()) {
return result;
}
// It's possible to have multiple roots (e.g. for multiple accounts in the
// same app) -- just add multiple cursor rows.
// Construct one row for a root called "MyCloud".
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, ROOT);
row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary));
// FLAG_SUPPORTS_CREATE means at least one directory under the root supports
// creating documents. FLAG_SUPPORTS_RECENTS means your application's most
// recently used documents will show up in the "Recents" category.
// FLAG_SUPPORTS_SEARCH allows users to search all documents the application
// shares.
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
Root.FLAG_SUPPORTS_RECENTS |
Root.FLAG_SUPPORTS_SEARCH);
// COLUMN_TITLE is the root title (e.g. Gallery, Drive).
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title));
// This document id cannot change once it's shared.
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir));
// The child MIME types are used to filter the roots and only present to the
// user roots that contain the desired type somewhere in their file hierarchy.
row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(mBaseDir));
row.add(Root.COLUMN_AVAILABLE_BYTES, mBaseDir.getFreeSpace());
row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);
return result;
}</pre>
<h4 id="queryChildDocuments">Implement queryChildDocuments</h4>
<p>Your implementation of
{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}
must return a {@link android.database.Cursor} that points to all the files in
the specified directory, using columns defined in
{@link android.provider.DocumentsContract.Document}.</p>
<p>This method gets called when you choose an application root in the picker UI.
It gets the child documents of a directory under the root. It can be called at any level in
the file hierarchy, not just the root. This snippet
makes a new cursor with the requested columns, then adds information about
every immediate child in the parent directory to the cursor.
A child can be an image, another directory—any file:</p>
<pre>@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
String sortOrder) throws FileNotFoundException {
final MatrixCursor result = new
MatrixCursor(resolveDocumentProjection(projection));
final File parent = getFileForDocId(parentDocumentId);
for (File file : parent.listFiles()) {
// Adds the file's display name, MIME type, size, and so on.
includeFile(result, null, file);
}
return result;
}
</pre>
<h4 id="queryDocument">Implement queryDocument</h4>
<p>Your implementation of
{@link android.provider.DocumentsProvider#queryDocument queryDocument()}
must return a {@link android.database.Cursor} that points to the specified file,
using columns defined in {@link android.provider.DocumentsContract.Document}.
</p>
<p>The {@link android.provider.DocumentsProvider#queryDocument queryDocument()}
method returns the same information that was passed in
{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()},
but for a specific file:</p>
<pre>@Override
public Cursor queryDocument(String documentId, String[] projection) throws
FileNotFoundException {
// Create a cursor with the requested projection, or the default projection.
final MatrixCursor result = new
MatrixCursor(resolveDocumentProjection(projection));
includeFile(result, documentId, null);
return result;
}
</pre>
<h4 id="openDocument">Implement openDocument</h4>
<p>You must implement {@link android.provider.DocumentsProvider#openDocument
openDocument()} to return a {@link android.os.ParcelFileDescriptor} representing
the specified file. Other apps can use the returned {@link android.os.ParcelFileDescriptor}
to stream data. The system calls this method once the user selects a file
and the client app requests access to it by calling
{@link android.content.ContentResolver#openFileDescriptor openFileDescriptor()}.
For example:</p>
<pre>@Override
public ParcelFileDescriptor openDocument(final String documentId,
final String mode,
CancellationSignal signal) throws
FileNotFoundException {
Log.v(TAG, "openDocument, mode: " + mode);
// It's OK to do network operations in this method to download the document,
// as long as you periodically check the CancellationSignal. If you have an
// extremely large file to transfer from the network, a better solution may
// be pipes or sockets (see ParcelFileDescriptor for helper methods).
final File file = getFileForDocId(documentId);
final boolean isWrite = (mode.indexOf('w') != -1);
if(isWrite) {
// Attach a close listener if the document is opened in write mode.
try {
Handler handler = new Handler(getContext().getMainLooper());
return ParcelFileDescriptor.open(file, accessMode, handler,
new ParcelFileDescriptor.OnCloseListener() {
@Override
public void onClose(IOException e) {
// Update the file with the cloud server. The client is done
// writing.
Log.i(TAG, "A file with id " +
documentId + " has been closed!
Time to " +
"update the server.");
}
});
} catch (IOException e) {
throw new FileNotFoundException("Failed to open document with id "
+ documentId + " and mode " + mode);
}
} else {
return ParcelFileDescriptor.open(file, accessMode);
}
}
</pre>
<h3 id="security">Security</h3>
<p>Suppose your document provider is a password-protected cloud storage service
and you want to make sure that users are logged in before you start sharing their files.
What should your app do if the user is not logged in? The solution is to return
zero roots in your implementation of {@link android.provider.DocumentsProvider#queryRoots
queryRoots()}. That is, an empty root cursor:</p>
<pre>
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
...
// If user is not logged in, return an empty root cursor. This removes our
// provider from the list entirely.
if (!isUserLoggedIn()) {
return result;
}
</pre>
<p>The other step is to call {@code getContentResolver().notifyChange()}.
Remember the {@link android.provider.DocumentsContract}? We’re using it to make
this URI. The following snippet tells the system to query the roots of your
document provider whenever the user's login status changes. If the user is not
logged in, a call to {@link android.provider.DocumentsProvider#queryRoots queryRoots()} returns an
empty cursor, as shown above. This ensures that a provider's documents are only
available if the user is logged into the provider.</p>
<pre>private void onLoginButtonClick() {
loginOrLogout();
getContentResolver().notifyChange(DocumentsContract
.buildRootsUri(AUTHORITY), null);
}
</pre>
|