Storage Access Framework

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.

Cloud or local storage services can participate in this ecosystem by implementing a 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.

The SAF includes the following:

  • Document provider—A content provider that allows a storage service (such as Google Drive) to reveal the files it manages. A document provider is implemented as a subclass of theDocumentsProvider 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.
  • Client app—A custom app that invokes theACTION_OPEN_DOCUMENT and/or ACTION_CREATE_DOCUMENT intent and receives the files returned by document providers.
  • Picker—A system UI that lets users access documents from all document providers that satisfy the client app's search criteria.

Some of the features offered by the SAF are as follows:

  • Lets users browse content from all document providers, not just a single app.
  • 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.
  • Supports multiple user accounts and transient roots such as USB storage providers, which only appear if the drive is plugged in.

Overview


The SAF centers around a content provider that is a subclass of the DocumentsProvider class. Within adocument 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:

  • Each document provider reports one or more "roots" which are starting points into exploring a tree of documents. Each root has a unique 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.
  • Under each root is a single document. That document points to 1 to N documents, each of which in turn can point to 1 to N documents.
  • Each storage backend surfaces individual files and directories by referencing them with a uniqueCOLUMN_DOCUMENT_ID. Document IDs must be unique and not change once issued, since they are used for persistent URI grants across device reboots.
  • Documents can be either an openable file (with a specific MIME type), or a directory containing additional documents (with the MIME_TYPE_DIR MIME type).
  • Each document can have different capabilities, as described by COLUMN_FLAGS. For example,FLAG_SUPPORTS_WRITEFLAG_SUPPORTS_DELETE, and FLAG_SUPPORTS_THUMBNAIL. The sameCOLUMN_DOCUMENT_ID can be included in multiple directories.

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 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 SAF to access stored data:

app

Figure 2. Storage Access Framework Flow

Note the following:

  • In the SAF, providers and clients don't interact directly. A client requests permission to interact with files (that is, to read, edit, create, or delete files).
  • The interaction starts when an application (in this example, a photo app) fires the intentACTION_OPEN_DOCUMENT or 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."
  • Once the intent fires, the system picker goes to each registered provider and shows the user the matching content roots.
  • 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.

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

picker

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.

picker

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 asACTION_PICK or 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 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.

ACTION_OPEN_DOCUMENT is not intended to be a replacement for ACTION_GET_CONTENT. The one you should use depends on the needs of your app:

  • Use 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.
  • Use 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.

This section describes how to write client apps based on the ACTION_OPEN_DOCUMENT andACTION_CREATE_DOCUMENT intents.

The following snippet uses 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:

  • When the app fires the ACTION_OPEN_DOCUMENT intent, it launches a picker that displays all matching document providers.
  • Adding the category CATEGORY_OPENABLE to the intent filters the results to display only documents that can be opened, such as image files.
  • The statement intent.setType("image/*") further filters to display only documents that have the image MIME data type.

Process Results

Once the user selects a document in the picker, onActivityResult() gets called. The URI that points to the selected document is contained in the resultData parameter. Extract the URI using 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 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 AsyncTask. Once you open the bitmap, you can display it in an ImageView.

Get an InputStream

Here is an example of how you can get an 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 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 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 Document.COLUMN_FLAGS contains SUPPORTS_DELETE, you can delete the document. For example:

DocumentsContract.deleteDocument(getContentResolver(), uri);

Edit a document

You can use the SAF to edit a text document in place. This snippet fires the ACTION_OPEN_DOCUMENT intent and uses the category 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 onActivityResult() (see Process results) you can call code to perform the edit. The following snippet gets a FileOutputStream from the 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 callgetContentResolver().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 SAF 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:

  • A target of API level 19 or higher.
  • <provider> element that declares your custom storage provider.
  • The name of your provider, which is its class name, including package name. For example:com.example.android.storageprovider.MyCloudProvider.
  • The name of your authority, which is your package name (in this example,com.example.android.storageprovider) plus the type of content provider (documents). For example,com.example.android.storageprovider.documents.
  • The attribute android:exported set to "true". You must export your provider so that other apps can see it.
  • The attribute android:grantUriPermissions set to "true". This setting 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 Persist permissions.
  • The MANAGE_DOCUMENTS permission. By default a provider is available to everyone. Adding this permission restricts your provider to the system. This restriction is important for security.
  • The 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,android:enabled="@bool/atLeastKitKat". In addition to including this attribute in the manifest, you need to do the following:
    • In your bool.xml resources file under res/values/, add this line:
      <bool name="atLeastKitKat">false</bool>
    • In your bool.xml resources file under res/values-v19/, add this line:
      <bool name="atLeastKitKat">true</bool>
  • An intent filter that includes the android.content.action.DOCUMENTS_PROVIDER action, so that your provider appears in the picker when the system searches for providers.

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"
            android:enabled="@bool/atLeastKitKat">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>
    </application>

