2013-10-27 14:47:18 -07:00
|
|
|
|
page.title=Storage Access Framework
|
|
|
|
|
@jd:body
|
|
|
|
|
<div id="qv-wrapper">
|
|
|
|
|
<div id="qv">
|
|
|
|
|
|
2013-11-12 18:42:08 -08:00
|
|
|
|
<h2>In this document
|
|
|
|
|
<a href="#" onclick="hideNestedItems('#toc44',this);return false;" class="header-toggle">
|
|
|
|
|
<span class="more">show more</span>
|
|
|
|
|
<span class="less" style="display:none">show less</span></a></h2>
|
|
|
|
|
<ol id="toc44" class="hide-nested">
|
2013-10-27 14:47:18 -07:00
|
|
|
|
<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>
|
2013-11-12 18:42:08 -08:00
|
|
|
|
</ol>
|
|
|
|
|
|
|
|
|
|
<h2>Videos</h2>
|
|
|
|
|
|
|
|
|
|
<ol>
|
|
|
|
|
<li><a href="http://www.youtube.com/watch?v=zxHVeXbK1P4">
|
|
|
|
|
DevBytes: Android 4.4 Storage Access Framework: Provider</a></li>
|
|
|
|
|
<li><a href="http://www.youtube.com/watch?v=UFj9AEz0DHQ">
|
|
|
|
|
DevBytes: Android 4.4 Storage Access Framework: Client</a></li>
|
|
|
|
|
</ol>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<h2>Code Samples</h2>
|
|
|
|
|
|
|
|
|
|
<ol>
|
|
|
|
|
<li><a href="{@docRoot}samples/StorageProvider/index.html">
|
|
|
|
|
Storage Provider</a></li>
|
|
|
|
|
<li><a href="{@docRoot}samples/StorageClient/index.html">
|
|
|
|
|
StorageClient</a></li>
|
2013-10-27 14:47:18 -07:00
|
|
|
|
</ol>
|
|
|
|
|
|
|
|
|
|
<h2>See Also</h2>
|
|
|
|
|
<ol>
|
|
|
|
|
<li>
|
|
|
|
|
<a href="{@docRoot}guide/topics/providers/content-provider-basics.html">
|
|
|
|
|
Content Provider Basics
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
|
|
|
|
</ol>
|
2013-11-12 18:42:08 -08:00
|
|
|
|
|
2013-10-27 14:47:18 -07:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2013-11-12 18:42:08 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<p>Android 4.4 (API level 19) introduces the Storage Access Framework (SAF). The SAF
|
|
|
|
|
makes it simple for users to browse and open documents, images, and other files
|
|
|
|
|
across all of their their preferred document storage providers. A standard, easy-to-use UI
|
|
|
|
|
lets users browse files and access recents in a consistent way across apps and providers.</p>
|
|
|
|
|
|
2013-12-11 18:12:22 -08:00
|
|
|
|
<p>Cloud or local storage services can participate in this ecosystem by implementing a
|
2013-11-12 18:42:08 -08:00
|
|
|
|
{@link android.provider.DocumentsProvider} that encapsulates their services. Client
|
|
|
|
|
apps that need access to a provider's documents can integrate with the SAF with just a few
|
|
|
|
|
lines of code.</p>
|
|
|
|
|
|
|
|
|
|
<p>The SAF includes the following:</p>
|
2013-10-27 14:47:18 -07:00
|
|
|
|
|
|
|
|
|
<ul>
|
|
|
|
|
<li><strong>Document provider</strong>—A content provider that allows a
|
2013-11-12 18:42:08 -08:00
|
|
|
|
storage service (such as Google Drive) to reveal the files it manages. A document provider is
|
2013-10-27 14:47:18 -07:00
|
|
|
|
implemented as a subclass of the {@link android.provider.DocumentsProvider} class.
|
2013-11-12 18:42:08 -08:00
|
|
|
|
The document-provider schema is based on a traditional file hierarchy,
|
2013-10-27 14:47:18 -07:00
|
|
|
|
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>
|
|
|
|
|
|
2013-11-12 18:42:08 -08:00
|
|
|
|
<p>Some of the features offered by the SAF are as follows:</p>
|
2013-10-27 14:47:18 -07:00
|
|
|
|
<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>
|
|
|
|
|
|
2013-11-12 18:42:08 -08:00
|
|
|
|
<p>The SAF centers around a content provider that is a
|
2013-10-27 14:47:18 -07:00
|
|
|
|
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>
|
|
|
|
|
|
2013-11-12 18:42:08 -08:00
|
|
|
|
<p>Figure 2 shows an example of how a photo app might use the SAF
|
2013-10-27 14:47:18 -07:00
|
|
|
|
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>
|
|
|
|
|
|
2013-11-12 18:42:08 -08:00
|
|
|
|
<li>In the SAF, providers and clients don't interact
|
2013-10-27 14:47:18 -07:00
|
|
|
|
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>
|
|
|
|
|
|
2013-11-12 18:42:08 -08:00
|
|
|
|
<p>You can use the SAF to edit a text document in place.
|
2013-10-27 14:47:18 -07:00
|
|
|
|
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
|
2013-11-12 18:42:08 -08:00
|
|
|
|
a cloud save service), you can make your files available through the
|
|
|
|
|
SAF by writing a custom document provider. This section describes
|
2013-10-27 14:47:18 -07:00
|
|
|
|
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
|
2013-11-12 18:42:08 -08:00
|
|
|
|
<code>"true"</code>. This setting allows the system to grant other apps access
|
2013-10-27 14:47:18 -07:00
|
|
|
|
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
|
2013-11-12 18:42:08 -08:00
|
|
|
|
to everyone. Adding this permission restricts your provider to the system.
|
|
|
|
|
This restriction is important for security.</li>
|
|
|
|
|
|
|
|
|
|
<li>The {@code android:enabled} attribute set to a boolean value defined in a resource
|
|
|
|
|
file. The purpose of this attribute is to disable the provider on devices running Android 4.3 or lower.
|
|
|
|
|
For example, {@code android:enabled="@bool/atLeastKitKat"}. In
|
|
|
|
|
addition to including this attribute in the manifest, you need to do the following:
|
|
|
|
|
<ul>
|
|
|
|
|
<li>In your {@code bool.xml} resources file under {@code res/values/}, add
|
|
|
|
|
this line: <pre><bool name="atLeastKitKat">false</bool></pre></li>
|
|
|
|
|
|
|
|
|
|
<li>In your {@code bool.xml} resources file under {@code res/values-v19/}, add
|
|
|
|
|
this line: <pre><bool name="atLeastKitKat">true</bool></pre></li>
|
|
|
|
|
</ul></li>
|
2013-10-27 14:47:18 -07:00
|
|
|
|
|
|
|
|
|
<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"
|
2013-11-12 18:42:08 -08:00
|
|
|
|
android:permission="android.permission.MANAGE_DOCUMENTS"
|
|
|
|
|
android:enabled="@bool/atLeastKitKat">
|
2013-10-27 14:47:18 -07:00
|
|
|
|
<intent-filter>
|
|
|
|
|
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
|
|
|
|
</intent-filter>
|
|
|
|
|
</provider>
|
|
|
|
|
</application>
|
|
|
|
|
|
|
|
|
|
</manifest></pre>
|
|
|
|
|
|
2013-11-12 18:42:08 -08:00
|
|
|
|
<h4 id="43">Supporting devices running Android 4.3 and lower</h4>
|
2013-10-27 14:47:18 -07:00
|
|
|
|
|
|
|
|
|
<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
|
2013-11-12 18:42:08 -08:00
|
|
|
|
your manifest for devices running Android 4.4 or higher. A
|
2013-10-27 14:47:18 -07:00
|
|
|
|
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
|
2013-11-12 18:42:08 -08:00
|
|
|
|
other metadata that pertain to the provider. The SAF
|
2013-10-27 14:47:18 -07:00
|
|
|
|
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);
|
|
|
|
|
}
|
2013-11-12 18:42:08 -08:00
|
|
|
|
</pre>
|