文章大纲

  • 引言
  • 一、DirectFragment
  • 1、当选中DirectoryFragment中RecyclerView的Item时
  • 2、选中DirectoryFragment中RecyclerView的Item后弹出的Menu事件
  • 3、点击Menu上的“复制到”操作后打开DocumentsActivity,然后点击DocumentsActivity里PickFragment里的“复制”
  • 4、点击Menu 上的“删除”选中的文件
  • 二、Android 存储的物理数据库存储结构
  • 三、刷新RootsFragment 列表
  • 1、在每次进入到FilesActivity 时自动刷新RootsFragment 列表
  • 2、在DocumentsUI 每次进行文件操作后主动刷新RootsFragment列表
  • 四、SAF 的简单应用
  • 1、用ACTION_OPEN_DOCUMENT打开图片选择器
  • 2、获取返回的结果
  • 3、创建一个新的文件
  • 4、删除文件
  • 5、实现自己的Document Provider

引言

书接上文Android 进阶——Framework 核心之Android Storage Access Framework(SAF)存储访问框架机制详解(一),继续分析SAF 中DocumentsUI 的主要思想,记得按顺序阅读。

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

一、DirectFragment

RootsFrament 用于展示存储类型列表,而各种存储类型对应的具体内容则展示于DirectoryFragment 的RecylerView上,为了效率设计了不同的Holder 类缓存之用,DocumentHolder、ListDocumentHolder和GridDocumentHolder。

1、当选中DirectoryFragment中RecyclerView的Item时

依次触发DirectoryFragment.SelectionModeListener#onBeforeItemStateChange——>DirectoryFragment.SelectionModeListener#onItemStateChanged——>DirectoryFragment.SelectionModeListener#onSelectionChanged并在onSelectionChanged**里通过MultiSelectManager#notifySelectionChanged方法来更新显示或隐藏DirectionFragment里的menu

2、选中DirectoryFragment中RecyclerView的Item后弹出的Menu事件

当点击menu后触发的是DirectoryFragment.SelectionModeListener#onActionItemClicked

public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            Selection selection = mSelectionManager.getSelection(new Selection());

            switch (item.getItemId()) {
                case R.id.menu_open:
                    openDocuments(selection);
                    mode.finish();
                    return true;

                case R.id.menu_share:
                    shareDocuments(selection);
                    // TODO: Only finish selection if share action is completed.
                    mode.finish();
                    return true;

                case R.id.menu_delete:
                    // deleteDocuments will end action mode if the documents are deleted.
                    // It won't end action mode if user cancels the delete.
                    deleteDocuments(selection);
                    return true;

                case R.id.menu_copy_to:
                    // TODO: Only finish selection mode if copy-to is not canceled.
                    // Need to plum down into handling the way we do with deleteDocuments.
                    mode.finish();
                    transferDocuments(selection, FileOperationService.OPERATION_COPY);
                    return true;

                case R.id.menu_move_to:
                    // Exit selection mode first, so we avoid deselecting deleted documents.
                    mode.finish();
                    transferDocuments(selection, FileOperationService.OPERATION_MOVE);
                    return true;

                case R.id.menu_copy_to_clipboard:
                    copySelectedToClipboard();
                    return true;

                case R.id.menu_select_all:
                    selectAllFiles();
                    return true;

                case R.id.menu_rename:
                    // Exit selection mode first, so we avoid deselecting deleted
                    // (renamed) documents.
                    mode.finish();
                    renameDocuments(selection);
                    return true;

                default:
                    if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
                    return false;
            }
        }

3、点击Menu上的“复制到”操作后打开DocumentsActivity,然后点击DocumentsActivity里PickFragment里的“复制”

安卓allow access to manage all files无法点击 安卓accessory framework_移动开发

com.android.documentsui.PickFragment#mPickListener

private View.OnClickListener mPickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            final DocumentsActivity activity = DocumentsActivity.get(PickFragment.this);
            activity.onPickRequested(mPickTarget);
        }
    };

进而触发DocumentsActivity#onPickRequested

public void onPickRequested(DocumentInfo pickTarget) {
        Uri result;
        if (mState.action == ACTION_OPEN_TREE) {
            result = DocumentsContract.buildTreeDocumentUri(
                    pickTarget.authority, pickTarget.documentId);
        } else if (mState.action == ACTION_PICK_COPY_DESTINATION) {
            result = pickTarget.derivedUri;
        } else {
            // Should not be reached.
            throw new IllegalStateException("Invalid mState.action.");
        }
        new PickFinishTask(this, result).executeOnExecutor(getExecutorForCurrentDirectory());
    }