</manifest>
Supporting devices running Android 4.3 and lower

The ACTION_OPEN_DOCUMENT intent is only available on devices running Android 4.4 and higher. If you want your application to support ACTION_GET_CONTENT to accommodate devices that are running Android 4.3 and lower, you should disable the ACTION_GET_CONTENT intent filter in your manifest for devices running Android 4.4 or higher. A document provider and 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 ACTION_GET_CONTENT intent filter for devices running Android version 4.4 or higher:

  1. In your bool.xml resources file under res/values/, add this line:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. In your bool.xml resources file under res/values-v19/, add this line:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. Add an activity alias to disable the 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>

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 public final class that contains constant definitions for the URIs, column names, MIME types, and other metadata that pertain to the provider. The SAF 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 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 DocumentsProvider for details.

Implement queryRoots

Your implementation of queryRoots() must return a Cursor pointing to all the root directories of your document providers, using columns defined in DocumentsContract.Root.

In the following snippet, the 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 queryChildDocuments() must return a Cursor that points to all the files in the specified directory, using columns defined in 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 queryDocument() must return a Cursor that points to the specified file, using columns defined in DocumentsContract.Document.

The queryDocument() method returns the same information that was passed in 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 openDocument() to return a ParcelFileDescriptor representing the specified file. Other apps can use the returned 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 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 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 getContentResolver().notifyChange(). Remember the 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 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);

}

Android 4.4系统(API级别19)推出存储访问框架(SAF)。新加坡武装部队变得非常简单,为用户在其所有自己喜欢的文件存储提供商的浏览和打开文档,图像和其他文件。一个标准的,易于使用的用户界面允许用户在浏览整个应用程序和供应商一致的方式文件和访问近期]。

云或本地存储服务可以通过实现参与这个生态系统DocumentsProvider封装了他们的服务。需要访问一个供应商的文档可以使用SAF集成,只需几行代码客户端应用程序。

该SAF包括以下内容:

  • 文档提供 -A内容提供商,它允许存储服务(如谷歌驱动器)来揭示它管理的文件。文档提供者实现为的一个子类DocumentsProvider类。该文件提供商的架构是基于传统文件的层次结构,但如何将文档提供物理存储的数据是由你。Android平台包括几个内置文档提供商,如下载,图片和视频。
  • 客户端应用程序 -A调用自定义应用程序 ACTION_OPEN_DOCUMENT和/或 ACTION_CREATE_DOCUMENT意图和接收文件供应商返回的文件。
  • 选择器 -A系统UI,让所有的文件提供满足客户端应用程序的搜索条件的用户访问文件。

是一些由SAF提供的功能如下:

  • 允许用户从所有文件提供者,而不仅仅是一个单一的应用程序浏览内容。
  • 它使你的应用,以获得长期的,持续的访问由文件供应商所拥有的文件。通过这个接入用户可以添加,编辑,保存和删除文件的供应商。
  • 支持多个用户帐户和瞬态根,如USB存储提供商,这只有在驱动器插入出现。

概观


该中心SAF周围是的一个子类内容提供商DocumentsProvider类。内的文件提供者,数据被构造为传统的文件层次结构:

数据模型

图1.文档提供数据模型。根指向一个单一的文件,然后启动扇出整个树。

请注意以下几点:

  • 每个文档提供报告的一个或多个“根”这是起点为探索文档的树。每一根都有一个唯一COLUMN_ROOT_ID,它指向一个文件(一个目录),表示根目录下的内容。根是设计动态,支持使用情况下,像多个帐户,短暂的USB存储设备或用户登录/注销。
  • 在每一根都是一个单独的文档。该文件指向1至Ñ文件,其中每个依次可以指向1至Ñ文档。
  • 每个存储后端通过一个独特的引用它们表面单个文件和目录 COLUMN_DOCUMENT_ID。文档ID必须是唯一的,因为它们是用来在设备重新启动持久URI补助没有改变,一旦发出。
  • 文件可以是可打开的文件(具有特定MIME类型),或含有额外的文件的目录(用 MIME_TYPE_DIR MIME类型)。
  • 每个文档可以具有不同的能力,如通过描述 COLUMN_FLAGS。例如,FLAG_SUPPORTS_WRITEFLAG_SUPPORTS_DELETE和 FLAG_SUPPORTS_THUMBNAIL。同一COLUMN_DOCUMENT_ID可以包含在多个目录。

