文章大纲

  • 引言
  • 一、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里的“复制”

com.android.documentsui.PickFragment#mPickListener

private View.OnClickListener mPickListener = new View.OnClickListener() {@Overridepublic 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;}@Overrideprotected Void run(Void... params) {mOwner.writeStackToRecentsBlocking();return null;}@Overrideprotected void finish(Void result) {//本质上就是把onPostExecute 公开出来,由于AsyncTask#onPostExecute 是final的 不可重写mOwner.onTaskFinished(mUri);}}

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

    @Overridevoid 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() {@Overridepublic 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)

    @Overridevoid 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)

        <providerandroid: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

    @Overridepublic 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 startsendScanBroadcast(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 存储的物理数据库存储结构

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

不同的挂载方式分别对应不同的数据库,内部存储对应的是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);}@Overridepublic 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();}}@Overrideprotected 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(){@Overridepublic void run(){//防止因耗时操作导致的卡顿或阻塞MainThreadscanAndRefresh();}});///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() {@Overridepublic 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();}@Overridepublic void onMediaScannerConnected() {if(mListener!=null){mListener.onScannerConnected();}}@Overridepublic 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);}@Overridepublic 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,删除执行后主动发起广播告知系统进行扫描

    @Overridepublic 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 startprivate SyncHandler mHandler, mMainHandler;private HandlerThread mWorkThread;private SafeMediaScanner mediaScanner;@Overridepublic void onResume() {super.onResume();startSyncFile();onDisplayStateChanged();}@Overridepublic 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() {@Overridepublic void onScanCompleted(String arg0, Uri arg1) {}});}}private void doProcessMsg(Message msg) {if (mHandler != null) {mHandler.post(new Runnable() {@Overridepublic void run() {refreshRoots(getActivity());scanAndRefresh();}});mMainHandler.post(new Runnable() {@Overridepublic 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);}@Overridepublic void onChange(boolean selfChange) {if(mHandler!=null) {mHandler.sendEmptyMessage(MSG_SYNC_FILE);}}}private class SyncFileReceiver extends BroadcastReceiver {@Overridepublic 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);}@Overridepublic 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 fileIntent 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;@Overrideprotected 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);}@Overridepublic void onClick(View v) {performFileSearch();}@Overrideprotected 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-sdkandroid:minSdkVersion="19"android:targetSdkVersion="19" />....<providerandroid: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 = newMatrixCursor(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) throwsFileNotFoundException {// Create a cursor with the requested projection, or the default projection.final MatrixCursor result = newMatrixCursor(resolveDocumentProjection(projection));includeFile(result, documentId, null);return result;
}

未完待续…

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

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

    文章大纲 引言 一.Android Storage Access Framework 二.Storage Access Framework 的主要角色成员 1.Document Provider 文件 ...

  2. android SAF存储访问框架

    Android 4.4(API 级别 19)引入了Storage Access Framework存储访问框架 (SAF),SAF 让用户能够在其所有首选文档存储提供程序中方便地浏览并打开文档.图像以 ...

  3. android saf写sd卡,使用SAF(存储访问框架)的Android SD卡写权限

    关于如何在SD卡( android 5及以上版本)中编写(和重命名)文件的大量调查结果后,我认为android提供的新SAF需要获得用户写入SD卡文件的许可. 我在这个文件管理器应用程序ES文件资源管 ...

  4. android的访问存储权限,使用SAF(存储访问框架)的Android SD卡写权限

    关于如何在SD卡(android 5及以上版本)中编写(和重命名)文件的大量调查结果后,我认为android提供的新SAF需要获得用户写入SD卡文件的许可. 我在这个文件管理器应用程序ES文件资源管理 ...

  5. Android SAF(Storage Access Framework)使用攻略

    漫长的假期,在家整理了一下Android 10的适配内容.因为适配篇的篇幅问题,就将这一部本单独出来,也先放出来. 1.介绍 Android 4.4 就引入了存储访问框架 (SAF).借助 SAF,用 ...

  6. SAF(Storage Access Framework)使用攻略

    原文链接: https://weilu.blog.csdn.net/article/details/104199446 https://open.oppomobile.com/wiki/doc#id= ...

  7. Android 进阶 教你打造 Android 中的 IOC 框架 【ViewInject】 (下)

    上一篇博客我们已经带大家简单的吹了一下IoC,实现了Activity中View的布局以及控件的注入,如果你不了解,请参考:Android 进阶 教你打造 Android 中的 IOC 框架 [View ...

  8. 【我的Android进阶之旅】Android 混淆文件资源分类整理之二:将混淆文件拆分成更小粒度的混淆文件

    在我2017年的文章[我的Android进阶之旅]Android 混淆文件资源分类整理中,我已经提及过. 之前将所有的混淆都配置在一个 proguard-rules.pro 这个Android Stu ...

  9. 【我的Android进阶之旅】Android混淆踩坑之各模块各自单独配置混淆,但是将minifyEnabled设置为true导致的编译错误

    一.背景描述 在之前的两篇文章中 [我的Android进阶之旅]Android 混淆文件资源分类整理 [我的Android进阶之旅]Android 混淆文件资源分类整理之二:将混淆文件拆分成更小粒度的 ...

最新文章

  1. Windows Server AppFabric Caching
  2. 点击文字,把input type=radio也选中
  3. 0801 am使用tp框架对数据库增删改查
  4. 会计的思考(3):通过公司例会制度加强财务管理职能
  5. 学习MongoDB(三) Add an Arbiter to Replica Set 集群中加入仲裁节点
  6. 如何把项目部署到云服务器上,如何把项目部署到云主机
  7. Python工作笔记-dictionary的遍历以及enumerate使用以及Py3中has_key的替代
  8. w3school和w3cschool两个网站有什么关系和区别?
  9. muduo 异步日志实现
  10. 怎么添加桌面计算机快捷键,怎么添加桌面快捷方式图标,教你怎么添加桌面快捷方式图标...
  11. 【wav】wav文件查看
  12. android 清理后自动重启,解决Android后台清理APP后,程序自动重启的问题
  13. cocos2d-x lua 框架中 self.super.ctor(self, app) 和 self.super:ctor(app) 的区别
  14. 用ChatGPT处理word表格数据:直接采用ChatGPt和利用ChatGPT编写python脚本两种方法
  15. sass安装步骤、概述、基本语法等
  16. C++核心准则讨论:如果一个类是资源句柄,则它需要一个构造函数,一个析构函数以及复制和/或移动操作
  17. 千锋android培训学院!双非渣本Android四年磨一剑,真香!
  18. python中如何创建一个txt文件
  19. 你的账号是否被泄露了?
  20. 2009年5月9日 紫蓬山观鸟记

热门文章

  1. ATFX:英首相特拉斯的减税政策,或加重高通胀问题
  2. 为什么很多人自律,最后变成了放纵?
  3. web自动化笔记九:验证码的处理方式
  4. android 如何给图片添加水印
  5. win10安装PL2303_Prolific_DriverInstaller_v1.5.0驱动
  6. 职场中如何谈加薪,这么谈,成了也等于失败
  7. JDOJ 3055: Nearest Common Ancestors
  8. 爬虫必备-mysqldb-海量数据解决方案
  9. 让你的Android应用支持转移到SD卡
  10. bzoj2733 永无乡