开启异步任务PickFinishTask(本质上就是一个AsyncTask)执行“复制操作”

private static final class PickFinishTask extends PairedTask<DocumentsActivity, Void, Void> {
        private final Uri mUri;

        public PickFinishTask(DocumentsActivity activity, Uri uri) {
            super(activity);
            mUri = uri;
        }

        @Override
        protected Void run(Void... params) {
            mOwner.writeStackToRecentsBlocking();
            return null;
        }

        @Override
        protected void finish(Void result) {
        	//本质上就是把onPostExecute 公开出来,由于AsyncTask#onPostExecute 是final的 不可重写
            mOwner.onTaskFinished(mUri);
        }
    }

最终执行完毕后回调DocumentsActivity(即mOwner)#onTaskFinished方法,

@Override
    void onTaskFinished(Uri... uris) {
        if (DEBUG) Log.d(TAG, "onFinished() " + Arrays.toString(uris));

        final Intent intent = new Intent();
        if (uris.length == 1) {
            intent.setData(uris[0]);
        } else if (uris.length > 1) {
            final ClipData clipData = new ClipData(
                    null, mState.acceptMimes, new ClipData.Item(uris[0]));
            for (int i = 1; i < uris.length; i++) {
                clipData.addItem(new ClipData.Item(uris[i]));
            }
            intent.setClipData(clipData);
        }

        if (mState.action == ACTION_GET_CONTENT) {
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        } else if (mState.action == ACTION_OPEN_TREE) {
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                    | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
                    | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
        } else if (mState.action == ACTION_PICK_COPY_DESTINATION) {
            // Picking a copy destination is only used internally by us, so we
            // don't need to extend permissions to the caller.
            intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);
            intent.putExtra(FileOperationService.EXTRA_OPERATION, mState.copyOperationSubType);
        } else {
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                    | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
        }
        setResult(Activity.RESULT_OK, intent);
        ///TODO sendSyncBroadcast();
        finish();
    }

,基本上Menu下具体的文件操作除了删除以外,都是这样的逻辑,最后刷新进入到DirectoryFragment.SelectionModeListener#onSelectionChanged方法。

4、点击Menu 上的“删除”选中的文件

点击Menu 上的“删除”后,弹出一个对话框,确认删除时执行的是FileOperations.delete方法,

private void deleteDocuments(final Selection selected) {
    ...
        final DocumentInfo srcParent = getDisplayState().stack.peek();
        // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
        List<DocumentInfo> docs = mModel.getDocuments(selected);
        TextView message =
                (TextView) mInflater.inflate(R.layout.dialog_delete_confirmation, null);
        message.setText(generateDeleteMessage(docs));
        new AlertDialog.Builder(getActivity())
            .setView(message)
            .setPositiveButton(
                 android.R.string.yes,
                 new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int id) {
						...
                        FileOperations.delete(
                                getActivity(), docs, srcParent, getDisplayState().stack);
                    }
                })
            .setNegativeButton(android.R.string.no, null)
            .show();
    }

FileOperations.delete方法本质上就是通过ContentProvider 机制删除文件,具体流程如下:

SAF 把一些文件操作都是统一放到FileOperationService中的

public static String delete(
            Activity activity, List<DocumentInfo> srcDocs, DocumentInfo srcParent,
            DocumentStack location) {
        String jobId = createJobId();
        if (DEBUG) Log.d(TAG, "Initiating 'delete' operation id " + jobId + ".");
        Intent intent = createBaseIntent(OPERATION_DELETE, activity, jobId, srcDocs, srcParent,
                location);
        activity.startService(intent);
        return jobId;
    }

首先开启FileOperationService服务,然后在onStartCommand时执行com.android.documentsui.services.FileOperationService#createJob

Job 是对Runnable 接口的再封装,源码中很多地方都是这样的思想

private @Nullable Job createJob(
            @OpType int operationType, String id, List<DocumentInfo> srcs, DocumentInfo srcParent,
            DocumentStack stack) {
            ...
            switch (operationType) {
                case OPERATION_COPY:
                    return jobFactory.createCopy(
                            this, getApplicationContext(), this, id, stack, srcs);
                case OPERATION_MOVE:
                    return jobFactory.createMove(
                            this, getApplicationContext(), this, id, stack, srcs,
                            srcParent);
                case OPERATION_DELETE:
                    return jobFactory.createDelete(
                            this, getApplicationContext(), this, id, stack, srcs,
                            srcParent);
                default:
                    throw new UnsupportedOperationException();
            }
    }