控制流


如上所述,文档提供者数据模型是基于传统的文件的层次结构。但是,您可以物理存储你的数据,只要你喜欢,只要它可以通过访问DocumentsProvider API。例如,你可以使用你的数据基于标签的云存储。

图2示出的相片应用可能如何使用SAF访问存储的数据,例如:

应用

图2.存储访问架构流程

请注意以下几点:

  • 在SAF,供应商和客户不直接交互。客户端请求的权限与文件(即,阅读,编辑,创建或删除文件)进行交互。
  • 当一个应用程序(在此例中,一个照片应用)触发的意图的相互作用开始 ACTION_OPEN_DOCUMENTACTION_CREATE_DOCUMENT。这样做的目的可能包括过滤器,以进一步细化标准,例如,“给我说有'形象'MIME类型的所有打开的文件。”
  • 一旦意图火灾,该系统选择器前进到每个已注册的提供者和显示用户的匹配内容根源。
  • 在选择器为用户提供了访问文档,即使底层文件提供者可能是非常不同的标准接口。例如,图2显示了谷歌驱动器提供商,USB提供商和云服务提供商。

图3显示了用户在其中搜索图像选择了谷歌驱动器帐户选择器:

选择器

图3.选择器

当用户选择谷歌Drive都显示的图像,如图4从这一点上,用户可以与之互动以任何方式被提供者和客户机应用程序的支持。

选择器

图4.图片

编写客户端应用程序


在Android 4.3和更低的,如果你希望你的应用程序可以从另一台应用程序文件时,它必须调用的意图,如ACTION_PICK 或ACTION_GET_CONTENT。然后,用户必须选择其中一个应用程序来选择一个文件,并选择应用程序必须为用户提供的用户界面来浏览,并从可用的文件挑。

在Android 4.4及更高版本,可以选择使用的附加 ​​选项 ACTION_OPEN_DOCUMENT意图,其中显示由该允许用户浏览其他应用程序已提供的所有文件系统控制的选择器UI。从该单个用户界面中,用户可以从任何所支持的应用程序的选择一个文件。

ACTION_OPEN_DOCUMENT不旨在是用于替换ACTION_GET_CONTENT。你应该使用一个取决于你的应用程序的需求:

  • 使用ACTION_GET_CONTENT如果你希望你的应用程序只需读取/导入数据。用这种方法,应用导入的数据,拷贝诸如图像文件。
  • 使用ACTION_OPEN_DOCUMENT如果你希望你的应用,以获得长期的,持续的访问由文件供应商所拥有的文件。一个例子是一个照片编辑应用程序,允许用户编辑存储在文档图像提供商。

本节将介绍如何根据编写客户端应用程序 ACTION_OPEN_DOCUMENT和 ACTION_CREATE_DOCUMENT意图。

下面的代码片断使用ACTION_OPEN_DOCUMENT 搜索包含图像文件的文件提供者:

私人 静态 最终 INT READ_REQUEST_CODE =  42 ; 
...... 
/ ** 
 *火灾的意图旋转起来的“文件选择器”UI并选择图像。
 * / 
公共 无效performFileSearch () { 

    // ACTION_OPEN_DOCUMENT是选择一个文件的意图通过系统的文件
    //浏览器。
    意向意图=   的Intent 意向ACTION_OPEN_DOCUMENT ); 

    //过滤器,只显示效果,可以“打开”,如
    //文件(而不是联系人或时区列表)
    意图addCategory 意向CATEGORY_OPENABLE ); 

    //过滤器只显示图像,使用图像MIME数据类型
    //如果有人想搜索的Ogg Vorbis文件类型将是“音频/ OGG。” 
    //要搜索通过安装存储供应商提供所有文件,
    //这将是“* / *” 
    的意图的setType “图像/ *” ); 

    startActivityForResult 意图READ_REQUEST_CODE ); 
}

