0%

SAF框架

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())) {
// for sdcard
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
// performCreateFile("image/png", "test.png")
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) {
//to do
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);

// Check for the freshest data.
getContentResolver().takePersistableUriPermission(uri, takeFlags);

2. DocumentUi 相关调用解析

对应上述客户端的调用, startActivity解析出来跳转的分别是PickActivityScopedAccessActivity

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
  1. client app 发起 startActivity请求
  2. 跳转到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检查权限, 最终是通过AMSgetGrantedUriPermissions检查是否对package授予了该uri权限,遍历ams的mGrantedUriPermissionsmap 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;
// 请求目录和根目录的uri.
final Uri rootUri = internalRoot.equals(file) ? requestedUri
: getUriPermission(context, storageClient, internalRoot);

// 上述流程走完, 回调该callback
(file, volumeLabel, isRoot, isPrimary, grantedUri, rootUri) -> {
// Checks if the user has granted the permission already.
final Intent intent = getIntentForExistingPermission(activity,
activity.getCallingPackage(), grantedUri, rootUri);
if (intent != null) {
activity.setResult(RESULT_OK, intent);
activity.finish();
return true;
}

// Gets the package label.
final String appLabel = getAppLabel(activity);
if (appLabel == null) {
// Error already logged.
return false;
}

// Sets args that will be retrieve on onCreate()
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;
});
//如果上面没有检查到授权, 没在map中弹出ScopedAccessDialogFragment对话框
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();
}

  1. 跳转到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函数中.

  1. onCreate函数中首先初始化一些特性属性manager等, 绑定到Injector上.
  2. 调用super.onCreate(icicle); 初始化一些属性, 如action. 跳转到BaseActivity的onCreate
  3. 通过setupLayout 设置布局
  4. 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) -> {});
//如selectionMgr/menuManager/focusManager/actionModeController/searchManager 管理文档的各种特性,

//注意这几个
// 跳转到BaseActivity的onCreate

// 初始化state
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();
// 对state的action进行赋值, 根据传进来的action
includeState(state);

// 跳到子类pickActivity的 includeState函数中 对应ACTION_CREATE_DOCUMENT类型
} else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) {
state.action = ACTION_CREATE;
///////

state.showAdvanced = Shared.mustShowDeviceRoot(intent)
|| mInjector.prefs.getShowDeviceRoot();

// Only show the toggle if advanced isn't forced enabled.
state.showDeviceStorageOption = !Shared.mustShowDeviceRoot(intent);

return state;
}

mProviders = DocumentsApplication.getProvidersCache(this);

//下面有几个布局相关的

public PickActivity() {
super(R.layout.documents_activity, TAG);
}
// 相关布局情况请看 DocumentsUI/res/layout/drawer_layout.xml文件
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);




//setupLayout 根据action跳转到不同的布局fragment中
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);
}
}

  1. 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) {

// stack is initialized if it's restored from bundle, which means we're restoring a
// previously stored state.
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); // clientapp
args.putString(EXTRA_DISPLAY_NAME, displayName);

final SaveFragment fragment = new SaveFragment();
fragment.setArguments(args);

final FragmentTransaction ft = fm.beginTransaction();
// saveframent填入该布局
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. 生命周期分析
  1. 当一个fragment被创建的时候,它会经历以下状态.

onAttach()
onCreate()
onCreateView()
onActivityCreated()

  1. 当这个fragment对用户可见的时候,它会经历以下状态。

onStart()
onResume()

  1. 当这个fragment进入“后台模式”的时候,它会经历以下状态。

onPause()
onStop()

  1. 当这个fragment被销毁了(或者持有它的activity被销毁了),它会经历以下状态。

onPause()
onStop()
onDestroyView()
onDestroy() // 本来漏掉类这个回调,感谢xiangxue336提出。
onDetach()

  1. 就像activities一样,在以下的状态中,可以使用Bundle对象保存一个fragment的对象。

onCreate()
onCreateView()
onActivityCreated()

  1. 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();
// 真正的布局文件在这里 fragment_save DocumentsUI/res/layout/fragment_save.xml
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方法保存文档
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,
// 创建完成后会回调 onPickFinished 方法
this::onPickFinished)
.executeOnExecutor(getExecutorForCurrentDirectory());
}

