当前位置: 移动技术网 > IT编程>开发语言>.net > Android 进阶——Framework 核心 Android Storage Access Framework(SAF)存储访问框架机制详解(一)

Android 进阶——Framework 核心 Android Storage Access Framework(SAF)存储访问框架机制详解(一)

2020年09月20日  | 移动技术网IT编程  | 我要评论
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 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 概述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XrYAyq0p-1599916040530)(D:\Doc\个人文档\android\Android SAF存储访问框架\image-20200907113706942.png)]
如上图所示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

如您对本文有疑问或者有任何想说的,请点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网