创建com.android.documentsui.services.DeleteJob,在com.android.documentsui.services.DeleteJob#start方法执行时(即Runnable#run)

@Override
    void start() {
    	...
        for (DocumentInfo doc : mSrcs) {
            if (DEBUG) Log.d(TAG, "Deleting document @ " + doc.derivedUri);
            try {
                deleteDocument(doc, mSrcParent);
            } catch (ResourceException e) {
                onFileFailed(doc);
            }
        }
    }

回到com.android.documentsui.services.Job#deleteDocument

final void deleteDocument(DocumentInfo doc, DocumentInfo parent) throws ResourceException {
        try {
            if (doc.isRemoveSupported()) {
                DocumentsContract.removeDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
            } else if (doc.isDeleteSupported()) {
                DocumentsContract.deleteDocument(getClient(doc), doc.derivedUri);
            } else {
 				...
            }
        } catch (RemoteException | RuntimeException e) {
            ...
        }
    }

调用另一个package下的android.provider.DocumentsContract,这也可以看成是Binder通信,只不过是通过ContentProvider 形式。

DocumentsContract:Defines the contract between a documents provider and the platform.

/**
     * Delete the given document.
     *
     * @param documentUri document with {@link Document#FLAG_SUPPORTS_DELETE}
     * @return if the document was deleted successfully.
     */
    public static boolean deleteDocument(ContentResolver resolver, Uri documentUri) {
        final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
                documentUri.getAuthority());
        try {
            deleteDocument(client, documentUri);
            return true;
        } catch (Exception e) {
            Log.w(TAG, "Failed to delete document", e);
            return false;
        } finally {
            ContentProviderClient.releaseQuietly(client);
        }
    }

    public static void deleteDocument(ContentProviderClient client, Uri documentUri)
            throws RemoteException {
        final Bundle in = new Bundle();
        in.putParcelable(DocumentsContract.EXTRA_URI, documentUri);
        client.call(METHOD_DELETE_DOCUMENT, null, in);
    }

而且还不是直接调用ContentProvider,而是通过DocumentContact 去调用Binder 接口IContentProvider 里的call,至于删除时的具体调用哪个具体的ContentProvider,可以从前面传入的Uri(com.android.externalstorage.documents)

<provider
            android:name=".ExternalStorageProvider"
            android:authorities="com.android.externalstorage.documents"
            android:grantUriPermissions="true"
            android:exported="true"
            android:permission="android.permission.MANAGE_DOCUMENTS">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>

找到对应的ContentProvider————com.android.externalstorage.ExternalStorageProvider

@Override
    public void deleteDocument(String docId) throws FileNotFoundException {
        final File file = getFileForDocId(docId);
        final File visibleFile = getFileForDocId(docId, true);

        final boolean isDirectory = file.isDirectory();
        if (isDirectory) {
            FileUtils.deleteContents(file);
        }
        if (!file.delete()) {
            throw new IllegalStateException("Failed to delete " + file);
        }

        if (visibleFile != null) {
            final ContentResolver resolver = getContext().getContentResolver();
            final Uri externalUri = MediaStore.Files.getContentUri("external");
            // Remove media store entries for any files inside this directory, using
            // path prefix match. Logic borrowed from MtpDatabase.
            if (isDirectory) {
                final String path = visibleFile.getAbsolutePath() + "/";
                resolver.delete(externalUri,
                        "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
                        new String[] { path + "%", Integer.toString(path.length()), path });
            }

            // Remove media store entry for this exact file.
            final String path = visibleFile.getAbsolutePath();
            resolver.delete(externalUri,
                    "_data LIKE ?1 AND lower(_data)=lower(?2)",
                    new String[] { path, path });
			//Add by cmo start
            sendScanBroadcast(visibleFile);
            //add by cmo
        }
    }
	//Add by cmo 
    private void sendScanBroadcast(File visibleFile) {
        Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        intent.setData(Uri.fromFile(visibleFile));
        getContext().sendBroadcast(intent);
    }