// 查找线程池, 找到线程池后执行对应线程池的executeOnExecutor方法.
private Executor getExecutorForCurrentDirectory() {
final DocumentInfo cwd = mState.stack.peek();
if (cwd != null && cwd.authority != null) {
// executor为前面初始ActionHandler时的 ProviderExecutor::forAuthority
// 这里涉及到java8的一种特性, 实际lookup函数会转向forAuthority函数里执行.
// 类似范型labda表达式的用法. 函数式接口, 方法映射 @FunctionalInterface
return mExecutors.lookup(cwd.authority);
} else {
return AsyncTask.THREAD_POOL_EXECUTOR;
}
}

//这里最终执行CreatePickedDocumentTask的run方法, 至于具体的调用流程, 有待深究,借助AsyncTask的机制, 在其父类的
// doInBackground 方法中调用了run方法.
@Override
protected Uri run(Void... params) {
DocumentInfo cwd = mStack.peek();

// 此处的mDocs对象为Injector初始化时传入的,在BaseActivity onCreate时创建的
// mDocs = DocumentsAccess.create(this); 对应 RuntimeDocumentAccess对象


Uri childUri = mDocs.createDocument(cwd, mMimeType, mDisplayName);

@Override
public Uri createDocument(DocumentInfo parentDoc, String mimeType, String displayName) {
final ContentResolver resolver = mContext.getContentResolver();
// client 对应与getAuthority对应的provider, 这里文档对象如果是外部存储的, 则对应ExternalStorageProvider
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);

/** {@hide} */
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);
// 如果是ExternalStorageProvider会调到其对应的call方法中
> final Bundle out = client.call(METHOD_CREATE_DOCUMENT, null, in);
// 将provider创建完成的文件的完整uri取出来
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);
}

// 对应ContextImpl的内部类ApplicationContentResolver
// Activity内部的ContentResolver的初始化
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();
}


// Install provider will increment the reference count for us, and break
// any ties in the race.
holder = installProvider(c, holder, holder.info,
true /*noisy*/, 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:")) {
// Ignore non-platform methods
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);
// createDocument是子类FileSystemProvider实现的
final String newDocumentId = createDocument(documentId, mimeType, displayName);

// No need to issue new grants here, since caller either has
// manage permission or a prefix grant. We might generate a
// tree style URI if that's how they called us.
// 最后构造完整的uri
final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri,
newDocumentId);
// 将完整Uri 放在 DocumentsContract.EXTRA_URI中
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);
// 子类的getFileForDocId方法 查找或创建parent文件
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);
}
// 最终返回的是docid, 即卷的rootid: + 相对path
childId = getDocIdForFile(file);
// 还要往MediaProvider中同步
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;
}
// ExtenalStorageProvider的buildFile创建 parent 文件
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();
// Create a new anonymous "Recents" RootInfo. It's a faker.
mRecentsRoot = new RootInfo() {{
// Special root for recents
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
// fragment对用户可见的时候,它会经历 onStart onResume
@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);
}
// 触发Loader 执行
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;
// BROADCAST_ACTION是在ProviderCache的doInBackground中发送的, 这个更新是由前面介绍的 DocumentApplication的监听package变化触发的.
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) {
// 通知loader更新, 触发LoaderCallback 的onLoadFinished.
deliverResult(mResult);
}
// 查询mContentChanged的状态
if (takeContentChanged() || mResult == null) {
// 在mContentChanged为true时, 需要forceLoad, 该forceLoad会异步加载数据
forceLoad();
}
}

@Override
public void deliverResult(Collection<RootInfo> result) {
if (isReset()) {
return;
}

mResult = result;
// 通知loader更新, 触发LoaderCallback 的onLoadFinished.
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();
}
// AsyncTaskLoader
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 {
// 跑到子类中, 即RootsLoader中
1. D data = AsyncTaskLoader.this.onLoadInBackground();
return data;
}
}
}

protected D onLoadInBackground() {
1.2 return loadInBackground();
}

1.3 //执行到RootsLoader中, 返回Collection<RootInfo>数据.
@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()) {
// This cursor has been abandoned; just cancel the new data.
onCanceled(data);
} else {
commitContentChanged();
mLastLoadCompleteTime = SystemClock.uptimeMillis();
mTask = null;
if (DEBUG) Log.v(TAG, "Delivering result");
// 这个地方再次走到子类的deliverResult, 由会调用LoaderCallback的onLoadFinished回调
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) {
// ... 前面有一堆的判断条件, 根据state和root的状态判断是否要忽略一些root
matching.add(root);
return matching;
}

