在了解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的大致写法。

queryRoots的实现:

@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,包含查询图片,这个是系统里面的

android存储訪问框架Storage Access Framework的更多相关文章

  1. ContentProvider官方教程(11)Calendar Provider、Contacts Provider、Storage Access Framework

    Calendar Provider: guide/topics/providers/calendar-provider.html Contacts Provider: guide/topics/pro ...

  2. Android API Guides---Storage Access Framework

    存储訪问架构 Android 4.4系统(API级别19)推出存储訪问框架(SAF).新加坡武装部队变得很easy,为用户在其全部自己喜欢的文件存储提供商的浏览和打开文档,图像和其它文件.一个标准的, ...

  3. 有关Android存储的相关概念辨析

    我想念许多Android开发人员在碰到有关存储的相关问题时,肯定遇到过“内部存储/内存”.“外部存储/外存”等类似的概念,尤其是将相关概念跟非开发人员解释时,那真是“秀才遇到兵,有理说不清哪”.包括我 ...

  4. Phalcon 訪问控制列表 ACL(Access Control Lists ACL)

    Phalcon在权限方面通过 Phalcon\Acl 提供了一个轻量级的 ACL(訪问控制列表). Access Control Lists (ACL) 同意系统对用户的訪问权限进行控制,比方同意訪问 ...

  5. Android下的数据存储与訪问 --- 以文件的形式

    Android下的数据存储与訪问 --- 以文件的形式 1.1 储存文件存放在手机内存中: // *** 储存数据到 /data/data/包名/files/jxn.txt文件里 String dat ...

  6. 怎样从C++代码直接訪问android framework层的WifiService

    说究竟,Java层的service就是就C++层的binder的封装.所以从原理上来讲通过C++代码直接訪问android framework层的service是全然可能的,这篇文章以訪问WifiSe ...

  7. Android 訪问权限清单

    Android权限设置 概述 权限 说明 訪问登记属性 android.permission.ACCESS_CHECKIN_PROPERTIES 读取或写入登记check-in数据库属性表的权限 获取 ...

  8. 【Android架构综述篇】之应用程序、应用程序訪问硬件的流程

    对于分层的系统.刚開始认识时,从宏观的框架层面了解应用的构建过程,有助于形成自己对新系统的清晰概念. 1.Android应用程序构建框架: 这里就涉及活动.布局.注冊之间的关系.搞清了这三者.会对真个 ...

  9. Android訪问网络,使用HttpURLConnection还是HttpClient?

    原文地址:http://android-developers.blogspot.com/2011/09/androids-http-clients.html 大多数的Android应用程序都会使用HT ...

随机推荐

  1. 《UNIX环境高级编程》笔记——4.文件和目录

    一.引言 本章描述文件系统的其他特征和文件的性质.有些背景知识需要注意,例如用户ID与文件权限.文件系统等. 二.函数stat.fstat.fstatat和lstat #include <sys ...

  2. SpringMVC里静态网页不能加载到.js .css文件的问题

    在写SpringMVC项目时候,写的js css文件打不开,网上查了一下,解决办法: 在web.xml里面: <servlet> <servlet-name>dispatche ...

  3. python图像插值

    最近邻:选择离它所映射到的位置最近的输入像素的灰度值为插值结果. 最临近插值 图像的缩放很好理解,就是图像的放大和缩小.传统的绘画工具中,有一种叫做“放大尺”的绘画工具,画家常用它来放大图画.当然,在 ...

  4. Java面试——String、StringBuider以及StringBuffer的区别和使用场景

    1.  String.StringBuider.StringBuffer的区别  String是不可变的对象,因此在每次对String类型进行改变的时候,都会生成一个新的String对象,然后将指针指 ...

  5. Python requests模块params、data、json的区别

    json和dict对比 json的key只能是字符串,python的dict可以是任何可hash对象(hashtable type): json的key可以是有序.重复的:dict的key不可以重复. ...

  6. 大数据学习——hive的sql练习题

    ABC三个hive表 每个表中都只有一列int类型且列名相同,求三个表中互不重复的数 create table a(age int) row format delimited fields termi ...

  7. HTTP/1.1协议支持的8种请求方法

    方法 说明 GET 获取资源 POST 传输实体主体 PUT 传输文件 DELETE 删除文件 HEAD 获得报文首部 OPTIONS 询问支持的方法 TRACE 追踪路径 CONNECT 要求用隧道 ...

  8. 如何解决 错误code signing is required for product type 'xxxxx' in SDK 'iOS 8.2'

    如何解决 错误code signing is required for product type 'xxxxx' in SDK 'iOS 8.2' 大家在做真机调试的时候,或许会遇到这样的问题,那如何 ...

  9. POJ 1038 Bugs Integrated, Inc. ——状压DP

    状态压缩一下当前各格子以及上面总共放了几块,只有012三种情况,直接三进制保存即可. 然后转移的时候用搜索找出所有的状态进行转移. #include <map> #include < ...

  10. django-Ajax发送POST请求-csrf跨站请求的三种方式

    第一种 <script> $(".eq").on("click",function () { $.ajax({ url:"/eq/&quo ...