最终当删除SD卡上的文件时就是通过ContentResolver 去操作对应的Uri(即content://media/external/file),完成真正的删除动作。

二、Android 存储的物理数据库存储结构

安卓allow access to manage all files无法点击 安卓accessory framework_android_02

如上图所示,最终所有的存储(尤其是文件存储)都会最终保存到数据库中,毕竟在Android ContentProvider下本质上就是跨进程操作数据库,一般在保存在data/data/com.android.providers.media路径下

安卓allow access to manage all files无法点击 安卓accessory framework_DocumentsUI_03

不同的挂载方式分别对应不同的数据库,内部存储对应的是internal.db,而对应的外部存储对应的是external.db。

在高版本的SQlite 里默认采用WAL模式,所有连接数据的操作都必须使用WAL,然后在在数据库文件夹下生成一个后缀为-wal的文件保存操作日志,里面内容更象一份数据库的备份文件,大小比数据库有时还大,而-shm则是一个共享内存机制。

三、刷新RootsFragment 列表

本条知识点针对于项目中使用Android 7.1.2的AOSP 源码,DocumentsUI 界面在进行SD文件操作后,可用容量显示没有刷新的Bug进行的处理,仅在系统重启(系统每次在启动完成时都会去主动调用MedisScannerService 扫描更新媒体库)后首次打开DocumentsUI显示的容量才正确,仅供参考,或许还有更优的方案,涉及到到源码文件:

base/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java base/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java base/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java

1、在每次进入到FilesActivity 时自动刷新RootsFragment 列表

第一步修改FilesActivity,

private SyncHandler mHandler;
    private HandlerThread mWorkThread;
    private SafeMediaScanner mediaScanner;

//add by cmo start 
    private void sendSyncFileMessage() {
        if(mWorkThread==null){
            mWorkThread=new HandlerThread(TAG);
            mWorkThread.start();
        }
        if (mHandler == null) {
            mHandler = new SyncHandler(this,mWorkThread.getLooper());
        }
        mHandler.sendEmptyMessage(MSG_SYNC_FILE);
    }
	
	    @Override
    public void onResume() {
        super.onResume();
        final RootInfo root = getCurrentRoot();
        if (mRoots.getRootBlocking(root.authority, root.rootId) == null) {
            finish();
        } else {
            if(mediaScanner==null) {
                mediaScanner = new SafeMediaScanner(getBaseContext(), true, null);
            }
            sendSyncFileMessage();
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        stopSyncFile();
    }

    private void stopSyncFile() {
        if(mediaScanner!=null){
            mediaScanner.disconnect();
        }
        if(mWorkThread!=null) {
            mWorkThread.quit();
            mWorkThread=null;
        }
        if (mHandler != null) {
            mHandler.removeCallbacksAndMessages(null);
            mHandler=null;
        }
    }

    public void doProcessMsg(Message msg) {
        mHandler.post(new Runnable(){
            @Override
            public void run(){
				//防止因耗时操作导致的卡顿或阻塞MainThread
                scanAndRefresh();
            }
        });
        ///refreshDirectory(android.R.anim.fade_in);
//        mHandler.postDelayed(new Runnable() {
//            @Override
//            public void run() {
//                if (mHandler != null) {
//                    //mHandler.sendEmptyMessage(MSG_SYNC_FILE);
//                }
//            }
//        }, DELAY_SYNC_FILE);
    }

    private void scanAndRefresh() {
        if(mediaScanner!=null){
            mediaScanner.scanFile(getBaseContext(),new String[]{Environment.getExternalStorageDirectory().toString(),
                            Environment.getDataDirectory().toString(),
                            Environment.getDataDirectory().toString(),
                            Environment.getDownloadCacheDirectory().toString()},
                    new String[]{"audio/*", "image/*", "text/*", "video/*", "application/*", "model/*"},
                    new MediaScannerConnection.OnScanCompletedListener() {
                        @Override
                        public void onScanCompleted(String arg0, Uri arg1) {
                        }
                    }
            );
        }
    }
	//为了避免bind连接后没有及时释放导致的内存泄漏,而进行优化
    public static class SafeMediaScanner implements android.media.MediaScannerConnection.MediaScannerConnectionClient {

        private final MediaScannerConnection mConn;
        private boolean oneShot=false;
        private OnScanListener mListener;

        public SafeMediaScanner(Context context,boolean oneShot, OnScanListener listener) {
            this.mConn =  new MediaScannerConnection(context, this);
            this.mListener = listener;
            this.oneShot=oneShot;
            mConn.connect();
        }

        @Override
        public void onMediaScannerConnected() {
            if(mListener!=null){
                mListener.onScannerConnected();
            }
        }

        @Override
        public void onScanCompleted(String path, Uri uri) {
            if(oneShot){
                mConn.disconnect();
            }
            mListener.onScanCompleted(path, uri);
        }

        public void scanFile(Context context, String[] paths, String[] mimeTypes,
                             MediaScannerConnection.OnScanCompletedListener callback) {
            MediaScannerConnection.scanFile(context, paths, mimeTypes, callback);
        }

        public void disconnect(){
            if(mConn!=null){
                mConn.disconnect();
            }
        }

        public interface OnScanListener {
            void onScannerConnected();
            void onScanCompleted(String path, Uri uri);
        }
    }

    class SyncHandler<FilesActivity> extends Handler {
        protected final WeakReference<com.android.documentsui.FilesActivity> mReference;

        public SyncHandler(com.android.documentsui.FilesActivity r, Looper looper){
            super(looper);
            mReference=new WeakReference<>(r);
        }

        @Override
        public void handleMessage(Message msg) {
            com.android.documentsui.FilesActivity activity = mReference == null ? null : ((com.android.documentsui.FilesActivity) mReference.get());
            if (activity == null || activity.isFinishing()) {
                return;
            }
            activity.doProcessMsg(msg);
        }
    }
    //add by cmo end

第二步修改ExternalStorageProvider,删除执行后主动发起广播告知系统进行扫描

@Override
    public void deleteDocument(String docId) throws FileNotFoundException {
		...
        if (visibleFile != null) {
			...
            // Remove media store entry for this exact file.
            final String path = visibleFile.getAbsolutePath();
            resolver.delete(externalUri,
                    "_data LIKE ?1 AND lower(_data)=lower(?2)",
                    new String[] { path, path });
			//Add by lamy 
            sendScanBroadcast(visibleFile);
            //add by lamy end
        }
    }

    private void sendScanBroadcast(File visibleFile) {
        Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        intent.setData(Uri.fromFile(visibleFile));
        getContext().sendBroadcast(intent);
    }

2、在DocumentsUI 每次进行文件操作后主动刷新RootsFragment列表

//add by cmo start
	private SyncHandler mHandler, mMainHandler;
    private HandlerThread mWorkThread;
    private SafeMediaScanner mediaScanner;
	
    @Override
    public void onResume() {
        super.onResume();
        startSyncFile();
        onDisplayStateChanged();
    }
	
    @Override
    public void onStop() {
        super.onStop();
        stopSyncFile();
    }

    private void startSyncFile() {
        registerReceiver();
        if (mWorkThread == null) {
            mWorkThread = new HandlerThread(TAG);
            mWorkThread.start();
        }
        if (mHandler == null) {
            mHandler = new SyncHandler(this, mWorkThread.getLooper());
        }
        registObserver();
        if (mMainHandler == null) {
            mMainHandler = new SyncHandler(this, Looper.getMainLooper());
        }
        mHandler.sendEmptyMessage(MSG_SYNC_FILE);
    }

    private void stopSyncFile() {
        if (mediaScanner != null) {
            mediaScanner.disconnect();
        }
        if (mWorkThread != null) {
            mWorkThread.quit();
            mWorkThread=null;
        }
        if (mMainHandler != null) {
            mMainHandler.removeCallbacksAndMessages(null);
            mMainHandler = null;
        }
        if (mHandler != null) {
            mHandler.removeCallbacksAndMessages(null);
            mHandler = null;
        }
        unregisterReceiver();
        unregistObserver();
    }

    private void refreshRoots(Context context) {
        Uri rootsUri = Uri.parse("content://com.android.externalstorage.documents/root");
        context.getContentResolver().query(rootsUri, null, null, null, null);
        RootsCache cache = DocumentsApplication.getRootsCache(context);
        if (cache != null) {
            cache.updateAsync(true);
        }
    }

    public void refresh() {
        if (mAdapter != null) {
            mAdapter.notifyDataSetChanged();
        }
    }

    private void scanAndRefresh() {
        if (mediaScanner != null) {
            mediaScanner.scanFile(getActivity().getBaseContext(), new String[]{Environment.getExternalStorageDirectory().toString(),
                            Environment.getDataDirectory().toString(),
                            Environment.getDataDirectory().toString(),
                            Environment.getDownloadCacheDirectory().toString()},
                    new String[]{"audio/*", "image/*", "text/*", "video/*", "application/*", "model/*"},
                    new MediaScannerConnection.OnScanCompletedListener() {
                        @Override
                        public void onScanCompleted(String arg0, Uri arg1) {
                        }
                    }
            );
        }
    }

    private void doProcessMsg(Message msg) {
        if (mHandler != null) {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    refreshRoots(getActivity());
                    scanAndRefresh();
                }
            });
            mMainHandler.post(new Runnable() {
                @Override
                public void run() {
                    refresh();
                }
            });
        }
    }

    private void registerReceiver(){
        mReceiver = new SyncFileReceiver();
        IntentFilter filter = new IntentFilter("com.cmo.documentsui.refresh");
        if(getActivity()!=null) {
            getActivity().registerReceiver(mReceiver, filter);
        }
    }

    private void unregisterReceiver(){
        if(mReceiver != null && getActivity()!=null) {
            getActivity().unregisterReceiver(mReceiver);
        }
    }

    private void registObserver(){
        mFileObserver=new FileContentObserver(mHandler);
        if(getActivity()!=null){
            final Uri uri = MediaStore.Files.getContentUri("external");
            getActivity().getContentResolver().registerContentObserver(uri,true,mFileObserver);
        }
    }

    private void unregistObserver(){
        if(getActivity()!=null && mFileObserver!=null){
            getActivity().getContentResolver().unregisterContentObserver(mFileObserver);
        }
    }

    private class FileContentObserver extends ContentObserver{

        public FileContentObserver(Handler handler) {
            super(handler);
        }

        @Override
        public void onChange(boolean selfChange) {
            if(mHandler!=null) {
                mHandler.sendEmptyMessage(MSG_SYNC_FILE);
            }
        }
    }

    private class SyncFileReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if(mHandler!=null) {
                mHandler.sendEmptyMessage(MSG_SYNC_FILE);
            }
        }
    }

    class SyncHandler<RootsFragment> extends android.os.Handler {
        protected final WeakReference<com.android.documentsui.RootsFragment> mReference;

        public SyncHandler(com.android.documentsui.RootsFragment r, Looper looper) {
            super(looper);
            mReference = new WeakReference<>(r);
        }

        @Override
        public void handleMessage(Message msg) {
            com.android.documentsui.RootsFragment fragment = mReference == null ? null : ((com.android.documentsui.RootsFragment) mReference.get());
            if (fragment == null || fragment.isHidden()) {
                return;
            }
            fragment.doProcessMsg(msg);
        }
    }
    //add by cmo end

