Android 4.4(API 级别 19)引入了存储访问框架 (SAF)。它可以让用户方便的浏览、打开文档、图片和其它 的文件。用户可以通过标准的用户界面来浏览文件、打开最近访问的文件。
SAF框架包含下面三个部分:
Document provider
:
一种ContentProvider,DocumentsProvider的子类。Android平台内置了多个,例如Downloads,Images,Videos
Client App
:
应用端,调用`ACTION_OPEN_DOCUMENT`/`ACTION_CREATE_DOCUMENT`,并且接收provider返回的文件
Picker
:
系统界面,允许用户访问所有满足应用端搜索条件的provider提供的文档。例如平台的DocumentsUI
SAF 提供的部分功能如下:
允许用户浏览所有文档提供程序而不仅仅是单个应用中的内容;
让应用获得对provider所拥有文档的长期、持久性访问权限。 用户可以通过此访问权限添加、编辑、保存 和删除提供程序上的文件;
支持多个用户帐户和临时根目录,如只有在插入驱动器后才会出现的 USB 存储提供程序。 注意: 在SAF中,客户端并不会直接与provider交互。客户端请求与文件交互(即读、编辑、创建、删除)的权限。
1. 应用端代码示例 1.1. 授予目录访问权限 1.1.1. 通过ACTION_OPEN_DOCUMENT_TREE
授予应用访问特定目录的权限 1 2 3 4 5 public void performGrantSdWriteAcess () { Intent intent = new Intent (Intent.ACTION_OPEN_DOCUMENT_TREE); intent.addCategory(Intent.CATEGORY_DEFAULT); startActivityForResult(intent, TREE_REQUEST_CODE); }
通过DocumentsUi
picker 选择好目录后,可以在onActivityResult
中对返回的URI进行处理
1 2 3 4 5 6 7 8 9 10 11 12 public void onActivityResult (int requestCode, int resultCode, Intent resultData) { if (requestCode == TREE_REQUEST_CODE && resultData == Activity.RESULT_OK) { Uri uri = null ; if (resultData != null ) { uri = resultData.getData(); final int takeFlags = resultData.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); getContentResolver().takePersistableUriPermission(uri, takeFlags); } } }
在选择好的目录中新建文件或目录:
1 2 3 4 5 6 7 8 9 public void testTree () { Uri doc = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)); try { Uri pic = DocumentsContract.createDocument(getContentResolver(), doc, "image/png" , "test.png" ); Uri dir = DocumentsContract.createDocument(getContentResolver(), doc, DocumentsContract.Document.MIME_TYPE_DIR, "testDir" ); } catch (Exception e) { } }
1.1.2. 通过ACTION_OPEN_EXTERNAL_DIRECTORY
授予应用目录访问权限 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public void onScopedDirectoryTest () { final StorgeManager sm = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE); final List<StorageVolume> volumes = sm.getStorageVolumes(); for (StorageVolume volume: volumes) { final Intent intent = volume.createAccessIntent(); String volumePath = volume.getPath(); if (Environment.getExterStorageState(volumePath).equals(Environment.MEDIA_MOUNTED) && volumePath.equals(EnvironmentEx.getExternalStoragePath())) { if (intent != null ) { startActivityForResult(intent, SCOPED_REQUEST_CODE); } } } } public @Nullable Intent createAccessIntent (String directoryName) { if ((isPrimary() && directoryName == null ) || (directoryName != null && !Environment.isStandardDirectory(directoryName))) { return null ; } final Intent intent = new Intent (ACTION_OPEN_EXTERNAL_DIRECTORY); intent.putExtra(EXTRA_STORAGE_VOLUME, this ); intent.putExtra(EXTRA_DIRECTORY_NAME, directoryName); return intent; }
弹出DocumentUI的请求授予该存储的对话框
1.2. 文档搜索 1.2.1. 通过ACTION_OPEN_DOCUMENT
实现搜索文件 1 2 3 4 5 6 7 public void performFileSearch (String type) { Intent intent = new Intent (Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType(type); startActivityForResult(intent, READ_REQUEST_CODE); }
在选中目标后,可以在onActivityResult中对返回的URI根据自身的业务逻辑进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public void onActivityResult (int requestCode, int resultCode, Intent resultData) { if (requestCode == TREE_REQUEST_CODE && resultData == Activity.RESULT_OK) { Uri uri = null ; if (resultData != null ) { uri = resultData.getData(); getBitmapFromUri(uri); readTextFromUri(uri); } } } private Bitmap getBitmapFromUri (Uri uri) throws IoException { ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r" ); FileDescriptor fd = pfd.getFileDescriptor(); Bitmap image = BitmapFactory.decodeFileDescriptor(fd); pfd.close(); return image; } private String readTextFromUri (Uri uri) throws IoException{ InputStream is = getContentResolver().openInputStream(uri); BufferReader reader = new BufferReader (new InputStreamReader (is)); String line; StringBuilder sb = new StringBuilder () while ((line = reader.readLine()) != null ) { sb.append(line); } is.close(); return sb.toString(); }
1.3. 文档创建 1.3.1. 通过ACTION_CREATE_DOCUMENT
创建文档 1 2 3 4 5 6 7 8 public void performCreateFile (String type, String fileName) { Intent intent = new Intent (Intent.ACTION_CREATE_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType(type); intent.putExtra(Intent.EXTRA_TITLE, fileName); startActivityForResult(intent, WRITE_REQUEST_CODE); }
通过pickerUi, 可以选择新建文件的位置以及文件名, 可以在返回结果中对新返回的文档的uri进行处理.
1.4. 文档编辑 1.4.1. 通过ACTION_OPEN_DOCUMENT
编辑文档 1 2 3 4 5 6 private void editFile () { Intent intent = new Intent (Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("text/plain" ); startActivityForResult(intent, EDIT_REQUEST_CODE); }
通过pickerUi 选择要编辑的文档, 选择完成后, 在返回结果中对文档进行编辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public void onActivityResult (int requestCode, int resultCode, Intent resultData) { if (requestCode == TREE_REQUEST_CODE && resultData == Activity.RESULT_OK) { Uri uri = null ; if (resultData != null ) { uri = resultData.getData(); writeDocument(uri); } } } private writeDocument (Uri uri) { try { ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "w" ); FileOutputStream fos = new FileOutputStream (pfd.getFileDescriptor()); fos.write("write test code" ); fos.close(); pfd.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException) { e.printStackTrace(); } }
1.5. 删除文档 如果获得了文档的uri, 且文档的 Document.COLUMN_FLAGS 包含 SUPPORTS_DELETE
,便可以删除该文档
1 DocumentsContract.deleteDocument(getContentResolver(), uri);
在设置-存储-sd卡进入DocumentsUI,任意选中一个文档查看信息, 可以看到文档是否包含SUPPORTS_DELETE
1.6. 保留权限 当您的应用打开文件进行读取或写入时,系统会为您的应用提供针对该文件的 URI 授权。 该授权将一直持续到用户设备重启
时。但假定您的应用是图像编辑应用,而且您希望用户能够直接从应用中访问他们编辑的最后5 张图像。 如果用户的设备已经重启,您就需要将用户转回系统选取器以查找这些文件,这显然不是理想的做法。 为防止出现这种情况,您可以保留系统为您的应用授予的权限 。 您的应用实际上是获取
了系统提供的持 久 URI 授权。 这使用户能够通过您的应用持续访问文件,即使设备已重启也不受影响:
1 2 3 4 5 6 final int takeFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); getContentResolver().takePersistableUriPermission(uri, takeFlags);
2. DocumentUi 相关调用解析 对应上述客户端的调用, startActivity解析出来跳转的分别是PickActivity
和ScopedAccessActivity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <activity android:name =".picker.PickActivity" android:theme ="@style/DocumentsTheme" android:visibleToInstantApps ="true" > <intent-filter > <action android:name ="android.intent.action.OPEN_DOCUMENT" /> <category android:name ="android.intent.category.DEFAULT" /> <category android:name ="android.intent.category.OPENABLE" /> <data android:mimeType ="*/*" /> </intent-filter > <intent-filter > <action android:name ="android.intent.action.CREATE_DOCUMENT" /> <category android:name ="android.intent.category.DEFAULT" /> <category android:name ="android.intent.category.OPENABLE" /> <data android:mimeType ="*/*" /> </intent-filter > <intent-filter android:priority ="100" > <action android:name ="android.intent.action.GET_CONTENT" /> <category android:name ="android.intent.category.DEFAULT" /> <category android:name ="android.intent.category.OPENABLE" /> <data android:mimeType ="*/*" /> </intent-filter > <intent-filter > <action android:name ="android.intent.action.OPEN_DOCUMENT_TREE" /> <category android:name ="android.intent.category.DEFAULT" /> </intent-filter > </activity > <activity android:name =".ScopedAccessActivity" android:theme ="@android:style/Theme.Translucent.NoTitleBar" > <intent-filter > <action android:name ="android.os.storage.action.OPEN_EXTERNAL_DIRECTORY" /> <category android:name ="android.intent.category.DEFAULT" /> </intent-filter > </activity >
2.1. ACTION_OPEN_EXTERNAL_DIRECTORY
授权外部存储权限流程解析 在应用端调用startActivity后跳转到ScopedAccessActivity
看下provider/activity/client app 三者的调用关系
ExternalStorageProvider
ScopedAccessActivity
client app
client app 发起 startActivity请求
跳转到ScopedAccessActivity的onCreate函数中 a.先从保存的sharePreference中检查key为userId + “|” + packageName + “|” + uuid + “|” + directory的是否指定了 PERMISSION_NEVER_ASK
, 该动作是在弹出对话框点击不允许时勾选的,如果是这种情况,直接退出,返回给clientapp的结果为cancel. b. 调用showFragment弹出对话框提示用户是否给予给定的volume+directory权限. c. 判断目录是否是STANDARD_DIRECTORIES
, 不是返回deny
1 2 3 4 5 6 7 8 9 10 11 12 public static final String[] STANDARD_DIRECTORIES = { DIRECTORY_MUSIC, DIRECTORY_PODCASTS, DIRECTORY_RINGTONES, DIRECTORY_ALARMS, DIRECTORY_NOTIFICATIONS, DIRECTORY_PICTURES, DIRECTORY_MOVIES, DIRECTORY_DOWNLOADS, DIRECTORY_DCIM, DIRECTORY_DOCUMENTS };
d. 对当前存储卷进行遍历, 查找给定volume directory匹配的卷和目录
查找到卷, 对于外部存储, 通过getInternalPathForUser
方法转换了路径
return new File(path.replace(“/storage/“, “/mnt/media_rw/“));
e. 调用getUriPermission返回requestedUri, 通过检索AUTHORITY名获得对应的provider实例, 调用provider的 getDocIdForFileCreateNewDir方法
最终调用到ExternalStorageProvider的 `getDocIdForFileMaybeCreate`方法,返回对应volume的 rootId + ':' + path; 最终返回通过buildTreeDocumentUri构造完的uri
rootid为fsuuid
requestedUri格式 content://com.android.externalstorage.documents/tree/fsuuid:path ,path为相对volume root的路径.
构造requestedUri和rootUri, 请求uri和rooturi
g. 回调callback, 此处开始真正检查是否有权限, 没有权限需要弹出对话框让用户选择授予还是拒绝, getIntentForExistingPermission
检查权限, 最终是通过AMS
的getGrantedUriPermissions
检查是否对package授予了该uri权限,遍历ams的mGrantedUriPermissions
map ArrayMap<GrantUri, UriPermission>. 检查是否匹配requestedUri或rootUri,如果匹配返回createGrantedUriPermissionsIntent(requestedUri)
的intent, intent的data字段保存uri, flag中保存权限信息. 最终将该intent返回给clientapp. 如果没有授权, 即没在map中, 弹出对话框ScopedAccessDialogFragment, 点击允许按钮 , 调用createGrantedUriPermissionsIntent
, 最终还是跟上面的步骤一样返回createGrantedUriPermissionsIntent(requestedUri)
的intent, intent的data字段保存uri, flag中保存权限信息. 最终将该intent返回给clientapp.点击拒绝 , 如果勾选never ask, 以userId + “|” + packageName + “|” + uuid + “|” + directory为key保存PERMISSION_NEVER_ASK
的sharePref
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 private synchronized ContentProviderClient getExternalStorageClient () { if (mExternalStorageClient == null ) { mExternalStorageClient = getContentResolver().acquireContentProviderClient(Providers.AUTHORITY_STORAGE); } return mExternalStorageClient; } bundle = storageProvider.call("getDocIdForFileCreateNewDir" , file.getPath(), null ); final String docId = bundle == null ? null : bundle.getString("DOC_ID" ); final Uri uri = DocumentsContract.buildTreeDocumentUri(Providers.AUTHORITY_STORAGE, docId); return uri; ... requestedUri = uri; final Uri rootUri = internalRoot.equals(file) ? requestedUri : getUriPermission(context, storageClient, internalRoot); (file, volumeLabel, isRoot, isPrimary, grantedUri, rootUri) -> { final Intent intent = getIntentForExistingPermission(activity, activity.getCallingPackage(), grantedUri, rootUri); if (intent != null ) { activity.setResult(RESULT_OK, intent); activity.finish(); return true ; } final String appLabel = getAppLabel(activity); if (appLabel == null ) { return false ; } final Bundle args = new Bundle (); args.putString(EXTRA_FILE, file.getAbsolutePath()); args.putString(EXTRA_VOLUME_LABEL, volumeLabel); args.putString(EXTRA_VOLUME_UUID, storageVolume.getUuid()); args.putString(EXTRA_APP_LABEL, appLabel); args.putBoolean(EXTRA_IS_ROOT, isRoot); args.putBoolean(EXTRA_IS_PRIMARY, isPrimary); final FragmentManager fm = activity.getFragmentManager(); final FragmentTransaction ft = fm.beginTransaction(); final ScopedAccessDialogFragment fragment = new ScopedAccessDialogFragment (); fragment.setArguments(args); ft.add(fragment, FM_TAG); ft.commitAllowingStateLoss(); return true ; }); public void onClick (DialogInterface dialog, int which) { Intent intent = null ; if (which == DialogInterface.BUTTON_POSITIVE) { intent = createGrantedUriPermissionsIntent(mActivity, mActivity.getExternalStorageClient(), mFile); } if (which == DialogInterface.BUTTON_NEGATIVE || intent == null ) { final boolean checked = mDontAskAgain.isChecked(); if (checked) { logValidScopedAccessRequest(mActivity, directory, SCOPED_DIRECTORY_ACCESS_DENIED_AND_PERSIST); setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(), mVolumeUuid, directoryName, PERMISSION_NEVER_ASK); } else { setScopedAccessPermissionStatus(context, mActivity.getCallingPackage(), mVolumeUuid, directoryName, PERMISSION_ASK_AGAIN); } mActivity.setResult(RESULT_CANCELED); } else { logValidScopedAccessRequest(mActivity, directory, SCOPED_DIRECTORY_ACCESS_GRANTED); mActivity.setResult(RESULT_OK, intent); } mActivity.finish(); }
跳转到clientApp的onActivityResult中, 需要调用takePersistableUriPermission
来持久化权限. 该函数最终会调用到ams中.最终保存到/data/system/urigrants.xml文件中.
1 2 3 4 5 6 7 8 9 10 11 12 if (resultCode == Activity.RESULT_OK) { if (requestCode == REQUEST_CODE){ Uri uri = null ; if (data != null ) { uri = data.getData(); final ContentResolver resolver = getContentResolver(); final int modeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);32 resolver.takePersistableUriPermission(uri, modeFlags); } } }
2.2. 通过ACTION_CREATE_DOCUMENT
创建文档调用流程分析 跳转到pickui, 因为在AndroidManifest中的声明. 先跳转到pickActivitity的onCreate
函数中.
onCreate函数中首先初始化一些特性属性manager等, 绑定到Injector上.
调用super.onCreate(icicle); 初始化一些属性, 如action. 跳转到BaseActivity的onCreate
通过setupLayout 设置布局
initLocation初始化当前位置.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 mInjector = new Injector <>( features, new Config (), prefs, new MessageBuilder (this ), DialogController.create(features, this , null ), DocumentsApplication.getFileTypeLookup(this ), (Collection<RootInfo> roots) -> {}); mState = getState(icicle); private State getState (@Nullable Bundle icicle) { State state = new State (); final Intent intent = getIntent(); state.sortModel = SortModel.createModel(); state.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false ); state.excludedAuthorities = getExcludedAuthorities(); includeState(state); } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) { state.action = ACTION_CREATE; state.showAdvanced = Shared.mustShowDeviceRoot(intent) || mInjector.prefs.getShowDeviceRoot(); state.showDeviceStorageOption = !Shared.mustShowDeviceRoot(intent); return state; } mProviders = DocumentsApplication.getProvidersCache(this ); public PickActivity () { super (R.layout.documents_activity, TAG); } mLayoutId = R.layout.documents_activity setContentView (mLayoutId) ;mNavigator = new NavigationViewManager (mDrawer, toolbar, mState, this , breadcrumb); DocumentsApplication.getFileTypeLookup(this ), mInjector.actions = new ActionHandler <>( this , mState, mProviders, mDocs, mSearchManager, ProviderExecutor::forAuthority, mInjector, mLastAccessed); private void setupLayout (Intent intent) { if (mState.action == ACTION_CREATE) { final String mimeType = intent.getType(); final String title = intent.getStringExtra(Intent.EXTRA_TITLE); SaveFragment.show(getFragmentManager(), mimeType, title); } else if (mState.action == ACTION_OPEN_TREE || mState.action == ACTION_PICK_COPY_DESTINATION) { PickFragment.show(getFragmentManager()); } if (mState.action == ACTION_GET_CONTENT) { final Intent moreApps = new Intent (intent); moreApps.setComponent(null ); moreApps.setPackage(null ); RootsFragment.show(getFragmentManager(), moreApps); } else if (mState.action == ACTION_OPEN || mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE || mState.action == ACTION_PICK_COPY_DESTINATION) { RootsFragment.show(getFragmentManager(), (Intent) null ); } }
initLocation当前位置, 主要与startActivity后初始打开的document位置有关, 下面的参数都是clientapp调用时带的, 如下面指定EXTRA_INITIAL_URI
的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 intent.setAction(Intent.ACTION_CREATE_DOCUMENT); intent.setType("plain/text" ); intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, docUri); public void initLocation (Intent intent) { if (mState.stack.isInitialized()) { if (DEBUG) Log.d(TAG, "Stack already resolved for uri: " + intent.getData()); restoreRootAndDirectory(); return ; } if (launchHomeForCopyDestination(intent)) { if (DEBUG) Log.d(TAG, "Launching directly into Home directory for copy destination." ); return ; } if (mFeatures.isLaunchToDocumentEnabled() && launchToDocument(intent)) { if (DEBUG) Log.d(TAG, "Launched to a document." ); return ; } if (DEBUG) Log.d(TAG, "Load last accessed stack." ); loadLastAccessedStack(); }
我们这里从最普遍的场景进行分析, 进来后跳转到 SaveFragment
2.2.1. SaveFragment SaveFragment.show(getFragmentManager(), mimeType, title);
// clientapp传入的type filename
1 2 # 布局置换, 在前面介绍的drawer_layout.xml布局文件中包含该布局 <include layout ="@layout/directory_cluster" />
1 2 3 4 5 6 7 8 9 10 11 12 13 static void show (FragmentManager fm, String mimeType, String displayName) { final Bundle args = new Bundle (); args.putString(EXTRA_MIME_TYPE, mimeType); args.putString(EXTRA_DISPLAY_NAME, displayName); final SaveFragment fragment = new SaveFragment (); fragment.setArguments(args); final FragmentTransaction ft = fm.beginTransaction(); ft.replace(R.id.container_save, fragment, TAG); ft.commitAllowingStateLoss(); }
2.2.1.1. fragment生命周期 Fragment is added | onAttach onCreate onCreateView onActivityCreated onStart onResume || Fragment is active || onPause onStop onDestroyView onDestroy onDetach ||| Fragment is destroyed
2.2.1.1.1. 生命周期分析
当一个fragment被创建的时候,它会经历以下状态.
onAttach() onCreate() onCreateView() onActivityCreated()
当这个fragment对用户可见的时候,它会经历以下状态。
onStart() onResume()
当这个fragment进入“后台模式”的时候,它会经历以下状态。
onPause() onStop()
当这个fragment被销毁了(或者持有它的activity被销毁了),它会经历以下状态。
onPause() onStop() onDestroyView() onDestroy() // 本来漏掉类这个回调,感谢xiangxue336提出。 onDetach()
就像activities一样,在以下的状态中,可以使用Bundle对象保存一个fragment的对象。
onCreate() onCreateView() onActivityCreated()
fragments的大部分状态都和activitie很相似,但fragment有一些新的状态。
onAttached()
—— 当fragment被加入到activity时调用(在这个方法中可以获得所在的activity)。onCreateView()
—— 当activity要得到fragment的layout时,调用此方法,fragment在其中创建自己的layout(界面)。onActivityCreated()
—— 当activity的onCreated()方法返回后调用此方法onDestroyView()
—— 当fragment中的视图被移除的时候,调用这个方法。onDetach()
—— 当fragment和activity分离的时候,调用这个方法。
一旦activity进入resumed状态(也就是running状态),你就可以自由地添加和删除fragment了。因此,只有当activity在resumed状态时,fragment的生命周期才能独立的运转,其它时候是依赖于activity的生命周期变化的。
2.2.1.2. saveFragment onCreateView 填充布局 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public View onCreateView ( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final Context context = inflater.getContext(); final View view = inflater.inflate(R.layout.fragment_save, container, false ); final ImageView icon = (ImageView) view.findViewById(android.R.id.icon); icon.setImageDrawable( IconUtils.loadMimeIcon(context, getArguments().getString(EXTRA_MIME_TYPE))); mDisplayName = (EditText) view.findViewById(android.R.id.title); mDisplayName.addTextChangedListener(mDisplayNameWatcher); mDisplayName.setText(getArguments().getString(EXTRA_DISPLAY_NAME)); mDisplayName.setOnKeyListener( new View .OnKeyListener() { @Override public boolean onKey (View v, int keyCode, KeyEvent event) { .... if (keyCode == KeyEvent.KEYCODE_ENTER && mSave.isEnabled()) { performSave(); private void performSave () { if (mReplaceTarget != null ) { mInjector.actions.saveDocument(getChildFragmentManager(), mReplaceTarget); } else { final String mimeType = getArguments().getString(EXTRA_MIME_TYPE); final String displayName = mDisplayName.getText().toString(); mInjector.actions.saveDocument(mimeType, displayName, mInProgressStateListener); } } return true ; } return false ; } }); mSave = (TextView) view.findViewById(android.R.id.button1); mSave.setOnClickListener(mSaveListener); mSave.setEnabled(false ); ... return view; }
2.2.1.3. saveFragment 保存文件 在点击save时, 最终调用到 mInjector.actions.saveDocument, 新建一个 CreatePickedDocumentTask (继承AsyncTask) 通过AsyncTask机制, 异步执行getExecutorForCurrentDirectory方法, 执行完成后回调主线程的onPickFinished
方法 pickActivity关联的初始executor为前面初始ActionHandler时的 ProviderExecutor::forAuthority
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 void saveDocument ( String mimeType, String displayName, BooleanConsumer inProgressStateListener) { assert (mState.action == ACTION_CREATE); new CreatePickedDocumentTask ( mActivity, mDocs, mLastAccessed, mState.stack, mimeType, displayName, inProgressStateListener, this ::onPickFinished) .executeOnExecutor(getExecutorForCurrentDirectory()); } private Executor getExecutorForCurrentDirectory () { final DocumentInfo cwd = mState.stack.peek(); if (cwd != null && cwd.authority != null ) { return mExecutors.lookup(cwd.authority); } else { return AsyncTask.THREAD_POOL_EXECUTOR; } } @Override protected Uri run (Void... params) { DocumentInfo cwd = mStack.peek(); Uri childUri = mDocs.createDocument(cwd, mMimeType, mDisplayName); @Override public Uri createDocument (DocumentInfo parentDoc, String mimeType, String displayName) { final ContentResolver resolver = mContext.getContentResolver(); try (ContentProviderClient client = DocumentsApplication.acquireUnstableProviderOrThrow( resolver, parentDoc.derivedUri.getAuthority())) { 1. return DocumentsContract.createDocument( client, parentDoc.derivedUri, mimeType, displayName); } catch (Exception e) { Log.w(TAG, "Failed to create document" , e); return null ; } } if (childUri != null ) { mLastAccessed.setLastAccessed(mOwner, mStack); } return childUri; }
最终执行的方法是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 return DocumentsContract.createDocument( client, parentDoc.derivedUri, mimeType, displayName); public static Uri createDocument (ContentProviderClient client, Uri parentDocumentUri, String mimeType, String displayName) throws RemoteException { final Bundle in = new Bundle (); in.putParcelable(DocumentsContract.EXTRA_URI, parentDocumentUri); in.putString(Document.COLUMN_MIME_TYPE, mimeType); in.putString(Document.COLUMN_DISPLAY_NAME, displayName); > final Bundle out = client.call(METHOD_CREATE_DOCUMENT, null , in); return out.getParcelable(DocumentsContract.EXTRA_URI); }
对应client对象, 该对象是通过 DocumentsApplication 的 静态函数acquireUnstableProviderOrThrow 拿到的, 再往下追, 最终发现是ContextImpl的 内部类ApplicationContentResolver 最终是通过调用ams对端的 getContentProvider( getApplicationThread(), auth, userId, stable); 保存了一份客户端. 还要通过installProvider增加引用计数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 DocumentsApplication.acquireUnstableProviderOrThrow( resolver, parentDoc.derivedUri.getAuthority()) public static ContentProviderClient acquireUnstableProviderOrThrow ( ContentResolver resolver, String authority) throws RemoteException { final ContentProviderClient client = resolver.acquireUnstableContentProviderClient( authority); if (client == null ) { throw new RemoteException ("Failed to acquire provider for " + authority); } client.setDetectNotResponding(PROVIDER_ANR_TIMEOUT); return client; } public final @Nullable ContentProviderClient acquireUnstableContentProviderClient ( @NonNull String name) { Preconditions.checkNotNull(name, "name" ); IContentProvider provider = acquireUnstableProvider(name); if (provider != null ) { return new ContentProviderClient (this , provider, false ); } return null ; } public final IContentProvider acquireUnstableProvider (String name) { if (name == null ) { return null ; } return acquireUnstableProvider(mContext, name); } mContentResolver = new ApplicationContentResolver (this , mainThread); private static final class ApplicationContentResolver extends ContentResolver { protected IContentProvider acquireUnstableProvider (Context c, String auth) { return mMainThread.acquireProvider(c, ContentProvider.getAuthorityWithoutUserId(auth), resolveUserIdFromAuthority(auth), false ); } } public final IContentProvider acquireProvider ( Context c, String auth, int userId, boolean stable) { final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable); if (provider != null ) { return provider; } ContentProviderHolder holder = null ; try { holder = ActivityManager.getService().getContentProvider( getApplicationThread(), auth, userId, stable); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } holder = installProvider(c, holder, holder.info, true , holder.noReleaseNeeded, stable); return holder.provider; }
2.2.1.4. provider端调用 上面讲到了最终会执行到ams的getContentProvider拿到provider的ContentProviderHolder对象. 最终返回provider Ams的 mProviderMap 以provider name(Authority)为key保存了provider的实例 ContentProviderRecord.
找到ExternalStorageProvider后, 首先调用基类的call方法, 上面调用的是 METHOD_CREATE_DOCUMENT
ExternalStorageProvider -|> FileSystemProvider -|> DocumentsProvider
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public static final String METHOD_CREATE_DOCUMENT = "android:createDocument" ;public Bundle call (String method, String arg, Bundle extras) { if (!method.startsWith("android:" )) { return super .call(method, arg, extras); } try { return callUnchecked(method, arg, extras); } catch (FileNotFoundException e) { throw new ParcelableException (e); } } private Bundle callUnchecked (String method, String arg, Bundle extras) { else if (METHOD_CREATE_DOCUMENT.equals(method)) { enforceWritePermissionInner(documentUri, getCallingPackage(), null ); final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE); final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); final String newDocumentId = createDocument(documentId, mimeType, displayName); final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, newDocumentId); out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); } }
这里直接走的DocumentProvider的call方法
, 并没有在子类ExternalStorageProvider处理.
createDocument创建流程都是在provider进程中, provider进程会持有其管理目录的(读写访问等的)权限. 创建完文件后, 通过buildDocumentUriMaybeUsingTree方法返回完整的uri
. 由对端即发起方client.call
取出uri.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public String createDocument (String docId, String mimeType, String displayName) throws FileNotFoundException { displayName = FileUtils.buildValidFatFilename(displayName); final File parent = getFileForDocId(docId); if (!parent.isDirectory()) { throw new IllegalArgumentException ("Parent document isn't a directory" ); } final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName); final String childId; if (Document.MIME_TYPE_DIR.equals(mimeType)) { if (!file.mkdir()) { throw new IllegalStateException ("Failed to mkdir " + file); } childId = getDocIdForFile(file); addFolderToMediaStore(getFileForDocId(childId, true )); } else { try { if (!file.createNewFile()) { throw new IllegalStateException ("Failed to touch " + file); } childId = getDocIdForFile(file); } catch (IOException e) { throw new IllegalStateException ("Failed to touch " + file + ": " + e); } } return childId; } protected File getFileForDocId (String docId, boolean visible) throws FileNotFoundException { RootInfo root = getRootFromDocId(docId); return buildFile(root, docId, visible); }
2.2.2. application 分析 上面看到了DocumentsApplication
的相关调用, 这里说下相关的生命周期问题. 应用可以自定义application, DocumentUi进程对应的application 为自定义的DocumentApplication
1 2 3 4 5 6 7 <application android:name =".DocumentsApplication" android:label ="@string/app_label" android:icon ="@drawable/app_icon" android:supportsRtl ="true" android:allowBackup ="true" />
继承自Application, 复写onCreate
onTrimMemory
方法, 监听package
变化. 如ACTION_PACKAGE_ADDED, 初始化
mProviders = new ProvidersCache(this);
1 2 3 4 5 6 7 8 9 10 11 12 13 public ProvidersCache (Context context) { mContext = context; mObserver = new RootsChangedObserver (); mRecentsRoot = new RootInfo () {{ derivedIcon = R.drawable.ic_root_recent; derivedType = RootInfo.TYPE_RECENTS; flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD; title = mContext.getString(R.string.root_recent); availableBytes = -1 ; }}; }
这里涉及到onTrimMemory的地方:
内存资源紧张时释放内存 在应用生命周期的任何阶段 onTrimMemory() 回调方法都可以告诉你设备的内存越来越低的情况, 你可以根据该方法推送的内存紧张级别来释放资源.
优先级从高到低, 需要释放资源的严重性由低到高
TRIM_MEMORY_RUNNING_MODERATE
表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经有点低了,系统可能会开始根据LRU缓存规则来去杀死进程了。
TRIM_MEMORY_RUNNING_LOW
应用处于运行状态并且不会被杀掉, 设备可以使用的内存非常低, 可以把不用的资源释放一些提高性能(会直接影响程序的性能)
TRIM_MEMORY_RUNNING_CRITICAL
应用处于运行状态但是系统已经把大多数缓存应用杀掉了, 你必须释放掉不是非常关键的资源, 如果系统不能回收足够的运行内存, 系统会清除所有缓存应用并且会把正在活动的应用杀掉. 还有, 当你的应用被系统正缓存时, 通过 onTrimMemory() 回调方法可以收到以下几个内存级别:
TRIM_MEMORY_UI_HIDDEN
表示应用程序的所有UI界面被隐藏了,即用户点击了Home键或者Back键导致应用的UI界面不可见.这时候应该释放一些资源
TRIM_MEMORY_BACKGROUND
系统处于低内存的运行状态中并且你的应用处于缓存应用列表的初级阶段. 虽然你的应用不会处于被杀的高风险中, 但是系统已经开始清除缓存列表中的其它应用, 所以你必须释放资源使你的应用继续存留在列表中以便用户再次回到你的应用时能快速恢复进行使用.
TRIM_MEMORY_MODERATE
系统处于低内存的运行状态中并且你的应用处于缓存应用列表的中级阶段. 如果系运行内存收到限制, 你的应用有被杀掉的风险.
TRIM_MEMORY_COMPLETE
系统处于低内存的运行状态中如果系统现在没有内存回收你的应用将会第一个被杀掉. 你必须释放掉所有非关键的资源从而恢复应用的状态.
2.3. ACTION_OPEN_DOCUMENT
编辑文档相关调用分析 在Picker ui的includeState对于ACTION_OPEN_DOCUMENT转化为action的ACTION_OPEN, setupLayout函数中, 对于ACTION_OPEN, 转到RootsFragment
中.
RootsFragment为DocumentUI的侧边栏, sidebar
1 2 3 4 5 6 7 8 9 if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) { state.action = ACTION_OPEN; } else if (mState.action == ACTION_OPEN || mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE || mState.action == ACTION_PICK_COPY_DESTINATION) { RootsFragment.show(getFragmentManager(), (Intent) null ); }
这里还是用到了fragment的生命周期分析 , 在onCreateView
函数中, 主要为布局相关的设置, 用到了ListView, 这里对该ListView添加了右键行为.
1 2 3 final View view = inflater.inflate(R.layout.fragment_roots, container, false );mList = (ListView) view.findViewById(R.id.roots_list); mList.setOnItemClickListener(mItemListener);
在onActivityCreated
函数中, 添加了拖拽处理. 并初始化了Loader, 对应为RootsLoader
, 复写其onCreateLoader
/onLoadFinished
/onLoaderReset
方法, 跟RootsLoader关联的是RootsAdapter
1 private LoaderCallbacks<Collection<RootInfo>> mCallbacks;
这里需要重新回顾一下Loader机制
2.3.1. Loader机制 在RootFragmenti中有进行Loader的相关调用, 作为客户端
客户端触发Loader, 需调用LoaderManager的initLoader
()或restartLoader
(), 向这两个方法中传入一个LoaderCallbacks的实例。LoaderCallbacks有三个回调方法需要实现:onCreateLoader
()、onLoadFinished
()以及onLoaderReset
()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Override public void onResume () { super .onResume(); onDisplayStateChanged(); } public void onDisplayStateChanged () { final Context context = getActivity(); final State state = ((BaseActivity) context).getDisplayState(); if (state.action == State.ACTION_GET_CONTENT) { mList.setOnItemLongClickListener(mItemLongClickListener); } else { mList.setOnItemLongClickListener(null ); mList.setLongClickable(false ); } getLoaderManager().restartLoader(2 , null , mCallbacks); }
2.3.2. restartLoader
调用 调用这个方法,将会重新创建一个指定ID的Loader,如果当前已经有一个和这个ID关联的Loader,那么会对它进行canceled/stopped/destroyed等操作,之后,使用新传入的Bundle参数来创建一个新的Loader,并在数据加载完毕后递交给调用者。并且,在调用完这个函数之后,所有之前和这个ID关联的Loader
都会失效,我们将不会收到它们传递过来的任何数据。
即使之前的loader关联的失效, 并构建新的loader. 先不考虑使之前loader失效的情况, 只考虑创建新loader的情况:
调用过程:
LoaderManager.createLoader() ->LoaderCallbacks.onCreateLoader()
-> 得到loader之后创建LoaderInfo -> LoaderManager.installLoader() -> 将其放入LoaderManager内部维护的mLoaders数组中 -> LoaderInfo.start() -> Loader处于started
状态 ->Loader.startLoading() ->Loader.onStartLoading()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public Loader<Collection<RootInfo>> onCreateLoader (int id, Bundle args) { return new RootsLoader (activity, providers, state); } String BROADCAST_ACTION = "com.android.documentsui.action.ROOT_CHANGED" ; public RootsLoader (Context context, ProvidersCache providers, State state) { super (context); mProviders = providers; mState = state; LocalBroadcastManager.getInstance(context).registerReceiver( mReceiver, new IntentFilter (ProvidersAccess.BROADCAST_ACTION)); } private final BroadcastReceiver mReceiver = new BroadcastReceiver () { @Override public void onReceive (Context context, Intent intent) { onContentChanged(); } }; public void onContentChanged () { if (mStarted) { forceLoad(); } else { mContentChanged = true ; } } @Override protected void onStartLoading () { if (mResult != null ) { deliverResult(mResult); } if (takeContentChanged() || mResult == null ) { forceLoad(); } } @Override public void deliverResult (Collection<RootInfo> result) { if (isReset()) { return ; } mResult = result; if (isStarted()) { super .deliverResult(result); } }
Loader在started
状态下,Loader应该监控数据源的变化,并将新数据发送给客户端
具体来说就是当监控到新数据后,调用Loader.deliverResult()方法,触发LoadCallbacks.onLoadFinished()回调的执行,从而客户端可以从该回调中轻松获取数据。
上面在onStartLoading
中, 一般用法是先调用Loader.deliverResult()方法,触发LoadCallbacks.onLoadFinished
()回调的执行, 另一方面异步执行load数据.
在该处, 调用forceLoad: , 注意RootsLoader继承AsyncTaskLoader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public void forceLoad () { onForceLoad(); } protected void onForceLoad () { super .onForceLoad(); cancelLoad(); mTask = new LoadTask (); if (DEBUG) Log.v(TAG, "Preparing load: mTask=" + mTask); executePendingTask(); } final class LoadTask extends AsyncTask <Void, Void, D> implements Runnable { private final CountDownLatch mDone = new CountDownLatch (1 ); @Override protected D doInBackground (Void... params) { if (DEBUG) Log.v(TAG, this + " >>> doInBackground" ); try { 1. D data = AsyncTaskLoader.this .onLoadInBackground(); return data; } } } protected D onLoadInBackground () { 1.2 return loadInBackground(); } 1.3 @Override public final Collection<RootInfo> loadInBackground () { return mProviders.getMatchingRootsBlocking(mState); }
AsyncTask执行完成后, 会在主线程中执行onPostExecute
方法, 正常情况下又会调用LoaderCallback的onLoadFinished
, 即load完数据后, 通知数据更新的机制.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 protected void onPostExecute (D data) { if (DEBUG) Log.v(TAG, this + " onPostExecute" ); try { 1. AsyncTaskLoader.this .dispatchOnLoadComplete(this , data); } finally { mDone.countDown(); } } void dispatchOnLoadComplete (LoadTask task, D data) { if (mTask != task) { if (DEBUG) Log.v(TAG, "Load complete of old task, trying to cancel" ); dispatchOnCancelled(task, data); } else { if (isAbandoned()) { onCanceled(data); } else { commitContentChanged(); mLastLoadCompleteTime = SystemClock.uptimeMillis(); mTask = null ; if (DEBUG) Log.v(TAG, "Delivering result" ); 2. deliverResult(data); } } }
下面重点看下RootsLoader 对应的LoaderCallback的onLoadFinished
方法, 数据的来源是mProviders.getMatchingRootsBlocking(mState);
查看下相关流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 @Override public Collection<RootInfo> getMatchingRootsBlocking (State state) { waitForFirstLoad(); loadStoppedAuthorities(); synchronized (mLock) { return ProvidersAccess.getMatchingRoots(mRoots.values(), state); } } public static List<RootInfo> getMatchingRoots (Collection<RootInfo> roots, State state) { matching.add(root); return matching; } protected Void doInBackground (Void... params) { final long start = SystemClock.elapsedRealtime(); mTaskRoots.put(mRecentsRoot.authority, mRecentsRoot); final PackageManager pm = mContext.getPackageManager(); final Intent intent = new Intent (DocumentsContract.PROVIDER_INTERFACE); final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0 ); for (ResolveInfo info : providers) { ProviderInfo providerInfo = info.providerInfo; if (providerInfo.authority != null ) { 1. handleDocumentsProvider(providerInfo); } } LocalBroadcastManager.getInstance(mContext).sendBroadcast(new Intent (BROADCAST_ACTION)); return null ; } 1.1 private void handleDocumentsProvider (ProviderInfo info) { if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0 ) { if (VERBOSE) Log.v(TAG, "Ignoring stopped authority " + info.authority); mTaskStoppedAuthorities.add(info.authority); return ; } final boolean forceRefresh = mForceRefreshAll || Objects.equals(info.packageName, mForceRefreshPackage); mTaskRoots.putAll(info.authority, loadRootsForAuthority(mContext.getContentResolver(), info.authority, forceRefresh)); } 2. 加载完roots信息后, 通知客户端RootsFragment更新. @Override public void onLoadFinished ( Loader<Collection<RootInfo>> loader, Collection<RootInfo> roots) {... List<Item> sortedItems = sortLoadResult(roots, excludePackage, handlerAppIntent); mAdapter = new RootsAdapter (activity, sortedItems, mDragListener); mList.setAdapter(mAdapter); mInjector.shortcutsUpdater.accept(roots); onCurrentRootChanged(); }
RootFragment的更新都是通过restartLoader
调用触发forceLoad
过程更新数据随后更新视图. 如点击menu的 “Show Internal Storage”进行的更新动作.
2.3.3. RootsLoader 关联的 RootsAdapter 2.3.3.1. mvc框架 根据MVC模式(model-view-Controller), Adapter处理视图上的数据显示, 处于Controller层
model(模型层)
保持程序的数据状态, 如数据存储, 网络请求等. 与相应的view有一定的耦合, 通过某种事件机制通知view的状态更新, 还会接收Controller的事件
此例子中 RootsLoader 管理的 RootItem数据为model.
view(视图层)
GUI组件, 响应用户的交互行为并触发Controller的逻辑, 还可能通过在Model中注册事件监听model的改变, 以此刷新并展示给用户.
此例子中为ListView
Controller(控制器)
控制器由View根据用户行为触发并响应View的用户交互, 通过修改model并由model的事件机制来触发view更新.
此例子中为RootsAdapter
Adapter是连接后端数据和前端显示的适配器接口,是数据和UI(View)之间一个重要的纽带。在常见的View(ListView,GridView)等地方都需要用到Adapter。
Adapter官方文档
RootsAdapter –|> ArrayAdapter –>BaseAdapter
此处数据是一次性添加的, 每次RootsItem数据变化时, 最后都是调用OnloadFinished方法重新初始化RootsAdapter的.
这里只用到了getView方法, 这里的Apdater是比较简单的
2.3.3.2. 需要关注 RootItem RootItem作为model层, 通过bindView 关联到 item view层
1 2 3 4 5 6 7 8 9 10 11 public RootItem (RootInfo root, ActionHandler actionHandler) { super (R.layout.item_root, getStringId(root)); this .root = root; mActionHandler = actionHandler; } convertView = LayoutInflater.from(parent.getContext()) .inflate(mLayoutId, parent, false ); public void bindView (View convertView) {}
每个Item 最终组成List 填充到RootAdapter中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 final List<RootItem> libraries = new ArrayList <>(); final List<RootItem> others = new ArrayList <>(); for (final RootInfo root : roots) { final RootItem item = new RootItem (root, mActionHandler); Activity activity = getActivity(); if (root.isHome() && !Shared.shouldShowDocumentsRoot(activity)) { continue ; } else if (root.isLibrary()) { libraries.add(item); } else { others.add(item); } } List<Item> sortedItems = sortLoadResult(roots, excludePackage, handlerAppIntent); mAdapter = new RootsAdapter (activity, sortedItems, mDragListener); mList.setAdapter(mAdapter); public void onCurrentRootChanged () { if (mAdapter == null ) { return ; } final RootInfo root = ((BaseActivity) getActivity()).getCurrentRoot(); for (int i = 0 ; i < mAdapter.getCount(); i++) { final Object item = mAdapter.getItem(i); if (item instanceof RootItem) { final RootInfo testRoot = ((RootItem) item).root; if (Objects.equals(testRoot, root)) { mList.setItemChecked(i, true ); return ; } } } }
2.4. DirectoryFragment 前面讲完了RootFragment和SaveFragment, 对应侧边栏和下边栏, 但Document最主要的部分还是中间的内容部分, 即DirectoryFragment.
此处通过RootsFragment的item的点击事件往下追.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 private final OnItemClickListener mItemListener = new OnItemClickListener () { @Override public void onItemClick (AdapterView<?> parent, View view, int position, long id) { final Item item = mAdapter.getItem(position); item.open(); getBaseActivity().setRootsDrawerOpen(false ); } }; @Override void open () { mActionHandler.openRoot(root); } final RootItem item = new RootItem (root, mActionHandler); public void onActivityCreated (Bundle savedInstanceState) { super .onActivityCreated(savedInstanceState); mActionHandler = mInjector.actions; } mInjector = getBaseActivity().getInjector(); @Override public void openRoot (RootInfo root) { Metrics.logRootVisited(mActivity, Metrics.PICKER_SCOPE, root); mActivity.onRootPicked(root); } mInjector.actions = new ActionHandler <>( this , mState, mProviders, mDocs, mSearchManager, ProviderExecutor::forAuthority, mInjector, mLastAccessed); class PickActivity extends BaseActivity implements ActionHandler .Addonsabstract class BaseActivity extends Activity implements CommonAddons @Override public void onRootPicked (RootInfo root) { mSearchManager.cancelSearch(); mInjector.actionModeController.finishActionMode(); mSortController.onViewModeChanged(mState.derivedMode); mState.sortModel.setDimensionVisibility( SortModel.SORT_DIMENSION_ID_SUMMARY, root.isRecents() || root.isDownloads() ? View.VISIBLE : View.INVISIBLE); mState.stack.changeRoot(root); if (mProviders.isRecentsRoot(root)) { refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); } else { 1. mInjector.actions.getRootDocument( root, TimeoutTask.DEFAULT_TIMEOUT, 2. doc -> mInjector.actions.openRootDocument(doc)); } } 1. -> @Override public void getRootDocument (RootInfo root, int timeout, Consumer<DocumentInfo> callback) { GetRootDocumentTask task = new GetRootDocumentTask ( root, mActivity, timeout, mDocs, callback); task.executeOnExecutor(mExecutors.lookup(root.authority)); } 2. -> public static ProviderExecutor forAuthority (String authority) { synchronized (sExecutors) { ProviderExecutor executor = sExecutors.get(authority); if (executor == null ) { executor = new ProviderExecutor (); executor.setName("ProviderExecutor: " + authority); executor.start(); sExecutors.put(authority, executor); } return executor; } } @Override public @Nullable DocumentInfo run (Void... args) { return mDocs.getRootDocument(mRootInfo); } public @Nullable DocumentInfo getRootDocument (RootInfo root) { return getDocument( DocumentsContract.buildDocumentUri(root.authority, root.documentId)); } @Override public @Nullable DocumentInfo getDocument (Uri uri) { try { 3. return DocumentInfo.fromUri(mContext.getContentResolver(), uri); } } 3. -> public static DocumentInfo fromUri (ContentResolver resolver, Uri uri) throws FileNotFoundException { final DocumentInfo info = new DocumentInfo (); info.updateFromUri(resolver, uri); return info; } public void updateFromUri (ContentResolver resolver, Uri uri) throws FileNotFoundException { ContentProviderClient client = null ; Cursor cursor = null ; try { client = DocumentsApplication.acquireUnstableProviderOrThrow( resolver, uri.getAuthority()); cursor = client.query(uri, null , null , null , null ); if (!cursor.moveToFirst()) { throw new FileNotFoundException ("Missing details for " + uri); } 3.1 updateFromCursor(cursor, uri.getAuthority()); } catch (Throwable t) { throw asFileNotFoundException(t); } finally { IoUtils.closeQuietly(cursor); ContentProviderClient.releaseQuietly(client); } } 3.1 -> public void updateFromCursor (Cursor cursor, String authority) { this .authority = authority; this .documentId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID); this .mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); this .displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME); this .lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED); this .flags = getCursorInt(cursor, Document.COLUMN_FLAGS); this .summary = getCursorString(cursor, Document.COLUMN_SUMMARY); this .size = getCursorLong(cursor, Document.COLUMN_SIZE); this .icon = getCursorInt(cursor, Document.COLUMN_ICON); this .deriveFields(); } 2. -> 在AbstractHandler中执行 @Override public void openRootDocument (@Nullable DocumentInfo rootDoc) { if (rootDoc == null ) { mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); } else { 4. openContainerDocument(rootDoc); } } 4. -> @Override public void openContainerDocument (DocumentInfo doc) { assert (doc.isContainer()); if (mSearchMgr.isSearching()) { loadDocument( doc.derivedUri, (@Nullable DocumentStack stack) -> openFolderInSearchResult(stack, doc)); } else { openChildContainer(doc); } } private void openChildContainer (DocumentInfo doc) { DocumentInfo currentDoc = null ; if (doc.isDirectory()) { currentDoc = doc; } else if (doc.isArchive()) { currentDoc = mDocs.getArchiveDocument(doc.derivedUri); } assert (currentDoc != null ); mActivity.notifyDirectoryNavigated(currentDoc.derivedUri); mState.stack.push(currentDoc); final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1 ) ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE; 5. mActivity.refreshCurrentRootAndDirectory(anim); } 5. -> public final void refreshCurrentRootAndDirectory (int anim) { mSearchManager.cancelSearch(); mState.derivedMode = LocalPreferences.getViewMode(this , mState.stack.getRoot(), MODE_GRID); 6. refreshDirectory(anim); final RootsFragment roots = RootsFragment.get(getFragmentManager()); if (roots != null ) { roots.onCurrentRootChanged(); } mNavigator.update(); setTitle(mState.stack.getTitle()); invalidateOptionsMenu(); } 6. -> protected void refreshDirectory (int anim) { final FragmentManager fm = getFragmentManager(); final RootInfo root = getCurrentRoot(); final DocumentInfo cwd = getCurrentDirectory(); if (mState.stack.isRecents()) { DirectoryFragment.showRecentsOpen(fm, anim); boolean visualMimes = MimeTypes.mimeMatches( MimeTypes.VISUAL_MIMES, mState.acceptMimes); mState.derivedMode = visualMimes ? State.MODE_GRID : State.MODE_LIST; } else { 7. DirectoryFragment.showDirectory(fm, root, cwd, anim); } if (mState.action == ACTION_CREATE) { final SaveFragment save = SaveFragment.get(fm); if (save != null ) { save.setReplaceTarget(null ); } } if (mState.action == ACTION_OPEN_TREE || mState.action == ACTION_PICK_COPY_DESTINATION) { final PickFragment pick = PickFragment.get(fm); if (pick != null ) { pick.setPickTarget(mState.action, mState.copyOperationSubType, cwd); } } }
在上述流程中, 最终走到了DirectoryFragment里.
这里总结下相关的接口类的情况, 见下图
2.4.1. DirectoryFragment 展示root内容 1 2 3 4 public static void showDirectory ( FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) { create(fm, root, doc, anim); }
这里创建了DirectoryFragment, 会进入DirectoryFragment的生命周期, 参考生命周期 , 会接着进入下面几个回调中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 public View onCreateView ( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { mActivity = (BaseActivity) getActivity(); final View view = inflater.inflate(R.layout.fragment_directory, container, false ); mProgressBar = view.findViewById(R.id.progressbar); assert mProgressBar != null ; mRecView = (RecyclerView) view.findViewById(R.id.dir_list); mRecView.setRecyclerListener( new RecyclerListener () { @Override public void onViewRecycled (ViewHolder holder) { cancelThumbnailTask(holder.itemView); } }); mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout); mRefreshLayout.setOnRefreshListener(this ); mRecView.setItemAnimator(new DirectoryItemAnimator (mActivity)); mInjector = mActivity.getInjector(); mModel = mInjector.getModel(); mModel.reset(); private final Runnable mOnDisplayStateChanged = this ::onDisplayStateChanged; private void onDisplayStateChanged () { updateLayout(mState.derivedMode); mRecView.setAdapter(mAdapter); } mInjector.actions.registerDisplayStateChangedListener(mOnDisplayStateChanged); mClipper = DocumentsApplication.getDocumentClipper(getContext()); if (mInjector.config.dragAndDropEnabled()) { DirectoryDragListener listener = new DirectoryDragListener ( new DragHost <>( mActivity, DocumentsApplication.getDragAndDropManager(mActivity), mInjector.selectionMgr, mInjector.actions, mActivity.getDisplayState(), mInjector.dialogs, (View v) -> { return getModelId(v) != null ; }, this ::getDocumentHolder, this ::getDestination )); mDragHoverListener = DragHoverListener.create(listener, mRecView); } mRecView.setOnDragListener(mDragHoverListener); return view; }
绑定了model对象, 可以把model理解为DirectoryFragment
对象的model数;据对象, 恰恰是mvc框架中的 model 模型层
接着进到onActivityCreated中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public void onActivityCreated (Bundle savedInstanceState) { mState = mActivity.getDisplayState(); mLocalState.restore(args); mAdapter = new DirectoryAddonsAdapter ( mAdapterEnv, new ModelBackedDocumentsAdapter (mAdapterEnv, mIconHelper, mInjector.fileTypeLookup) ); mRecView.setAdapter(mAdapter); mLayout = new GridLayoutManager (getContext(), mColumnCount) { @Override public void onLayoutCompleted (RecyclerView.State state) { super .onLayoutCompleted(state); mFocusManager.onLayoutCompleted(); } }; mRecView.setLayoutManager(mLayout); mModel.addUpdateListener(mAdapter.getModelUpdateListener()); mModel.addUpdateListener(mModelUpdateListener); mActions = mInjector.getActionHandler(mContentLock); mSelectionMgr = mInjector.getSelectionManager(mAdapter, selectionPredicate); mSelectionMetadata = new SelectionMetadata (mModel::getItem); mSelectionMgr.addObserver(mSelectionMetadata); mDetailsLookup = new DocsItemDetailsLookup (mRecView); mActionModeController = mInjector.getActionModeController( mSelectionMetadata, this ::handleMenuItemClick); mSelectionMgr.addObserver(mActionModeController); 1. mActions.loadDocumentsForCurrentStack(); } 1. -> public void loadDocumentsForCurrentStack () { DocumentStack stack = mState.stack; if (!stack.isRecents() && stack.isEmpty()) { .... } mActivity.getLoaderManager().restartLoader(LOADER_ID, null , mBindings); }
调用restartLoader后进入了Loader的流程
mBindings绑定的Loader为DirectoryLoader, 正是在onCreateLoader返回的.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public Loader<DirectoryResult> onCreateLoader (int id, Bundle args) { Context context = mActivity; if (mState.stack.isRecents()) { if (DEBUG) Log.d(TAG, "Creating new loader recents." ); return new RecentsLoader ( context, mProviders, mState, mInjector.features, mExecutors, mInjector.fileTypeLookup); } else { Uri contentsUri = mSearchMgr.isSearching() ? DocumentsContract.buildSearchDocumentsUri( mState.stack.getRoot().authority, mState.stack.getRoot().rootId, mSearchMgr.getCurrentSearch()) : DocumentsContract.buildChildDocumentsUri( mState.stack.peek().authority, mState.stack.peek().documentId); if (mInjector.config.managedModeEnabled(mState.stack)) { contentsUri = DocumentsContract.setManageMode(contentsUri); } if (DEBUG) Log.d(TAG, "Creating new directory loader for: " + DocumentInfo.debugString(mState.stack.peek())); return new DirectoryLoader ( mInjector.features, context, mState.stack.getRoot(), mState.stack.peek(), contentsUri, mState.sortModel, mInjector.fileTypeLookup, mContentLock, mSearchMgr.isSearching()); } } }
在DirectoryLoader start状态后, 异步加载数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public final DirectoryResult loadInBackground () { synchronized (this ) { if (isLoadInBackgroundCanceled()) { throw new OperationCanceledException (); } mSignal = new CancellationSignal (); } final ContentResolver resolver = getContext().getContentResolver(); final String authority = mUri.getAuthority(); final DirectoryResult result = new DirectoryResult (); result.doc = mDoc; ContentProviderClient client = null ; Cursor cursor; try { client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority); if (mDoc.isInArchive()) { ArchivesProvider.acquireArchive(client, mUri); } result.client = client; Resources resources = getContext().getResources(); if (mFeatures.isContentPagingEnabled()) { Bundle queryArgs = new Bundle (); mModel.addQuerySortArgs(queryArgs); cursor = client.query(mUri, null , queryArgs, mSignal); } else { cursor = client.query( mUri, null , null , null , mModel.getDocumentSortQuery(), mSignal); } cursor.registerContentObserver(mObserver); cursor = new RootCursorWrapper (mUri.getAuthority(), mRoot.rootId, cursor, -1 ); if (mSearchMode && !mFeatures.isFoldersInSearchResultsEnabled()) { cursor = new FilteringCursorWrapper (cursor, null , SEARCH_REJECT_MIMES); } if (mFeatures.isContentPagingEnabled() && cursor.getExtras().containsKey(ContentResolver.QUERY_ARG_SORT_COLUMNS)) { if (VERBOSE) Log.d(TAG, "Skipping sort of pre-sorted cursor. Booya!" ); } else { cursor = mModel.sortCursor(cursor, mFileTypeLookup); } result.cursor = cursor; } catch (Exception e) { Log.w(TAG, "Failed to query" , e); result.exception = e; } ... return result; }
在数据加载完后, 调用LoaderCallback的 onLoadFinished
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 public void onLoadFinished (Loader<DirectoryResult> loader, DirectoryResult result) { if (DEBUG) Log.d(TAG, "Loader has finished for: " + DocumentInfo.debugString(mState.stack.peek())); assert (result != null ); 1. 数据被封装到模型层 model中 mInjector.getModel().update(result); } 1. -> protected void update (DirectoryResult result) { assert (result != null ); if (DEBUG) Log.i(TAG, "Updating model with new result set." ); if (result.exception != null ) { Log.e(TAG, "Error while loading directory contents" , result.exception); reset(); notifyUpdateListeners(result.exception); return ; } mCursor = result.cursor; mCursorCount = mCursor.getCount(); doc = result.doc; 2. updateModelData(); final Bundle extras = mCursor.getExtras(); if (extras != null ) { info = extras.getString(DocumentsContract.EXTRA_INFO); error = extras.getString(DocumentsContract.EXTRA_ERROR); mIsLoading = extras.getBoolean(DocumentsContract.EXTRA_LOADING, false ); } 3. notifyUpdateListeners(); } 2. -> private void updateModelData () { mIds = new String [mCursorCount]; mFileNames.clear(); mCursor.moveToPosition(-1 ); for (int pos = 0 ; pos < mCursorCount; ++pos) { if (!mCursor.moveToNext()) { Log.e(TAG, "Fail to move cursor to next pos: " + pos); return ; } if (mCursor instanceof MergeCursor) { mIds[pos] = getCursorString(mCursor, RootCursorWrapper.COLUMN_AUTHORITY) + "|" + getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID); } else { mIds[pos] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID); } mFileNames.add(getCursorString(mCursor, Document.COLUMN_DISPLAY_NAME)); } mPositions.clear(); for (int i = 0 ; i < mCursorCount; ++i) { mPositions.put(mIds[i], i); } } 3. -> 3.1 -> private void onModelUpdate (Update event) { mDelegate.getModelUpdateListener().accept(event); mModelUpdateListener = new EventListener <Model.Update>() { @Override public void accept (Update event) { if (event.hasException()) { onModelUpdateFailed(event.getException()); } else { onModelUpdate(mEnv.getModel()); } } }; private void onModelUpdate (Model model) { String[] modelIds = model.getModelIds(); mModelIds = new ArrayList <>(modelIds.length); for (String id : modelIds) { mModelIds.add(id); } } } private final class ModelUpdateListener implements EventListener <Model.Update> { @Override public void accept (Model.Update update) { if (DEBUG) Log.d(TAG, "Received model update. Loading=" + mModel.isLoading()); mProgressBar.setVisibility(mModel.isLoading() ? View.VISIBLE : View.GONE); updateLayout(mState.derivedMode); mAdapter.notifyDataSetChanged(); if (mRestoredSelection != null ) { mSelectionMgr.restoreSelection(mRestoredSelection); mRestoredSelection = null ; } final SparseArray<Parcelable> container = mState.dirConfigs.remove(mLocalState.getConfigKey()); final int curSortedDimensionId = mState.sortModel.getSortedDimensionId(); final SortDimension curSortedDimension = mState.sortModel.getDimensionById(curSortedDimensionId); if (container != null && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, false )) { getView().restoreHierarchyState(container); } else if (mLocalState.mLastSortDimensionId != curSortedDimension.getId() || mLocalState.mLastSortDimensionId == SortModel.SORT_DIMENSION_ID_UNKNOWN || mLocalState.mLastSortDirection != curSortedDimension.getSortDirection()) { mRecView.smoothScrollToPosition(0 ); } ... if (!mModel.isLoading()) { mActivity.notifyDirectoryLoaded( mModel.doc != null ? mModel.doc.derivedUri : null ); } } }
更新视图, 这里以ListDocumentHolder为例,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 @Override public DocumentHolder onCreateViewHolder (ViewGroup parent, int viewType) { DocumentHolder holder = null ; final State state = mEnv.getDisplayState(); switch (state.derivedMode) { ... case MODE_LIST: holder = new ListDocumentHolder ( mEnv.getContext(), parent, mIconHelper, mFileTypeLookup); break ; } mEnv.initDocumentHolder(holder); return holder; } public ListDocumentHolder (Context context, ViewGroup parent, IconHelper iconHelper, Lookup<String, String> fileTypeLookup) { super (context, parent, R.layout.item_doc_list); mIconLayout = itemView.findViewById(android.R.id.icon); mIconMime = (ImageView) itemView.findViewById(R.id.icon_mime); mIconThumb = (ImageView) itemView.findViewById(R.id.icon_thumb); mIconCheck = (ImageView) itemView.findViewById(R.id.icon_check); mTitle = (TextView) itemView.findViewById(android.R.id.title); mSummary = (TextView) itemView.findViewById(android.R.id.summary); mSize = (TextView) itemView.findViewById(R.id.size); mDate = (TextView) itemView.findViewById(R.id.date); mType = (TextView) itemView.findViewById(R.id.file_type); mDetails = (LinearLayout) itemView.findViewById(R.id.line2); mIconHelper = iconHelper; mFileTypeLookup = fileTypeLookup; mDoc = new DocumentInfo (); } @Override public void onBindViewHolder (DocumentHolder holder, int position, List<Object> payload) { if (payload.contains(SelectionHelper.SELECTION_CHANGED_MARKER)) { final boolean selected = mEnv.isSelected(mModelIds.get(position)); holder.setSelected(selected, true ); } else { onBindViewHolder(holder, position); } } @Override public void onBindViewHolder (DocumentHolder holder, int position) { String modelId = mModelIds.get(position); Cursor cursor = mEnv.getModel().getItem(modelId); 1. holder.bind(cursor, modelId); final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS); boolean enabled = mEnv.isDocumentEnabled(docMimeType, docFlags); boolean selected = mEnv.isSelected(modelId); if (!enabled) { assert (!selected); } holder.setEnabled(enabled); holder.setSelected(mEnv.isSelected(modelId), false ); mEnv.onBindDocumentHolder(holder, cursor); } 1. -> public void bind (Cursor cursor, String modelId) { assert (cursor != null ); mModelId = modelId; mDoc.updateFromCursor(cursor, getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY)); mIconHelper.stopLoading(mIconThumb); mIconMime.animate().cancel(); mIconMime.setAlpha(1f ); mIconThumb.animate().cancel(); mIconThumb.setAlpha(0f ); mIconHelper.load(mDoc, mIconThumb, mIconMime, null ); mTitle.setText(mDoc.displayName, TextView.BufferType.SPANNABLE); mTitle.setVisibility(View.VISIBLE); boolean hasDetails = false ; if (mDoc.isDirectory()) { hasDetails = false ; } else { if (mDoc.isPartial() && mDoc.summary != null ) { hasDetails = true ; mSummary.setText(mDoc.summary); mSummary.setVisibility(View.VISIBLE); } else { mSummary.setVisibility(View.INVISIBLE); } if (mDoc.lastModified > 0 ) { hasDetails = true ; mDate.setText(Shared.formatTime(mContext, mDoc.lastModified)); } else { mDate.setText(null ); } if (mDoc.size > -1 ) { hasDetails = true ; mSize.setVisibility(View.VISIBLE); mSize.setText(Formatter.formatFileSize(mContext, mDoc.size)); } else { mSize.setVisibility(View.INVISIBLE); } mType.setText(mFileTypeLookup.lookup(mDoc.mimeType)); } if (mDetails != null ) { mDetails.setVisibility(hasDetails ? View.VISIBLE : View.GONE); } } }