/* 前面root的来源需要看下, state是RootFragment初始化时传入的*/
// 还是来源于DocumentApplication 监听package变化, 导致的UpdateTask
// mRoot = mTaskRoots

protected Void doInBackground(Void... params) {
final long start = SystemClock.elapsedRealtime();
// 先加入RecentRoot
mTaskRoots.put(mRecentsRoot.authority, mRecentsRoot);

final PackageManager pm = mContext.getPackageManager();

// Pick up provider with action string
// 查询 PROVIDER_INTERFACE "android.content.action.DOCUMENTS_PROVIDER" 的action , Manifest中定义, provider定义, 存活的provider, 比如ExternalStorageProvider, 通过pm查询.
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);
}
}
// 发送ContentChanged, 前面说过, 会影响一个标志位
LocalBroadcastManager.getInstance(mContext).sendBroadcast(new Intent(BROADCAST_ACTION));
return null;
}

1.1
private void handleDocumentsProvider(ProviderInfo info) {
// Ignore stopped packages for now; we might query them
// later during UI interaction.
//忽略Stopped的provider
if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) {
if (VERBOSE) Log.v(TAG, "Ignoring stopped authority " + info.authority);
mTaskStoppedAuthorities.add(info.authority);
return;
}
// 这里有个需要注意的地方 loadRootsForAuthority, 后面需要再深入/
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);
// 指定关联的Adapter, RootsAdapter 绑定到 mList ListView中, MVC模式
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

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) {
// 通过构造函数传入布局id
super(R.layout.item_root, getStringId(root));
this.root = root;
mActionHandler = actionHandler;
}
// bindView 的参数 convertView 来自于构造函数中的mLayoutId 即 R.layout.item_root
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);