四、SAF 的简单应用

1、用ACTION_OPEN_DOCUMENT打开图片选择器

public void performFileSearch() {
    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
    // Filter to only show results that can be "opened", such as a file 
    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);
}

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final int READ_REQUEST_CODE = 42;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btn_show = (Button) findViewById(R.id.btn_show);
        btn_show.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        performFileSearch();
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            Uri uri;
            if (data != null) {
                uri = data.getData();
            }
        }
    }
}

当我们选中图片后,然后DocumentsUI会关掉,返回对应的URI。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的文件。

2、获取返回的结果

返回结果一般是一个Uri,数据保存在onActivityResult的第三个参数resultData中,通过resultData.getData()获取Uri。一旦得到Uri,你就可以用uri获取文件的元数据 。

public void dumpImageMetaData(Uri uri) {
    // The query, since it only applies to a single document, will only returnone 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 {
        if (cursor != null && cursor.moveToFirst()) {
            // Note it's called "Display Name".  This is 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);
            // 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)) {
                size = cursor.getString(sizeIndex);
            } else {
                size = "Unknown";
            }
            Log.i(TAG, "Size: " + size);
        }
    } finally {
        cursor.close();
    }
}

SAF只是帮我们获取到Uri,而获取Uri之后如何得到真正的资源与SAF无关,

  • 从Uri获得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();
}

上面的获取元数据、bitmap、输出流的代码和SAF并没有什么关系,只是告诉你通过一个Uri你可以知道什么,而Uri的获取是利用SAF得到的。

3、创建一个新的文件

使用ACTION_CREATE_DOCUMENT intent来创建文件

// 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。

4、删除文件

前提是Document.COLUMN_FLAGS包含SUPPORTS_DELETE

DocumentsContract.deleteDocument(getContentResolver(), uri),具体参见上文。

5、实现自己的Document Provider

如果你希望自己应用的数据也能在DocumentsUI中打开,你就需要写一个自己的Document Provider,步骤如下:

  • 继承自DocumentsProvider.java重写相关抽象方法
  • 在清单文件里声明注册定义的Document 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>

以一个实现访问文件(file)系统的DocumentsProvider的为例:

...
@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;
}

@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;
}

@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;
}

未完待续…