Android 进阶——Framework 核心 Android Storage Access Framework(SAF)存储访问框架机制详解(一)
引言
如果我们App希望能选择Android手机中的某张图片,只需要发送一个Intent:
Intent intent=new Intent(Intent.ACTION_GET_CONTENT);//ACTION_OPEN_DOCUMENT
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/jpeg");
在Android 4.4 之前,如果想从另外一个App中选择一个文件(比如从图库中选择一张图片文件)必须触发一个ACTION为ACTION_PICK或者ACTION_GET_CONTENT的Intent,再在候选的App中选择一个App,从中获得你想要的文件,最关键的是被选择的App中要具有能为你提供文件的功能,但如果一个不负责任的第三方开发者注册了一个恰恰符合你需求的Intent,但是没有实现返回文件的功能,那么就会出现意想不到的错误。
- Android 进阶——Framework 核心 Android Storage Access Framework(SAF)存储访问框架机制详解(一)
- Android 进阶——Framework 核心 Android Storage Access Framework(SAF)存储访问框架机制详解(二)
本系列文章基于源码Android 7.1.2 ,由于精力和水平有限,SAF的机制和知识还有很多详情未来得及一一总结,本文内容仅代表个人理解,仅供参考
一、Android Storage Access Framework
Android 4.4中引入了Storage Access Framework存储访问框架(SAF)。SAF为用户浏览手机中存储的内容(不仅包括文档、图片,视频、音频、下载、GoogleDrive等,还包括所有继承自DocumentsProvider的特定云存储、本地存储提供的内容)提供了统一的管理和展现形式。无论内容来自于哪里,是哪个应用调用浏览系统文件内容的命令,SAF都会用一个统一的界面(DocumentsUI App)让你去使用,通过发送Intent.ACTION_OPEN_DOCUMENT的 Intent来弹出一个很漂亮的界面,有点像一个文件管理器,其实他比文件管理器更强大,他是一个存储管理角色,可以按照目录一层一层的操作文件,也可以按照文件种类操作文件,还可以打开一个应用程序选择文件。但是并不是说ACTION_GET_CONTENT就完全没有用了,如果你只是打开读取一个文件,ACTION_GET_CONTENT还是可以的,如果你是要有写入编辑的需求,那就用ACTION_OPEN_DOCUMENT。
在4.4系统中ACTION_GET_CONTENT启动的还是DocumentsUI。
二、Storage Access Framework 的主要角色成员
1、Document Provider 文件存储服务提供者
SAF的核心机制,Document Provider让一个存储服务(比如Google Drive)可以对外以统一的形式展示自己所管理的文件,一个Document Provider代码上就是实现了DocumentsProvider.java的子类,其schema 和传统的文件存径格式一致,但是至于你的内容是怎么存储的完全取决于你自己,android系统中已经内置了几个这样的Document Provider(比如关于下载、图片以及视频的Document Provider)
DocumentsProvider.java是一个Android为ASF 实现的一个存储服务内容提供者基类,如需要把自己实现,第一步就是继承这个基类,而分开写的Document Provider只是一种描述,代表存储服务是通过ContentProvider 机制对外提供的。
用户可以浏览所有由Document Provider提供的内容,提供了长期、持续的访问Document Provider中文件的能力以及数据的持久化,用户可以实现添加、删除、编辑、保存Document Provider所维护的内容。当客户端App与Document Provider之间的交互是在触发了ACTION_OPEN_DOCUMENT或者ACTION_CREATE_DOCUMENT 的 Intent之后,Intent还可以进一步设置过滤条件(如筛选图片类型的,设置MIME Type 为“image/*”)当Intent触发之后选择器去寻找每一个注册了的Provider,并将符合条件的Document Provider的的根目录显示出来。
2、DocumentsUI 文件存储选择器App
SAF 中的文件选择器App 是来自于xx\frameworks\base\packages\DocumentsUI,他提供了访问满足客户端过滤条件的所有Document Provider内容的通道,可以看成是SAF中一个统一交互的UI。
因为DocumentsUI的manifest里声明的Activity下intent-filter节点里的category属性没有同时设置android.intent.category.HOME和android.intent.category.LAUNCHER的属性,因此DocumentsUI是不会显示在Launcher界面上。
@Override
public void attachInfo(Context context, ProviderInfo info) {
registerAuthority(info.authority);
// Sanity check our setup
if (!info.exported) {
throw new SecurityException("Provider must be exported");
}
if (!info.grantUriPermissions) {
throw new SecurityException("Provider must grantUriPermissions");
}
if (!android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.readPermission)
|| !android.Manifest.permission.MANAGE_DOCUMENTS.equals(info.writePermission)) {
throw new SecurityException("Provider must be protected by MANAGE_DOCUMENTS");
}
super.attachInfo(context, info);
}
/**
*
* // content://com.example/root/
// content://com.example/root/sdcard/
// content://com.example/root/sdcard/recent/
// content://com.example/root/sdcard/search/?query=pony
// content://com.example/document/12/
// content://com.example/document/12/children/
// content://com.example/tree/12/document/24/
// content://com.example/tree/12/document/24/children/
*
*/
private void registerAuthority(String authority) {
mAuthority = authority;
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mMatcher.addURI(mAuthority, "root", MATCH_ROOTS);
mMatcher.addURI(mAuthority, "root/*", MATCH_ROOT);
mMatcher.addURI(mAuthority, "root/*/recent", MATCH_RECENT);
mMatcher.addURI(mAuthority, "root/*/search", MATCH_SEARCH);
mMatcher.addURI(mAuthority, "document/*", MATCH_DOCUMENT);
mMatcher.addURI(mAuthority, "document/*/children", MATCH_CHILDREN);
mMatcher.addURI(mAuthority, "tree/*/document/*", MATCH_DOCUMENT_TREE);
mMatcher.addURI(mAuthority, "tree/*/document/*/children", MATCH_CHILDREN_TREE);
}
不同版本位置和内部实现有所不同
3、客户端内容使用者
通过触发ACTION_OPEN_DOCUMENT或者ACTION_CREATE_DOCUMENTintent的客户端App,以触发ACTION_OPEN_DOCUMENT或者ACTION_CREATE_DOCUMENT形式,客户端可以接收来自于Document Provider的内容。
三、DocumentsUI
1、DocumentsUI 概述
如上图所示DocumentsUI 的主界面,由BaseActivity+FilesActivity+RootsFragment+DirectoryFragment 组成,数据均由Loader机制和AsycTask机制进行加载,很多Activity 自身不充当具体的UI容器,仅仅是用于Fragment的容器,具体的内容由Fragment进行展示。
adb shell dumpsys activity | findstr “mFocusedActivity” 查看当前Activity
2、BaseActivity
2.1、黑色部分就是BaseActivity 以menu 形式创建的形如Toolbar的结构
@Override
@CallSuper
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
mSearchManager.showMenu(canSearchRoot());
final boolean inRecents = getCurrentDirectory() == null;
final MenuItem sort = menu.findItem(R.id.menu_sort);
final MenuItem sortSize = menu.findItem(R.id.menu_sort_size);
final MenuItem grid = menu.findItem(R.id.menu_grid);
final MenuItem list = menu.findItem(R.id.menu_list);
final MenuItem advanced = menu.findItem(R.id.menu_advanced);
final MenuItem fileSize = menu.findItem(R.id.menu_file_size);
// Search uses backend ranking; no sorting, recents doesn't support sort.
sort.setEnabled(!inRecents && !mSearchManager.isSearching());
sortSize.setVisible(mState.showSize); // Only sort by size when file sizes are visible
fileSize.setVisible(!mState.forceSize);
// grid/list is effectively a toggle.
grid.setVisible(mState.derivedMode != State.MODE_GRID);
list.setVisible(mState.derivedMode != State.MODE_LIST);
advanced.setVisible(mState.showAdvancedOption);
advanced.setTitle(mState.showAdvancedOption && mState.showAdvanced
? R.string.menu_advanced_hide : R.string.menu_advanced_show);
fileSize.setTitle(LocalPreferences.getDisplayFileSize(this)
? R.string.menu_file_size_hide : R.string.menu_file_size_show);
return true;
}
2.2、当点击menu 时触发的是BaseActivity#onOptionsItemSelected
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
case R.id.menu_create_dir:
showCreateDirectoryDialog();
return true;
case R.id.menu_search:
// SearchViewManager listens for this directly.
return false;
case R.id.menu_sort_name:
setUserSortOrder(State.SORT_ORDER_DISPLAY_NAME);
return true;
case R.id.menu_sort_date:
setUserSortOrder(State.SORT_ORDER_LAST_MODIFIED);
return true;
case R.id.menu_sort_size:
setUserSortOrder(State.SORT_ORDER_SIZE);
return true;
case R.id.menu_grid://切换为网格展示形式
setViewMode(State.MODE_GRID);
return true;
case R.id.menu_list://列表展示形式
setViewMode(State.MODE_LIST);
return true;
case R.id.menu_paste_from_clipboard:
DirectoryFragment dir = getDirectoryFragment();
if (dir != null) {
dir.pasteFromClipboard();
}
return true;
case R.id.menu_advanced:
setDisplayAdvancedDevices(!mState.showAdvanced);
return true;
case R.id.menu_file_size:
setDisplayFileSize(!LocalPreferences.getDisplayFileSize(this));
return true;
case R.id.menu_settings:
Metrics.logUserAction(this, Metrics.USER_ACTION_SETTINGS);
final RootInfo root = getCurrentRoot();
final Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_ROOT_SETTINGS);
intent.setDataAndType(root.getUri(), DocumentsContract.Root.MIME_TYPE_ITEM);
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
选择不同类型的Item 时支持的menu 操作有所不同。
2.3、选中RootItem 时首先触发BaseActivity#onRootPicked
void onRootPicked(RootInfo root) {
// Clicking on the current root removes search
mSearchManager.cancelSearch();
// Skip refreshing if root nor directory didn't change
if (root.equals(getCurrentRoot()) && mState.stack.size() == 1) {
return;
}
mState.derivedMode = LocalPreferences.getViewMode(this, root, MODE_GRID);
// Clear entire backstack and start in new root
mState.onRootChanged(root);
// Recents is always in memory, so we just load it directly.
// Otherwise we delegate loading data from disk to a task
// to ensure a responsive ui.
if (mRoots.isRecentsRoot(root)) {
refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
} else {
new PickRootTask(this, root).executeOnExecutor(getExecutorForCurrentDirectory());
}
}
3、RootsFramgment
RootsFramgment 加载时由Loader机制把数据加载到ListView的RootsAdapter,ListView中支持三种不同类型Item:
- RootsFragment.RootItem——对应的item布局文件为R.layout.item_root,用于展示DocumentsUI 中默认自带的存储类型
- RootsFragment.AppItem——对应的item布局文件为R.layout.item_root,用于展示第三方App下的类型,比如Google Drive等。
- RootsFragment.SpacerItem——对应布局文件R.layout.item_root_spacer,就是一个分割线。
初次加载ListView数据完毕之后,会把这些数据缓存到RootsCache里,每一次打开DocumentsUI时都会在Appplication#onCreate方法中获取RootsCache并调用updateAsync通过AsyncTask方式从各种Document Provider中获取RootIem的数据。
每次进入到界面时:执行com.android.documentsui.RootsCache#loadRootsForAuthority
1970-01-01 09:31:55.897 1784-1800/com.android.documentsui D/RootsCache: Loading roots for com.android.externalstorage.documents
1970-01-01 09:31:55.925 1784-1800/com.android.documentsui D/RootsCache: Loading roots for com.android.mtp.documents
1970-01-01 09:31:56.059 1784-1818/com.android.documentsui D/RootsCache: Loading roots for com.android.providers.downloads.documents
1970-01-01 09:31:56.088 1784-1800/com.android.documentsui D/RootsCache: Loading roots for com.android.providers.downloads.documents
1970-01-01 09:31:56.089 1784-1800/com.android.documentsui D/RootsCache: Loading roots for com.android.providers.media.documents
//ExternalStorageProvider
1970-01-01 08:31:32.904 1775-1791/? D/RootsCache: RootsCache query->content://com.android.externalstorage.documents/root
//DownloadStorageProvider
1970-01-01 08:31:32.972 1775-1822/? D/RootsCache: RootsCache query->content://com.android.providers.downloads.documents/root
//MtpDocumentsProvider
1970-01-01 08:31:33.070 1775-1791/? D/RootsCache: RootsCache query->content://com.android.mtp.documents/root
//MediaDocumentsProvider
1970-01-01 08:31:33.089 1775-1791/? D/RootsCache: RootsCache query->content://com.android.providers.media.documents/root
//ExternalStorageProvider
1970-01-01 08:31:33.163 1775-1830/? D/RootsCache: RootsCache query->content://com.android.externalstorage.documents/root
1970-01-01 11:01:53.840 1948-1948/com.android.documentsui D/RootsFragment: Adding Root{authority=com.android.providers.media.documents, rootId=images_root, title=图片, isUsb=false, isSd=false, isMtp=false} as library.
1970-01-01 11:01:53.840 1948-1948/com.android.documentsui D/RootsFragment: Adding Root{authority=com.android.providers.media.documents, rootId=videos_root, title=视频, isUsb=false, isSd=false, isMtp=false} as library.
1970-01-01 11:01:53.840 1948-1948/com.android.documentsui D/RootsFragment: Adding Root{authority=com.android.providers.media.documents, rootId=audio_root, title=音频, isUsb=false, isSd=false, isMtp=false} as library.
1970-01-01 11:01:53.840 1948-1948/com.android.documentsui D/RootsFragment: Adding Root{authority=com.android.externalstorage.documents, rootId=47A3-19F8, title=SD卡, isUsb=false, isSd=true, isMtp=false} as non-library.
1970-01-01 11:01:53.840 1948-1948/com.android.documentsui D/RootsFragment: Adding Root{authority=com.android.providers.downloads.documents, rootId=downloads, title=下载, isUsb=false, isSd=false, isMtp=false} as non-library.
以上是RootsCache 相关的核心日志信息。篇幅问题,剩下部分,请参见下文,未完待续…
本文地址:https://blog.csdn.net/CrazyMo_/article/details/108555094