该中心SAF周围是DocumentsProvider类的子类内容提供商。内的文件提供者,数据被构造为传统的文件层次结构:
图1.文档提供数据模型。根指向一个单一的文件,然后启动扇出整个树。
请注意以下几点:
每个文档提供报告的一个或多个“根”这是起点为探索文档的树。每一根都有一个唯一COLUMN_ROOT_ID,它指向一个文件(一个目录),表示根目录下的内容。根是设计动态,支持使用情况下,像多个帐户,短暂的USB存储设备或用户登录/注销。
在每一根都是一个单独的文档。该文件指向1至N的文件,其中每个依次可以指向1至N的文档。
每个存储后端通过一个独特的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或行动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搜索包含图像文件的文件提供者:
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); }请注意以下几点:
@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); } } }检查文档元数据
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(); } }打开一个文档
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的显示。
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(); }创建一个新文档
// 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); }一旦你创建一个新文档,你可以得到其的onActivityResult(URI),这样就可以继续写吧。
DocumentsContract.deleteDocument(getContentResolver(), uri);编辑文档
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); }接下来,从的onActivityResult()(请参阅处理结果),你可以调用代码来执行编辑。下面的代码片段获取从ContentResolver的一个FileOutputStream。在默认情况下它使用“写入”模式。这是最好的做法,要求您需要访问最少的,所以不要问读/写,如果你需要的是写:
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(); } }坚持权限
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);还有最后一步。您可能已经保存您的应用程序访问最新的URI,但它们可能不再有效,另一个应用程序可能已删除或修改的文件。因此,你应该总是调用getContentResolver()。takePersistableUriPermission()来检查最新的数据。
<bool name="atLeastKitKat">false</bool>In your
bool.xml
resources file under
res/values-v19/
, add this line:
<bool name="atLeastKitKat">true</bool>一个意图过滤器,其中包括android.content.action.DOCUMENTS提供商的行动,让你的供应商在系统搜索提供商出现在选择器。
<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>运行Android4.3和更低的配套器件
<bool name="atMostJellyBeanMR2">true</bool>In your
bool.xml
resources file under
res/values-v19/
, add this line:
bool name="atMostJellyBeanMR2">false</bool>
添加活动别名禁用4.4版本的ACTION_GET_CONTENT意图过滤器(API等级19)高。 例如:
<!-- 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>合同
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
@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; }实施queryChildDocuments
@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; }
@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; }实现使用openDocument
@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); } }安全
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; }另一步骤是调用getContentResolver()。有NotifyChange()。还记得DocumentsContract? We'are用它来使这个URI。下面的代码片断告诉系统查询您的文档提供每当用户的登录状态变化的根源。如果用户没有登录,打电话查询根()返回一个空光标,如上图所示。这保证了如果用户登录到提供者的提供者的文件才可用。
private void onLoginButtonClick() { loginOrLogout(); getContentResolver().notifyChange(DocumentsContract .buildRootsUri(AUTHORITY), null); }