// 对于item的访问可以见下面
public void onCurrentRootChanged() {
if (mAdapter == null) {
return;
}

final RootInfo root = ((BaseActivity) getActivity()).getCurrentRoot();
for (int i = 0; i < mAdapter.getCount(); i++) {
// getItem
final Object item = mAdapter.getItem(i);
if (item instanceof RootItem) {
final RootInfo testRoot = ((RootItem) item).root;
if (Objects.equals(testRoot, root)) {
// 比对item的root信息跟当前的需要展示的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);
}
};

//RootItem

@Override
void open() {
mActionHandler.openRoot(root);
}

final RootItem item = new RootItem(root, mActionHandler);

public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mActionHandler = mInjector.actions;
}

// RootFragment 存活在Activity中, 如果是通过PickActivity进来的, mInjector就是PickActivity的.
mInjector = getBaseActivity().getInjector();

// picker/ActionHandler.java
@Override
public void openRoot(RootInfo root) {
Metrics.logRootVisited(mActivity, Metrics.PICKER_SCOPE, root);
mActivity.onRootPicked(root);
}

// mActivity 指向 pickActivity, 第一个参数
mInjector.actions = new ActionHandler<>(
this,
mState,
mProviders,
mDocs,
mSearchManager,
ProviderExecutor::forAuthority,
mInjector,
mLastAccessed);

class PickActivity extends BaseActivity implements ActionHandler.Addons

abstract class BaseActivity extends Activity implements CommonAddons

// onRootPicked是CommonAddons的接口, 在BaseActivity中实现

@Override
public void onRootPicked(RootInfo root) {
// Clicking on the current root removes search
mSearchManager.cancelSearch();

mInjector.actionModeController.finishActionMode();
mSortController.onViewModeChanged(mState.derivedMode);

// Set summary header's visibility. Only recents and downloads root may have summary in
// their docs.
mState.sortModel.setDimensionVisibility(
SortModel.SORT_DIMENSION_ID_SUMMARY,
root.isRecents() || root.isDownloads() ? View.VISIBLE : View.INVISIBLE);

// Clear entire backstack and start in new root
mState.stack.changeRoot(root);

// Recents is always in memory, so we just load it directly.
// Otherwise we delegate loading data from disk to a task
// to ensure a responsive ui.

// recents在内存中, 直接显示. 最近项
if (mProviders.isRecentsRoot(root)) {
refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
} else {
1. // 一般是下面这项, 走getRootDocument
mInjector.actions.getRootDocument(
root,
TimeoutTask.DEFAULT_TIMEOUT,
2. doc -> mInjector.actions.openRootDocument(doc));
}
}

1. -> // 进到了AbstractActionHandler中
@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. -> // 在找到docmentinfo后, 执行mInjector.actions.openRootDocument(doc)

// 1 和 2中间的步骤是构建了一个GetRootDocumentTask, 通过authority 找到excutor, 请查看前面的ProviderExecutor::forAuthority

public static ProviderExecutor forAuthority(String authority) {
synchronized (sExecutors) {
ProviderExecutor executor = sExecutors.get(authority);
if (executor == null) {
executor = new ProviderExecutor();
executor.setName("ProviderExecutor: " + authority);
// 执行start方法.
executor.start();
// 以authority为key, 保存了一个executor实例到sExecutors池中.
sExecutors.put(authority, executor);
}
return executor;
}
}

// 执行 GetRootDocumentTask的run方法

@Override
public @Nullable DocumentInfo run(Void... args) {
return mDocs.getRootDocument(mRootInfo);
}
// mDoc为DocumentsAccess的接口, 实现在RuntimeDocumentAccess类中

public @Nullable DocumentInfo getRootDocument(RootInfo root) {
// 构建root的完整uri
return getDocument(
DocumentsContract.buildDocumentUri(root.authority, root.documentId));
}

@Override
public @Nullable DocumentInfo getDocument(Uri uri) {
try {
3. return DocumentInfo.fromUri(mContext.getContentResolver(), uri);
}
}
3. -> // 进入 DocumentInfo类中, 构建DocumentInfo

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 {
// 找到对端provider实例
client = DocumentsApplication.acquireUnstableProviderOrThrow(
resolver, uri.getAuthority());
// 查询root下的内容,保存到cursor中
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 -> // updateFromCursor根据provider查到的内容填充DocumentInfo

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. -> // 在有了DocumentInfo数据后, 最终执行mInjector.actions.openRootDocument(doc)
在AbstractHandler中执行

@Override
public void openRootDocument(@Nullable DocumentInfo rootDoc) {
if (rootDoc == null) {
// There are 2 cases where rootDoc is null -- 1) loading recents; 2) failed to load root
// document. Either case we should call refreshCurrentRootAndDirectory() to let
// DirectoryFragment update UI.
mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
} else {
// 不为空, 打开rootdoc.
4. openContainerDocument(rootDoc);
}
}

4. -> // 打开rootdoc
@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()) {
// Regular directory.
currentDoc = doc;
} else if (doc.isArchive()) {
// Archive.
currentDoc = mDocs.getArchiveDocument(doc.derivedUri);
}

assert(currentDoc != null);
mActivity.notifyDirectoryNavigated(currentDoc.derivedUri);

mState.stack.push(currentDoc);
// Show an opening animation only if pressing "back" would get us back to the
// previous directory. Especially after opening a root document, pressing
// back, wouldn't go to the previous root, but close the activity.
// 在前面的stack push后, locationChanged, AnimationView.ANIM_ENTER
final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1)
? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
// 最终执行 refreshCurrentRootAndDirectory(AnimationView.ANIM_ENTER)
5. mActivity.refreshCurrentRootAndDirectory(anim);
}

5. -> // 刷新页面, refreshCurrentRootAndDirectory为CommonAddons的回调, 进到BaseActivity下


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) {
// 设置RootFragment的item的选中状态
roots.onCurrentRootChanged();
}
// 导航栏更新
mNavigator.update();

// Causes talkback to announce the activity's new title
// 标题栏表更
setTitle(mState.stack.getTitle());
// 菜单项更新.
invalidateOptionsMenu();
}

6. -> // refreshDirectory 为具体的实现类, 如果是通过pickActivity进来的, 则是他的.

