文章大纲
- 引言
- 一、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() {
@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 存储的物理数据库存储结构
如上图所示,最终所有的存储(尤其是文件存储)都会最终保存到数据库中,毕竟在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);
}
@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;
}
未完待续…