diff --git a/docs/html/guide/guide_toc.cs b/docs/html/guide/guide_toc.cs index f9a23a33a1d9b..84ffd055bc00f 100644 --- a/docs/html/guide/guide_toc.cs +++ b/docs/html/guide/guide_toc.cs @@ -63,6 +63,9 @@
  • Contacts Provider
  • +
  • + Storage Access Framework +
  • diff --git a/docs/html/guide/topics/providers/document-provider.jd b/docs/html/guide/topics/providers/document-provider.jd new file mode 100644 index 0000000000000..9af8d5ae616ab --- /dev/null +++ b/docs/html/guide/topics/providers/document-provider.jd @@ -0,0 +1,875 @@ +page.title=Storage Access Framework +@jd:body +
    +
    + +

    In this document

    +
      +
    1. + Overview +
    2. +
    3. + Control Flow +
    4. +
    5. + Writing a Client App +
        +
      1. Search for documents
      2. +
      3. Process results
      4. +
      5. Examine document metadata
      6. +
      7. Open a document
      8. +
      9. Create a new document
      10. +
      11. Delete a document
      12. +
      13. Edit a document
      14. +
      15. Persist permissions
      16. +
      +
    6. +
    7. Writing a Custom Document Provider +
        +
      1. Manifest
      2. +
      3. Contracts
      4. +
      5. Subclass DocumentsProvider
      6. +
      7. Security
      8. +
      +
    8. + +
    +

    Key classes

    +
      +
    1. {@link android.provider.DocumentsProvider}
    2. +
    3. {@link android.provider.DocumentsContract}
    4. +
    5. {@link android.provider.DocumentsContract.Document}
    6. +
    7. {@link android.provider.DocumentsContract.Root}
    8. +
    + +

    See Also

    +
      +
    1. + + Content Provider Basics + +
    2. +
    +
    +
    +

    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:

    + + + +

    Some of the features offered by the Storage Access Framework are as follows:

    + + +

    Overview

    + +

    The Storage Access Framework centers around a content provider that is a +subclass of the {@link android.provider.DocumentsProvider} class. Within a document provider, data is +structured as a traditional file hierarchy:

    +

    data model

    +

    Figure 1. Document provider data model. A Root points to a single Document, +which then starts the fan-out of the entire tree.

    + +

    Note the following:

    + + +

    Control Flow

    +

    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.

    + +

    Figure 2 shows an example of how a photo app might use the Storage Access Framework +to access stored data:

    +

    app

    + +

    Figure 2. Storage Access Framework Flow

    + +

    Note the following:

    + + +

    Figure 3 shows a picker in which a user searching for images has selected a +Google Drive account:

    + +

    + +

    Figure 3. Picker

    + +

    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. + +

    + +

    Figure 4. Images

    + +

    Writing a Client App

    + +

    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.

    + +

    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.

    + +

    {@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:

    + + + + +

    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.

    + + + + +

    +The following snippet uses {@link android.content.Intent#ACTION_OPEN_DOCUMENT} +to search for document providers that +contain image files:

    + +
    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);
    +}
    + +

    Note the following:

    + + +

    Process Results

    + +

    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:

    + +
    @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);
    +        }
    +    }
    +}
    +
    + +

    Examine document metadata

    + +

    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:

    + +
    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();
    +    }
    +}
    +
    + +

    Open a document

    + +

    Once you have the URI for a document, you can open it or do whatever else +you want to do with it.

    + +

    Bitmap

    + +

    Here is an example of how you might open a {@link android.graphics.Bitmap}:

    + +
    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;
    +}
    +
    + +

    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}. +

    + +

    Get an InputStream

    + +

    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:

    + +
    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();
    +}
    +
    + +

    Create a new document

    + +

    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:

    + + +
    +// 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);
    +}
    +
    + +

    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.

    + +

    Delete a document

    + +

    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:

    + +
    +DocumentsContract.deleteDocument(getContentResolver(), uri);
    +
    + +

    Edit a document

    + +

    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:

    + +
    +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);
    +}
    +
    + +

    Next, from {@link android.app.Activity#onActivityResult onActivityResult()} +(see Process results) 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:

    + +
    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();
    +    }
    +}
    + +

    Persist permissions

    + +

    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.

    + +

    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:

    + + +
    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);
    + +

    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.

    + +

    Writing a Custom Document Provider

    + +

    +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.

    + + +

    Manifest

    + +

    To implement a custom document provider, add the following to your application's +manifest:

    + +

    Here are excerpts from a sample manifest that includes a provider:

    + +
    <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>
    + +

    Supporting devices running Android 4.3 and lower

    + +

    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.

    + +

    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:

    + +
      +
    1. In your {@code bool.xml} resources file under {@code res/values/}, add +this line:
      <bool name="atMostJellyBeanMR2">true</bool>
    2. + +
    3. In your {@code bool.xml} resources file under {@code res/values-v19/}, add +this line:
      <bool name="atMostJellyBeanMR2">false</bool>
    4. + +
    5. Add an +activity +alias to disable the {@link android.content.Intent#ACTION_GET_CONTENT} intent +filter for versions 4.4 (API level 19) and higher. For example: + +
      +<!-- 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>
      +
      +
    6. +
    +

    Contracts

    + +

    Usually when you write a custom content provider, one of the tasks is +implementing contract classes, as described in the + +Content Providers 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:

    + + + +

    For example, here are the columns you might return in a cursor when +your document provider is queried for documents or the root:

    + +
    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,};
    +
    + +

    Subclass DocumentsProvider

    + +

    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:

    + + + +

    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.

    + +

    Implement queryRoots

    + +

    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}.

    + +

    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.

    + +
    +@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;
    +}
    + +

    Implement queryChildDocuments

    + +

    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}.

    + +

    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:

    + +
    @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;
    +}
    +
    + +

    Implement queryDocument

    + +

    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}. +

    + +

    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:

    + + +
    @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;
    +}
    +
    + +

    Implement openDocument

    + +

    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:

    + +
    @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);
    +    }
    +}
    +
    + +

    Security

    + +

    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:

    + +
    +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;
    +}
    +
    + +

    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.

    + +
    private void onLoginButtonClick() {
    +    loginOrLogout();
    +    getContentResolver().notifyChange(DocumentsContract
    +            .buildRootsUri(AUTHORITY), null);
    +}
    +
    diff --git a/docs/html/images/providers/storage_dataflow.png b/docs/html/images/providers/storage_dataflow.png new file mode 100644 index 0000000000000..ceb71cadb2717 Binary files /dev/null and b/docs/html/images/providers/storage_dataflow.png differ diff --git a/docs/html/images/providers/storage_datamodel.png b/docs/html/images/providers/storage_datamodel.png new file mode 100644 index 0000000000000..6864d37b08f08 Binary files /dev/null and b/docs/html/images/providers/storage_datamodel.png differ diff --git a/docs/html/images/providers/storage_photos.png b/docs/html/images/providers/storage_photos.png new file mode 100644 index 0000000000000..a83ebb14753c5 Binary files /dev/null and b/docs/html/images/providers/storage_photos.png differ diff --git a/docs/html/images/providers/storage_picker.png b/docs/html/images/providers/storage_picker.png new file mode 100644 index 0000000000000..a811c56c863e9 Binary files /dev/null and b/docs/html/images/providers/storage_picker.png differ