protected void refreshDirectory(int anim) {
final FragmentManager fm = getFragmentManager();
final RootInfo root = getCurrentRoot();
final DocumentInfo cwd = getCurrentDirectory();
// 如果是最近 item, 展示
if (mState.stack.isRecents()) {
DirectoryFragment.showRecentsOpen(fm, anim);

// In recents we pick layout mode based on the mimetype,
// picking GRID for visual types. We intentionally don't
// consult a user's saved preferences here since they are
// set per root (not per root and per mimetype).
boolean visualMimes = MimeTypes.mimeMatches(
MimeTypes.VISUAL_MIMES, mState.acceptMimes);
mState.derivedMode = visualMimes ? State.MODE_GRID : State.MODE_LIST;
} else {
// Normal boring directory
// 一般是这项, 需要展示root 的内容
7. DirectoryFragment.showDirectory(fm, root, cwd, anim);
}

// Forget any replacement target
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里.

这里总结下相关的接口类的情况, 见下图

DocumentUI

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();
// 使用RecyclerView的布局
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
// 当item被回收的时候回调
public void onViewRecycled(ViewHolder holder) {
cancelThumbnailTask(holder.itemView);
}
});

// RecyclerView的父布局
mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
mRefreshLayout.setOnRefreshListener(this);
mRecView.setItemAnimator(new DirectoryItemAnimator(mActivity));

// pickActivity的 mInjector
mInjector = mActivity.getInjector();
mModel = mInjector.getModel();
// model初始化
mModel.reset();
// 显示内容变更时, 回调mOnDisplayStateChanged
private final Runnable mOnDisplayStateChanged = this::onDisplayStateChanged;
private void onDisplayStateChanged() {
updateLayout(mState.derivedMode);
// 重新绑定adapter
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);
}
// Make the recycler and the empty views responsive to drop events when allowed.
mRecView.setOnDragListener(mDragHoverListener);
return view;
}

绑定了model对象, 可以把model理解为DirectoryFragment对象的model数;据对象, 恰恰是mvc框架中的 model 模型层

1
2
3
/**
* The data model for the current loaded directory.
*/

接着进到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) { 
// 判断调用状态, 前面的includeState
mState = mActivity.getDisplayState();
// 初始化DirectoryFragment传入参数
mLocalState.restore(args);
// RecyclerView绑定的Adapter, 这个地方使用了代理模式, 真正干活的是ModelBackedDocumentsAdapter
mAdapter = new DirectoryAddonsAdapter(
mAdapterEnv,
new ModelBackedDocumentsAdapter(mAdapterEnv, mIconHelper, mInjector.fileTypeLookup)
);
// 绑定到RecyclerView
mRecView.setAdapter(mAdapter);
// 布局相关的管理
mLayout = new GridLayoutManager(getContext(), mColumnCount) {
@Override
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
mFocusManager.onLayoutCompleted();
}
};
mRecView.setLayoutManager(mLayout);
// 添加两个回调, 在调用model的notifyUpdateListeners方法时, 会回调这两个监听的accept方法, 对应model数据更新时的动作

mModel.addUpdateListener(mAdapter.getModelUpdateListener());
mModel.addUpdateListener(mModelUpdateListener);
mActions = mInjector.getActionHandler(mContentLock);
// 选择管理器
mSelectionMgr = mInjector.getSelectionManager(mAdapter, selectionPredicate);
// 选择数据
mSelectionMetadata = new SelectionMetadata(mModel::getItem);
// 继承SelectionObserver抽象类, 订阅, 回调相应的方法, 回调在DefaultSelectionHelper 中, 工厂模式, mSelectionMetadata继承SelectionObserver,此处继承的方法为onItemStateChanged
mSelectionMgr.addObserver(mSelectionMetadata);
// item详情页面查找
mDetailsLookup = new DocsItemDetailsLookup(mRecView);
// 回调onActionItemClicked时, menu click 会执行handleMenuItemClick方法. java8 函数式接口
mActionModeController = mInjector.getActionModeController(
mSelectionMetadata,
this::handleMenuItemClick);
//又添加了监听, item动作回调
mSelectionMgr.addObserver(mActionModeController);
// Kick off loader at least once, 调用AbstractActionHandler中的
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()) {
// 对应最近item
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, 通过buildChildDocumentsUri构造,
// content://authority/document:rootid/children
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();
// 构建查询排序条件, 根据mSortedDimension制定按那个条件排序
mModel.addQuerySortArgs(queryArgs);
cursor = client.query(mUri, null, queryArgs, mSignal);
} else {
cursor = client.query(
mUri, null, null, null, mModel.getDocumentSortQuery(), mSignal);
}
// 数据变动时回调监听
cursor.registerContentObserver(mObserver);
// 下面有好多CursorWrapper, 后面可以详细看一下
cursor = new RootCursorWrapper(mUri.getAuthority(), mRoot.rootId, cursor, -1);

