android存储访问框架Storage Access Framework

在了解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的子类,即内容提供者(document
provider)。document
provider中数据是以传统的文件目录树组织起来的:

流程图

虽说document
provider中数据是以传统的文件目录树组织起来的,但是那只是对外表现的形式,至于你的数据在内部究竟是怎么样(甚至完全杂乱无章),完全取决于你自己,只要你对外的接口能够通过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中加入category
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的例子写法:

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

时间: 2024-10-09 11:52:51

android存储访问框架Storage Access Framework的相关文章

Android_存储访问框架SAF

概念 存储访问框架---Storage Access Framework (SAF),这是在Android4.4(API level 19)之后引入的. 借助 SAF,用户可轻松在其所有首选文档存储提供程序中浏览并打开文档.图像及其他文件.用户可通过易用的标准界面,以统一方式在所有应用和提供程序中浏览文件,以及访问最近使用的文件. 云存储服务或本地存储服务可实现封装其服务的 DocumentsProvider,进而参与此生态系统.只需几行代码,便可将需要访问提供程序文档的客户端应用与 SAF 进

2.App Components-Content Providers/Storage Access Framework

1. Storage Access Framework Android 4.4 (API level 19) introduces the Storage Access Framework (SAF). The SAF makes it simple for users to browse and open documents, images, and other files across all of their their preferred document storage provide

Android存储访问及目录

Android的外部存储 Android支持外部存储(case-insensitive filesystem with immutable POSIX permission classes and modes). 外部存储可以通过物理介质提供(如SD卡),也可以通过将内部存储中的一部分封装而成,设备可以有多个外部存储实例. 访问外部存储的权限 从Android 1.0开始,写操作受权限WRITE_EXTERNAL_STORAGE保护. 从Android 4.1开始,读操作受权限READ_EXTE

利用HTML5开发Android(7)---HTML5本地存储之Database Storage

在上一篇<HTML5本地存储之Web Storage篇>中,简单介绍了如何利用localStorage实现本地存储:实际上,除了sessionStorage和localStorage外,HTML5还支持通过本地数据库进行本地数据存储,HTML5采用的是"SQLite"这种文件型数据库,该数据库多集中在嵌入式设备上,熟悉IOS/Android开发的同学,应该对SQLite数据库比较熟悉. HTML5中的数据库操作比较简单,主要有如下两个函数: 1.通过openDatabase

利用HTML5开发Android(4)---HTML5本地存储之Web Storage

Web Storage是HTML5引入的一个非常重要的功能,可以在客户端本地存储数据,类似HTML4的cookie,但可实现功能要比cookie强大的多,cookie大小被限制在4KB,Web Storage官方建议为每个网站5MB. Web Storage又分为两种: sessionStorage localStorage 从字面意思就可以很清楚的看出来,sessionStorage将数据保存在session中,浏览器关闭也就没了:而localStorage则一直将数据保存在客户端本地: 不管

Android存储子系统

这篇文章主要是分析Android存储向关联的一些模块,这个分析主要从大的工作流程和代码模块分析,没有对于没有分析到地方后续遇到后在详细分析.主要从以下几个模块分析 系统分区的挂载.外部分区挂载.Vold守候进程分.MountService的业务分析.Sdcard的详细分析.MTP模式分析和设备存储空间的监控机制. 系统分区挂载 Android是基于linux内核的系统,遵从linux的文件系统的挂载方式.在Android中在init进程负责挂载常用的system,data,cache等分区.In

转载——Android permission 访问权限大全

程序执行需要读取到安全敏感项必需在androidmanifest.xml中声明相关权限请求, 完整列表如下: Android.permission.ACCESS_CHECKIN_PROPERTIES 允许读写访问"properties"表在checkin数据库中,改值可以修改上传( Allows read/write access to the "properties" table in the checkin database, to change values

Android Permission 访问权限大全(转)

程序执行需要读取到安全敏感项必需在androidmanifest.xml中声明相关权限请求, 完整列表如下: android.permission.ACCESS_CHECKIN_PROPERTIES允许读写访问”properties”表在 checkin数据库中,改值可以修改上传( Allows read/write access to the “properties” table in the checkin database, to change values that get upload

Android的系统框架的深入认识

Android采用层次化系统架构,官方公布的标准架构如下图所示.Android由底层往上分为4个主要功能层,分别是linux内核层(Linux Kernel),系统运行时库层(Libraries和Android Runtime),应用程序架构层(Application Framework)和应用程序层(Applications). Linux内核层 Android以Linux操作系统内核为基础,借助Linux内核服务实现硬件设备驱动,进程和内存管理,网络协议栈,电源管理,无线通信等核心功能.An