在了解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. centos7.2安装redis与配置(史上最全)

    学习了php已经快三年了,一直是在盲目的忙,也没整理下笔记,今天整理一下 分享下安装redis的方法 #首先去redis官网去下载   http://www.redis.cn/download.htm ...

  2. Python中threading的join和setDaemon的区别[带例子]

    python的进程和线程经常用到,之前一直不明白threading的join和setDaemon的区别和用法,今天特地研究了一下.multiprocessing中也有这两个方法,同样适用,这里以thr ...

  3. initcall机制

    参考:initcall机制 /* include/linux/init.h: */ /* For assembly routines */ #define __HEAD .section " ...

  4. 网址导航[IT]

    一.Linux/UNIX Linux公社:http://www.linuxidc.com/index.htm Linux命令大全:http://man.linuxde.net 伯乐在线:http:// ...

  5. java 罕见的依赖报错 jstat: error while loading shared libraries: libjli.so: cannot open shared object file: No such file or directory

    java 都用了N长时间了,突然,意外地发现有一个依赖的so文件从来没找见过 # ldd /usr/bin/java linux-vdso.so.1 =>  (0x00007fffba76900 ...

  6. perl 处理文件路径的一些模块

    perl有句格言:There is more than one way to do it.意思就是任何问题用perl都有好几种解决方法.以前处理文件路径的时候都是自己写正则表达式,而用perl的模块来 ...

  7. ubuntu系统下如何禁用笔记本触摸板

    命令行方式,得每次用终端输入命令行设置,不方便. sudo rmmod psmouse          # 用来禁用触摸板 sudo modprobe psmouse     # 用来启用触摸板 想 ...

  8. 机房合作(三):We are Team,We are Family

    导读:拖拖拉拉,机房的合作也算是接近了尾声了.在这个过程中,真心是感谢我的两个组员.这个机房合作,看似简单,但我的组员给我的帮助和感动,都是不可忽略的.记得刚开始的时候,我就说过:不怕猪一样的组长,咱 ...

  9. Terracotta

    Terracotta 3.2.1简介 (一) 博客分类: 企业应用面临的问题 Java&Socket 开源组件的应用 hibernatejava集群服务器EhcacheQuartzTerrac ...

  10. BZOJ 1260: [CQOI2007]涂色paint【区间DP】

    Description 假设你有一条长度为5的木版,初始时没有涂过任何颜色.你希望把它的5个单位长度分别涂上红.绿.蓝.绿.红色,用一个长度为5的字符串表示这个目标:RGBGR. 每次你可以把一段连续 ...