if (mSearchMode && !mFeatures.isFoldersInSearchResultsEnabled()) {
// There is no findDocumentPath API. Enable filtering on folders in search mode.
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);
}
// 最终的结果是封装了cursor
result.cursor = cursor;
} catch (Exception e) {
Log.w(TAG, "Failed to query", e);
// query出错, exception不为空
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. -> // model 数据的update
protected void update(DirectoryResult result) {
assert(result != null);

if (DEBUG) Log.i(TAG, "Updating model with new result set.");

if (result.exception != null) {
// 如果load出错, 清空数据, 回调addUpdateListener注册的监听
Log.e(TAG, "Error while loading directory contents", result.exception);
reset(); // Resets this model to avoid access to old cursors.
notifyUpdateListeners(result.exception);
return;
}

mCursor = result.cursor;
mCursorCount = mCursor.getCount();
doc = result.doc;
2. // 从cursor中load数据
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. -> // updateModelData, 文件夹下内容保存到mFileNames中, 并生成model id用于识别document
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;
}
// Generates a Model ID for a cursor entry that refers to a document. The Model ID is a
// unique string that can be used to identify the document referred to by the cursor.
// If the cursor is a merged cursor over multiple authorities, then prefix the ids
// with the authority to avoid collisions.
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中,
mFileNames.add(getCursorString(mCursor, Document.COLUMN_DISPLAY_NAME));
}

// Populate the positions.
mPositions.clear();
// 以string的model id为key保存了 该项在cursor中的位置. 后面在getItem(string modelid)时用于检索
for (int i = 0; i < mCursorCount; ++i) {
mPositions.put(mIds[i], i);
}
}

3. -> // 这里注册了两个监听, 回调其accept方法
3.1 -> // modelid的更新

private void onModelUpdate(Update event) {
// make sure the delegate handles the update before we do.
// This isn't ideal since the delegate might be listening
// the updates itself. But this is the safe thing to do
// since we read model ids from the delegate
// in our update handler.
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);
}
}
// mModelIds为holdview的内部数据,
}

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);
// 表格还是列表更新布局, 主要调用了 mRecView.requestLayout();方法, View的requestLayout方法会使view重绘, requeLayout() : 控件会重新执行 onMesure() onLayout() ,比如 ScrollView中有LinearLaout ,LinearLayout里面有纵向排列的ImageView和TextView,那么假如ImageView的长宽发生了变化,而要立即在手机上显示这个变化的话,就可调用 imageView.requestLayout();这样的话ScrollView 会重新执行onMesure()这个方法会确定控件的大小然后在确定出自己的宽高,最后在执行onLayout(),这个方法是对所有的子控件进行定位的
// 这里会重绘布局,
updateLayout(mState.derivedMode);
// 更新视图数据 , 对于RecyclerView 会重新调用其onBindViewHolder 方法
mAdapter.notifyDataSetChanged();

if (mRestoredSelection != null) {
mSelectionMgr.restoreSelection(mRestoredSelection);
mRestoredSelection = null;
}

// Restore any previous instance state
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()) {
// Scroll to the top if the sort order actually changed.
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) {
// 设置布局为 item_doc_list
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);
// Warning: mDetails view doesn't exists in layout-sw720dp-land layout
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);
// 由position 反检索出 cursor 和 id.
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;
// 根据cursor更新DocumentInfo对象
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()) {
// Note, we don't show any details for any directory...ever.
hasDetails = false;
} else {
// Show summary if the file is partial. Otherwise, there tends
// to be a bunch of confusing junk in the summary field
// as populated by Downlaods (and others). So to make things
// simpler and clearer for the user in list view, we only
// show the summary if the file is partial >
// which we believe to mean actively downloading.
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));
}

// mDetails view doesn't exists in layout-sw720dp-land layout
if (mDetails != null) {
mDetails.setVisibility(hasDetails ? View.VISIBLE : View.GONE);
}
}
}