请注意以下几点:

  • 当应用程序触发ACTION_OPEN_DOCUMENT 意图,它将启动一个显示所有匹配的文件提供者选择器。
  • 添加类别CATEGORY_OPENABLE到意图对结果进行过滤,以便仅显示可以打开文件,如图像文件。
  • 声明intent.setType(“图像/ *”)进一步过滤仅显示有图像MIME数据类型的文档。

处理结果

一旦用户选择器中选择一种文件 的onActivityResult()被调用。指向被选择的文档的URI包含在resultData参数。提取URI使用的getData() 。一旦你拥有它,你可以用它来 ​​获取用户想要的文件。例如:

@覆盖
公共 无效的onActivityResult INT requestCode  INT resultCode为
        意图resultData  { 

    //该ACTION_OPEN_DOCUMENT意图是与请求代码发送
    // READ_REQUEST_CODE。如果这里看到的请求代码不匹配,这是
    //应对一些其他的意图,和下面的代码应该不运行。

    如果 requestCode == READ_REQUEST_CODE && resultCode为==  活动RESULT_OK  { 
        //该由用户选择的文件不会在意图被返回。
        //相反,一个URI该文件将被包含在返回意图
        提供本方法作为参数//。
        //拉该URI使用resultData.getData( )。
        开放的URI =  ; 
        如果 resultData !=   { 
            URI = resultData 的getData (); 
            登录TAG  “URI:”  + URI 的toString ()); 
            showImage URI ); 
        } 
    } 
}

检查文档元数据

一旦你的URI为一个文件,你可以访问它的元数据。这段代码抓住由URI指定的文件元数据,并记录它:

公共 无效dumpImageMetaData 开放的URI  { 

    //查询,因为它仅适用于一个单一的文件,将只返回
    //一行。有没有必要进行筛选,排序,或选择字段,因为我们希望
    //各个领域的一个文件。
    光标光标= getActivity ()。getContentResolver () 
            查询URI      ); 

    尝试 { 
    如果游标有0行// moveToFirst()返回false。非常方便的
    //“如果有什么看,看它」条件。
        如果 指针!=   && 光标moveToFirst ()) { 

            //注意这就是所谓的”显示名称“。这是
            //提供商特有的,并且可能不一定是该文件 
            

             产品名称:“  + 显示名); 

            INT sizeIndex = 光标getColumnIndex OpenableColumns SIZE ); 
            //如果大小是未知的,存储的值是空但由于一个。
            在Java中// INT不能为空,则该行为实现特定的,
            //这仅仅是“不可预知”看中词那么作为
            //一个规则,检查其分配给一个int之前的空这将
            //经常发生:存储API允许远程文件,其
            //大小可能不被当地人称为。
            字符串大小=  ; 
            如果 (!指针参考isNull sizeIndex )) { 
                //技术上的列存储int,但cursor.getString()
                //会自动进行转换
                的大小= 光标的getString sizeIndex ); 
            }  否则 { 
                大小=  “未知” ; 
            } 
            登录TAG  “大小”  + 大小); 
        } 
    }  最后 { 
        光标密切(); 
    } 
}

打开一个文档

一旦你的URI为一个文件,你可以打开它,或者你想用它做其他。

位图

下面是如何你可能会打开一个示例位图

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

请注意,你不应该做的UI线程此操作。这样做的背景下,使用AsyncTask的。一旦你打开了位图,您可以在显示它的ImageView

获取一个InputStream

这里是你如何能得到一个例子的InputStream从URI。在这个片段中,该文件的行被读入一个字符串:

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

创建一个新文档

您的应用程序可以创建在使用一个文档提供一个新的文档 ACTION_CREATE_DOCUMENT 意图。要创建你给你的意图MIME类型和文件名 ​​的文件,并具有独特的请求的代码启动。其余的是照顾你:

//这里是你可能如何调用该方法的一些例子。
//第一个参数是MIME类型,第二个参数是名字
所创建的文件//:
// 
//的CreateFile(“text / plain的“,”foobar.txt“); 
//的CreateFile(”图像/ PNG“,”mypicture.png“); 

//唯一的请求 
    

   
      

    过滤器,只显示效果,可以“打开”,如
    //文件(而不是联系人或时区列表)
    的意图addCategory 意向CATEGORY_OPENABLE ); 

    //与请求MIME创建一个文件



