您的位置:首页 > 移动开发 > Android开发

Storage Access Framework

2016-04-03 19:51 671 查看
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 the
DocumentsProvider
class.
The document-provider schema is based on a traditional file hierarchy, though how your document provider physically stores data is up to you. The Android platform includes several built-in document providers, such as Downloads, Images, and Videos.
Client app—A custom app that invokes the
ACTION_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:



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 unique
COLUMN_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_WRITE
,
FLAG_SUPPORTS_DELETE
,
and
FLAG_SUPPORTS_THUMBNAIL
. The same
COLUMN_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:



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 intent
ACTION_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:



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
ACTION_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
and
ACTION_CREATE_DOCUMENT
intents.


Search for documents

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 call
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 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.
A
<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:

In your
bool.xml
resources file under
res/values/
, add this line:
<bool name="atMostJellyBeanMR2">true</bool>


In your
bool.xml
resources file under
res/values-v19/
, add this line:
<bool name="atMostJellyBeanMR2">false</bool>


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:

DocumentsContract.Document

DocumentsContract.Root


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:

)]queryRoots()

, java.lang.String)]queryChildDocuments()

)]queryDocument()

openDocument()


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
, java.lang.String)]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
, java.lang.String)]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)。新加坡武装部队变得非常简单,为用户在其所有自己喜欢的文件存储提供商的浏览和打开文档,图像和其他文件。一个标准的,易于使用的用户界面允许用户在浏览整个应用程序和供应商一致的方式文件和访问近期]。云或本地存储服务可以通过实现参与这个生态系统[code]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_WRITE
FLAG_SUPPORTS_DELETE
FLAG_SUPPORTS_THUMBNAIL
。同一
COLUMN_DOCUMENT_ID
可以包含在多个目录。

控制流

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

图2.存储访问架构流程请注意以下几点:在SAF,供应商和客户不直接交互。客户端请求的权限与文件(即,阅读,编辑,创建或删除文件)进行交互。
当一个应用程序(在此例中,一个照片应用)触发的意图的相互作用开始
ACTION_OPEN_DOCUMENT
ACTION_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 );
}[/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或更高版本的设备意图过滤器:在你
bool.xml
下的资源文件
RES /价值/
添加此行:
<布尔 名= “atMostJellyBeanMR2” > 真正</布尔>

在你
bool.xml
下的资源文件
RES /值-V19 /
添加此行:
<布尔 名= “atMostJellyBeanMR2” > 假</布尔>

添加 活动别名禁用
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类型和其他元数据类。新加坡武装部队提供了这些合同类你,所以你不需要编写自己的:
DocumentsContract.Document

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

子类DocumentsProvider

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

, java.lang.String)]queryChildDocuments()

)]queryDocument()

使用openDocument()

这些都是你实现严格要求的唯一方法,但是还有更多你可能想。见
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

你的实现
, java.lang.String)]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()
方法返回中传递相同的信息
, java.lang.String)]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 );
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息