在了解storage access framework之前。我们先来看看android4.4中的一个特性。假设我们希望能选择android手机中的一张图片,通常都是发送一个Intent给对应的程序。一般这个程序是系统自带的图库应用(假设你的手机中有两个图库类的app非常可能会叫你选择一个),这个Intent通常是这样写的:
Intent intent=new Intent(Intent.ACTION_GET_CONTENT);//ACTION_OPEN_DOCUMENT
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/jpeg");
使用这种一种方法来选择图片在android4.4中会直接弹出一个非常美丽的界面。有点像一个文件管理器。事实上他比文件管理器更强大。他是一个内容提供器,能够依照文件夹一层一层的选择文件,也能够依照文件种类选择文件,比方图片、视频、音频等,还能够打开一个应用程序选择文件。界面例如以下:
--
--
事实上这是一个叫做documentsui的内置程序。由于它的manifest没有带LAUNCHER的activity所以不会显示在桌面上。
以下是正文:
Storage Access Framework
Android4.4中引入了Storage Access Framework存储訪问框架,简称(SAF)。
SAF为用户浏览手机中存储的内容提供了方便,这些内容不仅包含文档、图片。视频、音频、下载,并且还包含全部由特定ContentProvider(须具有约定的API)提供的内容。
无论这些内容来自于哪里,无论是哪个应用调用浏览系统文件内容的命令,系统都会用一个统一的界面让你去浏览。
这样的能力姑且叫做一种生态系统,云存储以及本地存储都能够通过实现DocumentsProvider来參与到这个系统中。而clientapp要使用SAF提供的服务仅仅需几行代码就可以。
SAF框架包含下面内容:
(1)Document provider文件内容提供方
这是一个特殊的content provider(内容提供方),他让一个存储服务(比方Google Drive)能够对外展示自己所管理的文件。一个Document provider事实上就是实现了DocumentsProvider的子类。document-provider的schema 和传统的文件存径格式一致,可是至于你的内容是怎么存储的全然取决于你自己,android系统中已经内置了几个这样的Document provider,比方关于下载、图片以及视频的Document provider。(注意这里的红色DocumentsProvider是一个类。而分开写的Document provider仅仅是一种描写叙述,由于翻译出来可能会让人忘了他的特殊身份。)
(2)clientapp
一个触发ACTION_OPEN_DOCUMENT或者ACTION_CREATE_DOCUMENTintent的client软件。通过触发ACTION_OPEN_DOCUMENT或者ACTION_CREATE_DOCUMENTclient能够接收来自于Document provider的内容。
(3)选择器Picker
选择器事实上就是一个类似于文件管理器的界面,并且是系统级别的界面,他提供了訪问满足client过滤条件的全部Document provider内容的通道。说的详细点选择器就是文章开头提到的documentsui程序。
SAF的一些特性:
用户能够浏览全部document provider提供的内容,不光是一个app。
提供了长期、持续的訪问document provider中文件的能力以及数据的持久化,用户能够实现加入、删除、编辑、保存document provider所维护的内容。
支持多用户以及暂时性的内容服务。比方USB storage providers仅仅有当驱动成功安装才会出现。
概要
SAF的核心是实现了DocumentsProvider的子类,即内容提供者(documentprovider)。documentprovider中数据是以传统的文件文件夹树组织起来的:
流程图
虽说documentprovider中数据是以传统的文件文件夹树组织起来的。可是那仅仅是对外表现的形式,至于你的数据在内部到底是怎么样(甚至全然杂乱无章)。全然取决于你自己,仅仅要你对外的接口可以通过DocumentsProvider的api訪问就行。
以下的流程图展示了一个photo应用使用SAF可能的结构:
从上图能够看出选择器Picker(System UI)是一个链接调用者与内容提供者的桥梁。它提供了一个UI同一时候也告诉了调用者能够选择哪些内容提供者,比方这里的DriveDocProvider、UsbDocProvider、CloundDocProvider。
当clientapp与Document provider之间的交互是在触发了ACTION_OPEN_DOCUMENT或者ACTION_CREATE_DOCUMENT intent之后,intent还能够进一步设置过滤条件:比方限制MIME type为’image’。
当intent触发之后选择器去寻找每个注冊了的provider,并将provider的符合条件的根文件夹显示出来。
选择器(即documentsui)为訪问不同形式、不同来源的文件提供了统一的界面。你能够看到我的文件形式能够是图片、视频,文件的内容能够是来自本地或者是Google Drive的云服务。
下图显示了用户在选择图片的时候点中了Google Drive的情况。
client是怎样调用的
在android4.3时代,假设你想从另外一个app中选择一个文件,比方从图库中选择一张图片文件,你必须触发一个intent比方ACTION_PICK或者ACTION_GET_CONTENT。然后在候选的app中选择一个app。从中获得你想要的文件,最关键的是被选择的app中要具有能为你提供文件的功能,假设一个不负责任的第三方开发人员注冊了一个恰恰符合你需求的intent,可是没有实现返回文件的功能,那么就会出现意想不到的错误。
在4.4中。你多了一个选择方式,你能够发送ACTION_OPEN_DOCUMENTintent来调用系统的documentsui来选择不论什么文件,不须要再依赖于其它的app了。
可是并非说ACTION_GET_CONTENT就全然没实用了,假设你仅仅是打开读取一个文件。ACTION_GET_CONTENT还是能够的,假设你是要有写入编辑的需求,那就用ACTION_OPEN_DOCUMENT。
注: 实际上在4.4系统中ACTION_GET_CONTENT启动的还是documentsui。
以下演示怎样用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); }
ACTION_OPEN_DOCUMENT intent发出以后documentsui会显示全部满足条件的document provider(显示的是他们的标题),以图片为例,事实上它相应的document provider是MediaDocumentsProvider(在系统源代码中),而訪问MediaDocumentsProvider的URi形式为com.android.providers.media.documents;
假设在intent filter中增加categoryCATEGORY_OPENABLE的条件,则显示结果仅仅有能够打开的文件。比方图片文件(思考一下 ,哪些是不能够打开的呢?);
假设设置intent.setType("image/*")则仅仅显示MIME type为image的文件。
获取返回的结果
返回结果通常是一个uri。数据保存在onActivityResult的第三个參数resultData中。通过resultData.getData()获取uri。
@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); } } }
获取元数据
一旦得到uri,你就能够用uri获取文件的元数据。以下演示了怎样得到元数据信息,并打印到log中。
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(); } }
还能够获得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;
获得输出流
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 intent来创建文件
// 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。
删除文件
前提是Document.COLUMN_FLAGS包括SUPPORTS_DELETE
DocumentsContract.deleteDocument(getContentResolver(), uri);实现自己的document provider
假设你希望自己应用的数据也能在documentsui中打开,你就须要写一个自己的document provider。以下介绍自己定义DocumentsProvider的步骤。
api 为19+
首先你须要在manifest文件里声明有这样一个Provider:
Provider的name为类名加包名,比方:
com.example.android.storageprovider.MyCloudProvider
Authority为包名+provider的类型名,如:
Com.example.android.storageprovider.documents
android:exported属性的值为ture
以下是一个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>
DocumentsProvider的子类
你至少要实现例如以下几个方法:
queryRoots()
queryChildDocuments()
queryDocument()
openDocument()
还有些其它的方法。但并非必须的。
以下演示一个实现訪问文件(file)系统的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; }queryDocument的实现
@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; }
为了更好的理解这篇文章,能够參考以下这些链接。
參考文章
https://developer.android.com/guide/topics/providers/document-provider.htm这篇文章的英文原文要翻墙
http://blog.csdn.net/huangyanan1989/article/details/17263203Android4.4中获取资源路径问题由于Storage Access Framework而引起的
https://github.com/iPaulPro/aFileChooser 一个文件管理器。在4.4中他是直接启用了documentsui
https://github.com/ianhanniballake/LocalStorage一个自己定义的DocumentsProvider
https://github.com/xin3liang/platform_packages_providers_MediaProvider 实现了查询多媒体文件的DocumentsProvider,包含查询图片,这个是系统里面的