一旦你创建一个新文档,你可以得到它的URI 的onActivityResult() ,这样就可以继续写吧。

删除文件

如果你有URI为一个文件和文 ​​档的 Document.COLUMN_FLAGS 包含 SUPPORTS_DELETE,您可以删除该文件。例如:

DocumentsContract deleteDocument getContentResolver (),URI );

编辑文档

您可以使用SAF到位,编辑一个文本文件。这段代码触发ACTION_OPEN_DOCUMENT意图和使用类别CATEGORY_OPENABLE以只显示可打开的文档。它还过滤器只显示文本文件:

私人 静态 最终 INT EDIT_REQUEST_CODE =  44 ; 
/ ** 
 *打开一个文件进行写入和一些文本追加到它
 * / 
 私人 无效editDocument () { 
    // ACTION_OPEN_DOCUMENT是选择通过系统的文件的意图
    //文件浏览器。
    意向意图=   的Intent 意向ACTION_OPEN_DOCUMENT ); 

    //过滤器,只显示效果,可以“打开”,如
    //文件(而不是联系人或时区列表)
    的意图addCategory 意向CATEGORY_OPENABLE ); 

    //过滤器只显示文本文件。
    意图的setType “text / plain的” ); 

    startActivityForResult 意图EDIT_REQUEST_CODE ); 
}

接下来,从的onActivityResult() (请参阅处理结果),你可以调用代码来执行编辑。下面的代码片段得到一个FileOutputStream中 从ContentResolver的。在默认情况下它使用“写入”模式。这是最好的做法,要求您需要访问最少的,所以不要问读/写,如果你需要的是写:

private  void alterDocument ( Uri uri )  { 
    try  { 
        ParcelFileDescriptor pfd = getActivity (). getContentResolver (). 
                openFileDescriptor ( uri ,  "w" ); 
        FileOutputStream fileOutputStream = 
                new  FileOutputStream ( pfd . getFileDescriptor ()); 
        fileOutputStream . write (( "Overwritten通过MyCloud在“  + 
                系统的currentTimeMillis () +  ”\ n“ 的getBytes ()); 
        //让文件提供者知道你通过关闭完成stream. 
        fileOutputStream . close (); 
        pfd . close (); 
    }  catch  ( FileNotFoundException e )  { 
        e . printStackTrace (); 
    }  catch  ( IOException e )  { 
        e . printStackTrace (); 
    } 
}

坚持权限

当你的应用程序打开进行读取或写入文件时,系统会给你的应用该文件的URI权限授予。它会一直持续到用户的设备重新启动。但是,假设你的应用程序是一个图像编辑应用程序,并希望用户能够从你的应用程序访问他们所编辑的最后5张图片,直接。如果用户的设备重新启动后,你必须给用户发送回系统选择器来查找文件,这显然是不理想的。

为了防止这种情况发生,你可以坚持的系统给您的应用程序的权限。实际上,你的应​​用程序“需要”,该系统提供了持久化的URI权限授予。这使得通过您的应用程序文件的用户继续访问,即使该设备已经重新启动:

final  int takeFlags = intent . getFlags () 
            &  ( Intent . FLAG_GRANT_READ_URI_PERMISSION 
            |  Intent . FLAG_GRANT_WRITE_URI_PERMISSION ); 
//检查最新的数据。
getContentResolver ()。takePersistableUriPermission URI takeFlags );

还有最后一步。您可能已经保存您的应用程序访问最新的URI,但它们可能不再有效,另一个应用程序可能已删除或修改的文件。因此,你应该总是调用 getContentResolver()。takePersistableUriPermission()来检查最新的数据。

编写自定义文档提供


如果您正在开发,对于文件(如云端储存服务)提供存储服务的应用程序,你可以通过编写自定义文档提供使可通过SAF文件。本节介绍了如何做到这一点。

表现

