欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-03-09 16:08:14
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 概述

Android 进阶——Framework 核心 Android Storage Access Framework(SAF)存储访问框架机制详解(一)
如上图所示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