在了解 storage access framework 之前,我们先来看看 android4.4 中的一个特性。如果我们希望能选择 android 手机中的一张图片,通常都是发送一个 intent 给相应的程序,一般这个程序是系统自带的图库应用(如果你的手机中有两个图库类的 app 很可能会叫你
在了解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来参与到这个系统中。而客户端app要使用saf提供的服务只需几行代码即可。
saf框架包括以下内容:
(1)document provider文件内容提供方
这是一个特殊的content provider(内容提供方),他让一个存储服务(比如google drive)可以对外展示自己所管理的文件。一个document provider其实就是实现了documentsprovider的子类。document-provider的schema 和传统的文件存径格式一致,但是至于你的内容是怎么存储的完全取决于你自己,android系统中已经内置了几个这样的document provider,比如关于下载、图片以及视频的document provider。(注意这里的红色documentsprovider是一个类,而分开写的document provider只是一种描述,因为翻译出来可能会让人忘了他的特殊身份。)
(2)客户端app
一个触发action_open_document或者action_create_documentintent的客户端软件。通过触发action_open_document或者action_create_document客户端可以接收来自于document provider的内容。
(3)选择器picker
选择器其实就是一个类似于文件管理器的界面,而且是系统级别的界面,他提供了访问满足客户端过滤条件的所有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。
当客户端app与document provider之间的交互是在触发了action_open_document或者action_create_document intent之后,intent还可以进一步设置过滤条件:比如限制mime type为’image’。
当intent触发之后选择器去寻找每一个注册了的provider,并将provider的符合条件的根目录显示出来。
选择器(即documentsui)为访问不同形式、不同来源的文件提供了统一的界面,你可以看到我的文件形式可以是图片、视频,文件的内容可以是来自本地或者是google drive的云服务。
下图显示了用户在选择图片的时候点中了google drive的情况。
客户端是如何调用的
在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。
@overridepublic 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的例子写法:
... ....
documentsprovider的子类
你至少要实现如下几个方法:
queryroots()
querychilddocuments()
querydocument()
opendocument()
还有些其他的方法,但并不是必须的。
下面演示一个实现访问文件(file)系统的documentsprovider的大致写法。
queryroots的实现:@overridepublic 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的实现@overridepublic 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的实现
@overridepublic 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,包括查询图片,这个是系统里面的