在了解 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_DOCUMENT intent 的客户端软件。通过触发 ACTION_OPEN_DOCUMENT 或者 ACTION_CREATE_DOCUMENT 客户端可以接收来自于 Document provider 的内容。
( 3 )选择器 Picker
选择器其实就是一个类似于文件管理器的界面,而且是系统级别的界面,他提供了访问满足客户端过滤条件的所有 Document provider 内容的通道。 说的具体点选择器就是文章开头提到的 documentsui 程序 。
SAF 的一些特性:
用户可以浏览所有 d ocument provider 提供的内容,不光是一个 app 。
提供了长期、持续的访问 d ocument provider 中文件的能力以及数据的持久化,用户可以实现添加、删除、编辑、保存 d ocument provider 所维护的内容。
支持多用户以及临时性的内容服务,比如 USB storage providers 只有当驱动安装成功才会出现。
概要
SAF的核心是实现了 DocumentsProvider 的 子类,即内容提供者 ( d ocument pro vider )。 document pro vider中数据是以传统的文件目录树组织起来的:
流程图
虽说 document pro vider中数据是以传统的文件目录树组织起来的,但是那只是对外表现的形式,至于你的数据在内部究竟是怎么样(甚至完全杂乱无章),完全取决于你自己,只要你对外的接口能够通过 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中选择一个文件,比如从图库中选择一张图片文件,你必须触发一个int ent 比如 ACTION_PICK 或者 ACTION_GET_CONTENT 。然后在候选的 app 中选择一个 app ,从中获得你想要的文件,最关键的是被选择的 app 中要具有能为你提供文件的功能,如果一个不负责任的第三方开发者注册了一个恰恰符合你需求的 intent ,但是没有实现返回文件的功能,那么就会出现意想不到的错误。
在 4.4 中,你多了一个选择方式,你可以发送 ACTION_OPEN_DOCUMENT intent 来调用系统的 documentsui 来选择任何文件,不需要再依赖于其他的 app 了。
但是并不是说 ACTION_GET_CONTENT 就完全没有用了,如果你只是打开读取一个文件, ACTION_GET_CONTENT 还是可以的,如果你是要有写入编辑的需求,那就用 A CTION_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 中加入 ca tegory CATEGORY_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的例子写法:
...
....
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测试数据/guide/topics/providers/document-provider.htm 这篇文章的英文原文 要翻墙
http://blog.csdn.net/huangyanan1989/article/details/17263203 Android4.4 中获取资源路径问题 因为 Storage Access Framework 而引起的
https://github测试数据/iPaulPro/aFileChooser 一个文件管理器,在 4.4 中他是直接启用了 documentsui
https://github测试数据/ianhanniballake/LocalStorage 一个自定义的 DocumentsProvider
https://github测试数据/xin3liang/platform_packages_providers_MediaProvider 实现了查询多媒体文件的 DocumentsProvider, 包括查询图片,这个是系统里面的
查看更多关于android存储访问框架StorageAccessFramework的详细内容...