要实现自定义文档提供商,以下内容添加到您的应用程序的清单:

  • API级别19或更高的目标。
  • 一个<提供商>元素声明自定义的存储供应商。
  • 你的供应商,这是它的类名,包括包名的名称。例如:com.example.android.storageprovider.MyCloudProvider
  • 你的权威,这是您的包名称的名称(在该例子中, com.example.android.storageprovider)加的内容提供者的类型(文件)。例如,com.example.android.storageprovider.documents
  • 该属性的android:导出设置为“真”。您必须导出你的供应商,使其他应用程序可以看到它。
  • 属性安卓:grantUriPermissions设置为 “真”。此设置允许系统授予的其他应用程序访问内容提供商。对于如何坚持赠款为特定文档的讨论,参见坚持权限
  • MANAGE_DOCUMENTS许可。默认情况下,供应商是提供给大家。添加该权限限制你的供应商系统。此限制是出于安全很重要。
  • 机器人:启用属性设置为在资源文件中定义一个布尔值。这个属性的目的是禁止在运行Android 4.3或更低的设备供应商。例如,机器人:启用=“@布尔/ atLeastKitKat” 。除了 ​​包括在清单此属性,你需要做到以下几点:
    • 在你bool.xml下的资源文件RES /价值/添加此行:
      <布尔 = “atLeastKitKat” > </布尔>
    • 在你bool.xml下的资源文件RES /值-V19 /添加此行:
      <布尔 = “atLeastKitKat” > 真正</布尔>
  • 一个意图过滤器,其中包括 android.content.action.DOCUMENTS_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" 
            android:enabled = "@bool/atLeastKitKat" > 
            <intent-filter> 
                <action  android:name = "android.content.action.DOCUMENTS_PROVIDER"  /> 
            </intent-filter> 
        </provider> 
    </application> 

</manifest>
运行Android 4.3和更低的配套器件

该 ACTION_OPEN_DOCUMENT意图是仅适用于运行Android 4.4及更高版本的设备可用。如果你希望你的应用程序支持ACTION_GET_CONTENT ,以适应正在运行的是Android 4.3和更低的设备,您应该禁用ACTION_GET_CONTENT在你的清单意图过滤器运行Android 4.4或更高版本的设备。文档提供者和ACTION_GET_CONTENT应考虑相互排斥。如果同时支持他们两个,你的应用程序将在系统选择器UI中出现两次,提供访问您的存储数据的两种不同方式。这会让用户感到困惑。

下面是禁用的推荐方式 ACTION_GET_CONTENT运行Android版 ​​本4.4或更高版本的设备意图过滤器:

  1. 在你bool.xml下的资源文件RES /价值/添加此行:
    <布尔 = “atMostJellyBeanMR2” > 真正</布尔>
  2. 在你bool.xml下的资源文件RES /值-V19 /添加此行:
    <布尔 = “atMostJellyBeanMR2” > </布尔>
  3. 添加 活动别名禁用ACTION_GET_CONTENT 4.4版本的意图过滤器(API等级19)高。例如:
    <! -该活动别名被添加,使GET_CONTENT意图过滤器
         可以被禁止用于建立在API水平等于或高于19。 --> 
    <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>

合同

通常,当你写一个自定义的内容提供商,任务之一是实施合同类,如所描述的 内容提供商的开发人员指南。合同类是公共最后,它包含的URI,涉及到供应商常量定义,列名,MIME类型和其他元数据类。新加坡武装部队提供了这些合同类你,所以你不需要编写自己的:

例如,这里有您可能会在当你的文档提供查询的文档或根光标返回列:

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

子类DocumentsProvider

编写定制文档提供下一步是继承抽象类DocumentsProvider。至少,你需要实现以下方法:

这些都是你实现严格要求的唯一方法,但是还有更多你可能想。见DocumentsProvider 了解详情。

实施queryRoots

你的实现queryRoots()必须返回一个指针指向文档提供的所有根目录,使用定义的列DocumentsContract.Root

在下面的片段中,投影参数表示调用者想要找回特定字段。该片断创建一个新的光标,并增加了一个行吧,一个根,一个顶级目录,如下载或图像。大多数供应商只能有一个根。你可能有一个以上的,例如,在多个用户帐户的情况下。在这种情况下,只要添加一个第二排的光标。

@覆盖
公共 光标queryRoots 字符串[] 投影 抛出 FileNotFoundException异常 { 

    //使用任何所要求的领域,或默认创建一个游标
    //如果投影“投影”为null。
    最终 MatrixCursor 结果= 
             MatrixCursor resolveRootProjection 投影)) ; 

    //如果用户没有登录,则返回一个空的根指针。这消除了我们
    //从完全名单提供商。
    如果 (!isUserLoggedIn ()) { 
        返回结果; 
    } 

    //这是可能的有多个根(如多个帐户的
    //相同的应用程序) -只需添加多个光标行。
    //构造一行一个名为根 
     



    FLAG_SUPPORTS_CREATE指根支持下至少一个目录
    //创建文档。FLAG_SUPPORTS_RECENTS意味着应用程序的最
    //最近使用过的文档将在“最近”类别中显示。
    // FLAG_SUPPORTS_SEARCH允许用户搜索的所有文档应用
    // 
 
            
            

    COLUMN_TITLE是根标题(如画廊,驱动器)。
    行添加COLUMN_TITLE 的getContext ()的getString ř 字符串标题)); 

    //一旦它共享这个文件的ID不能改变
    行添加COLUMN_DOCUMENT_ID getDocIdForFile mBaseDir )); 

    //子MIME类型用于过滤的根源和目前唯一的
    某处包含在其文件所需的类型//用户根




    
实施queryChildDocuments

你的实现 queryChildDocuments() 必须返回一个指针指向指定目录下的所有文件,使用定义的列DocumentsContract.Document

当你选择的选择器UI应用程序根目录时调用此方法。它得到的根目录下的一个目录的子文档。它可以在文件层次结构的任何级别被调用,而不仅仅是根。这段代码使得与请求的列的新光标,然后将有关的父目录,将光标每一个直系子女的信息。一个孩子可以是图片,另一个目录的任何文件:

@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 ())  { 
        //添加文件的显示名称,MIME类型,大小,等等。
        includeFile 结果 文件); 
    } 
    返回结果; 
}
实施queryDocument

你的实现 queryDocument() 必须返回一个指针指向指定的文件,使用定义的列DocumentsContract.Document

queryDocument() 方法返回中传递相同的信息 queryChildDocuments() ,但对于一个特定的文件:

@Override 
public  Cursor queryDocument ( String documentId ,  String [] projection )  throws 
        FileNotFoundException  { 

    //与所请求的突起,或默认创建光标projection. 
    final  MatrixCursor result =  new 
            MatrixCursor ( resolveDocumentProjection ( projection )); 
    includeFile ( result , documentId ,  null ); 
    return result ; 
}
实现使用openDocument

您必须实现使用openDocument()返回一个ParcelFileDescriptor代表指定的文件。其他应用程序可以使用返回ParcelFileDescriptor 流数据。当用户选择一个文件和客户端应用程序请求访问它通过调用系统调用这个方法 openFileDescriptor() 。例如:


  
                                          
                                          
         
     模式:“  + 模式); 
    //这是确定这种方法做网络运营下载文档,
    //你只要定期检查CancellationSignal如果你有一个。
    //非常大的文件从网络转移,更好的解决方案可能
    //是管道或插座(参见ParcelFileDescriptor的帮手 

     

        
     
        如果该文件是写打开附加密切监听 
         
              
             
                          
                
                  

                    更新与云服务器的文件,客户端就完成了。
                    //写作。
                    登录TAG  “用ID的文件”  + 
                    documentId +  “已关闭!
                    时间”  + 
                    “更新服务器。” ); 
                } 

            } ); 
        }  赶上 IOException异常ē  { 
            抛出 新的 FileNotFoundException异常“无法用身份证打开文档” 
            + documentId +  “模式和”  + 模式); 
        } 
    }  其他 { 
        返回 ParcelFileDescriptor 开放文件accessMode ); 
    } 
}

安全

假设你的文档提供一个密码保护的云存储服务,并要确保用户在你开始分享他们的文件之前登录。什么应该您的应用程序做,如果用户没有登录?解决的办法是在你的实现返回零根queryRoots() 。也就是说,一个空的根光标:

公共 光标queryRoots 字符串[] 投影 抛出 FileNotFoundException异常 { 
... 
    //如果用户没有登录,则返回一个空的根指针。这消除了我们
    //从完全名单提供商。
    如果 (!isUserLoggedIn ()) { 
        返回结果; 
}

另一步骤是调用getContentResolver()。有NotifyChange() 。还记得DocumentsContract?我们用它来 ​​使这个URI。下面的代码片断告诉系统查询您的文档提供每当用户的登录状态变化的根源。如果用户没有登录,将呼叫queryRoots()返回一个空光标,如上图所示。这保证了如果用户登录到提供者的提供者的文件才可用。

private  void onLoginButtonClick ()  { 
    loginOrLogout (); 
    getContentResolver (). notifyChange ( DocumentsContract 
            . buildRootsUri ( AUTHORITY ),  null ); 
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值