DocumentsUI 架构文档
对象:
com.android.documentsui(AOSP Android 13 / TP1A) 工程:C:\code\DocumentsUIStudio(Android Studio / Gradle 移植工程) 源码基线:packages\apps\DocumentsUI本文档描述应用自身架构(分层、模块、运行时骨架、数据流、扩展点)。 移植过程、隐藏 API 编译方案、签名与装机等工程侧内容见
README.md。
目录
1. 全局视图
1.1 这是什么
DocumentsUI 是 Android Storage Access Framework (SAF) 的官方实现,一个应用同时扮演两个角色:
| 角色 | 说明 |
|---|---|
| 文件管理器 App | 用户可见的"文件"应用(FilesActivity),浏览/搜索/复制/移动/删除/压缩/解压 |
| 文档选择器(Picker) | 响应 ACTION_OPEN_DOCUMENT / ACTION_CREATE_DOCUMENT / ACTION_GET_CONTENT / ACTION_OPEN_DOCUMENT_TREE,供第三方应用选文件、选目录 |
| DocumentsProvider 宿主 | 自身也提供 ArchivesProvider(com.android.documentsui.archives),把压缩包当目录浏览 |
| 特权中介 | 唯一持有 MANAGE_DOCUMENTS 的应用,代表其他应用跨 profile 访问文档 |
1.2 在系统中的位置
flowchart TB
subgraph APP["第三方应用 / 系统应用"]
A1["Intent: OPEN_DOCUMENT<br/>CREATE_DOCUMENT<br/>GET_CONTENT<br/>OPEN_DOCUMENT_TREE"]
A2["返回 content:// URI<br/>+ 持久化授权"]
end
subgraph DOCSUI["DocumentsUI(本应用)"]
P["PickActivity<br/>选择器入口"]
F["FilesActivity<br/>文件管理器"]
DIR["DirectoryFragment<br/>目录内容渲染"]
PC["ProvidersCache<br/>存储后端缓存"]
FOS["FileOperationService<br/>跨进程复制/移动/删除"]
AR["ArchivesProvider<br/>压缩包 DocumentsProvider"]
end
subgraph FW["Android Framework"]
PMS["PackageManagerService<br/>解析 DOCUMENTS_PROVIDER 接口"]
DMS["ActivityTaskManagerService<br/>+ DocumentsContract<br/>(URI 授权的校验与分发)"]
ACT["IntentForwarderActivity<br/>跨 profile Intent 转发"]
end
subgraph PROV["各存储后端 DocumentsProvider"]
P1["ExternalStorageProvider(本机存储/SD)"]
P2["DownloadStorageProvider(下载)"]
P3["MediaDocumentsProvider(图片/视频/音频)"]
P4["MtpDocumentsProvider(USB)"]
P5["私有云盘/第三方 Provider"]
end
A1 --> P
A2 -.-> P
F --> DIR
P --> DIR
DIR --> PC
PC -->|"queryRoots / queryDocument"| PROV
PC -->|"PackageManager queryIntentContentProviders"| PMS
F --> FOS
FOS --> PROV
DIR --> AR
AR -->|"archive:// 伪 URI"| DIR
P -->|"跨 profile"| ACT
DMS -.->|"MANAGE_DOCUMENTS 校验"| P
核心约束:DocumentsUI 不直接读写文件和磁盘,所有内容都通过 DocumentsProvider(ContentProvider 的一种)以 content:// URI 访问。它自己是"消费者 + 中介",不是"实现者"(唯一例外是 ArchivesProvider,把 zip/7z/rar 映射成虚拟目录树)。
1.3 规模概览
| 指标 | 值 |
|---|---|
| Java 文件 | 258(documentsui 包 257 + 并入的 SdkLevel 1) |
| 代码行数 | ≈ 46,100 行 |
| 源码包 | 18 个(含根包) |
| 资源文件 | 457(41 layout / 9 menu / 92 个 values-* 语言目录) |
| 最大单类 | dirlist/DirectoryFragment.java 1500 行 |
| 最小 SDK | 29(Android 10) |
| 语言 | 纯 Java(零 Kotlin),依赖 AndroidX + Guava + commons-compress |
2. 分层架构
2.1 分层视图
flowchart TB
subgraph L1["① 入口层 Entry"]
E1["PickActivity<br/>(选择器)"]
E2["FilesActivity<br/>(文件管理器)"]
E3["LauncherActivity<br/>(task 聚合跳板)"]
E4["ScopedAccessActivity"]
E5["InspectorActivity"]
end
subgraph L2["② 宿主基类层 Host"]
B1["BaseActivity(967 行)<br/>模板方法:onCreate/onSaveInstanceState<br/>装配所有 Controller"]
B2["ActivityConfig<br/>行为特化(可选中/可拖拽/管理模式)"]
B3["Injector<br/>运行时依赖容器"]
end
subgraph L3["③ 动作与状态层 Action"]
A1["AbstractActionHandler(1000 行)<br/>动作总线"]
A2["files/ActionHandler<br/>picker/ActionHandler"]
A3["State(Parcelable)<br/>ACTION_* / MODE_* / 过滤条件"]
A4["ActionModeController<br/>选择态管理"]
A5["MenuManager / DialogController<br/>菜单与对话框"]
end
subgraph L4["④ 数据加载层 Data"]
D1["ProvidersCache<br/>Root 缓存(Multimap)"]
D2["DirectoryLoader<br/>AsyncTaskLoader"]
D3["MultiRootDocumentsLoader<br/>RecentsLoader / GlobalSearchLoader"]
D4["Model<br/>cursor ↔ ModelID 映射"]
D5["DocumentsAccess<br/>DocumentInfo 同步查询"]
D6["RootCursorWrapper / FilteringCursorWrapper<br/>SortingCursorWrapper"]
D7["DocumentsApplication<br/>App 级单例容器"]
end
subgraph L5["⑤ 展示层 Presentation"]
V1["DirectoryFragment(1500 行)<br/>RecyclerView + 手势 + 拖拽"]
V2["RootsFragment / DrawerController<br/>侧栏导航"]
V3["DocumentsAdapter 家族<br/>List/Grid/Photo Holder"]
V4["SelectionMetadata / DocsSelectionHelper<br/>recyclerview-selection"]
V5["SortController / SearchViewManager<br/>排序、搜索"]
V6["ThumbnailCache / IconHelper<br/>缩略图与图标"]
end
subgraph L6["⑥ 后台任务层 Background"]
G1["FileOperationService<br/>独立进程"]
G2["Job 家族<br/>Copy/Move/Delete/Compress"]
G3["ProviderExecutor<br/>Provider 线程池"]
G4["CheckedTask / PairedTask / TimeoutTask<br/>带校验与生命周期守卫的异步任务"]
end
subgraph L7["⑦ 平台资源层 Platform"]
R1["ArchivesProvider<br/>压缩包 DocumentsProvider"]
R2["ClipStorage / DocumentClipper<br/>剪贴板"]
R3["LocalPreferences / PreferencesMonitor / BackupAgent"]
R4["ThemeOverlayManager / DevicePolicyResources"]
R5["UserId / UserIdManager / ProfileTabs*<br/>多用户与跨 profile"]
end
L1 --> L2 --> L3 --> L4 --> L5
L5 --> L6
L5 --> L7
L4 --> L7
2.2 各层职责与依赖方向
| 层 | 职责 | 关键约束 |
|---|---|---|
| ① 入口层 | 解析 Intent,决定"我是选择器还是管理器" | 只做 Intent 解析与参数转换,不碰数据 |
| ② 宿主基类层 | 统一 Activity 生命周期与控制器装配 | 依赖倒置:基类定义抽象钩子,子类只填特化逻辑 |
| ③ 动作与状态层 | 把用户意图翻译成数据操作 | 所有动作最终汇入 AbstractActionHandler 单一入口 |
| ④ 数据加载层 | 与 DocumentsProvider 通信,产出 Cursor | 全部在后台线程(AsyncTaskLoader / ProviderExecutor) |
| ⑤ 展示层 | 渲染 + 手势 + 选择 | 只读 Model,不直接持有 Cursor |
| ⑥ 后台任务层 | 长耗时文件操作,带进度与取消 | 独立进程(:com.android.documentsui.services) |
| ⑦ 平台资源层 | 跨用户、剪贴板、偏好、主题、压缩包 | 与系统服务对接,权限敏感 |
依赖方向自上而下,不存在反向依赖:数据层不知道 UI 的存在(通过 EventListener<Update> 回调通知,而非直接调用)。
3. 工程与构建架构
3.1 目录结构
DocumentsUIStudio/
├── build.gradle.kts AGP 8.1.2 声明
├── settings.gradle.kts include(":app")
├── gradle.properties ★非 final R + 平台签名口令
├── local.properties SDK 路径(不入库)
├── README.md 移植与装机文档
├── ARCHITECTURE.md ← 本文档
├── scripts/
│ └── sync_back.sh 改动同步回 AOSP 源码树
└── app/
├── build.gradle.kts ★签名 + 隐藏 API 编译方案 + 依赖
├── proguard.flags AOSP 原混淆规则
├── keys/platform.jks 平台签名密钥(不入库)
├── libs/
│ └── framework-13-android-all.jar 178MB 全量框架 jar(仅编译用)
└── src/main/
├── AndroidManifest.xml
├── java/
│ ├── com/android/documentsui/ ← 257 个文件(应用全部逻辑)
│ └── com/android/modules/utils/build/
│ └── SdkLevel.java ← 从 modules-utils 并入
└── res/ ← 457 个资源文件
3.2 构建配置要点
| 配置项 | 值 | 为什么 |
|---|---|---|
| AGP / Gradle / JDK | 8.1.2 / 8.2 / 17 | JDK 21 会让 AGP 8.1.2 的 JdkImageTransform(jlink)失败 |
compileSdk | 33 | 对齐 TP1A |
minSdk / targetSdk | 29 / 30 | 对齐 AOSP documentsui_defaults |
sourceCompatibility | 11 | AOSP 该模块的 javac 级别 |
android.nonFinalResIds | false | ★AGP 8 默认 true 会让 R.id.x 非常量 → @IntDef/case R.id.* 全挂 |
android.nonTransitiveRClass | false | 保持 AOSP 的 R 类可见性语义 |
versionCode | 33 | ★对齐设备预置版,低于它会被 INSTALL_FAILED_VERSION_DOWNGRADE 拒绝 |
signingConfigs | platform(debug + release) | ★与 /system/priv-app/DocumentsUI 同签名才能覆盖安装并拿到签名权限 |
lint.abortOnError | false | AOSP 源码里大量 deprecated/隐藏用法是预期行为 |
isMinifyEnabled | false(release 保留 proguard.flags) | 需要瘦身时再打开 |
3.3 隐藏 API 编译方案(架构级影响)
DocumentsUI 在 AOSP 中以 sdk_version: "system_current" 编译,源码合法使用了 @SystemApi / @hide 符号。移植到 Gradle 后,标准 compileSdk 的 android.jar 里没有这些符号。解法:
flowchart LR
subgraph CP["javac 编译 classpath(顺序即优先级)"]
J1["framework-13-android-all.jar<br/>178MB · 66510 条目 · 含隐藏 API"] --> J2["platforms/android-33/android.jar<br/>仅公开 API"]
J2 --> J3["androidx / material / guava / commons-compress ..."]
end
J1 -.->|"android.* 类型命中此份"| OUT["编译产物<br/>class 文件"]
J2 -.->|"java.* 类型仍在标准库"| OUT
J1 -.->|"❌ 只参与编译"| APK["app-debug.apk<br/>不含框架类"]
三个必须遵守的点(详见 README §3):
- 前置到
classpath,不是bootstrapClasspath—— AGP 8 把android.jar放在常规编译 classpath 上,只设 bootclasspath 等于没设。 - 在
doFirst {}/gradle.projectsEvaluated {}里改写 —— AGP 会在配置阶段覆盖顶层configureEach。 gradle.properties必须android.nonFinalResIds=false—— 这是 105 个编译错误里的另一半个根因。
运行时用的仍是设备真实 framework(jar 不打进 APK)。debug 包因
debuggable会强制开启 Java 断言,而量产链路(D8/R8)会剥掉断言 —— 这个差异曾导致启动闪退,详见 README §4.1。
3.4 依赖与 AOSP 的映射
| Gradle 坐标 | AOSP 侧 | 用途 |
|---|---|---|
androidx.appcompat:appcompat:1.6.1 | androidx.appcompat_appcompat | AppCompatActivity、主题 |
com.google.android.material:material:1.9.0 | com.google.android.material_material | Toolbar、TabLayout、Snackbar |
androidx.recyclerview:recyclerview:1.3.0 | androidx.recyclerview_recyclerview | 目录列表 |
androidx.recyclerview:recyclerview-selection:1.1.0 | androidx.recyclerview_recyclerview-selection | SelectionTracker 选择框架 |
androidx.transition:transition:1.4.1 | androidx.transition_transition | 进入/退出转场 |
androidx.legacy:* | androidx.legacy_legacy-support-* | 旧 API 兼容 |
androidx.core / annotation / loader / localbroadcastmanager / swiperefreshlayout | 上述库的传递依赖(此处显式声明) | 核心工具、AsyncTaskLoader、应用内广播、下拉刷新 |
com.google.guava:guava:31.1-android | external/guava | Multimap、Lists 等集合增强 |
org.apache.commons:commons-compress:1.21 | external/apache-commons-compress | zip/7z/tar 解析 |
com.google.code.findbugs:jsr305:3.0.2 | external/jsr305 | @Nullable / @GuardedBy |
| —(源码并入) | modules-utils-build_system | SdkLevel |
| —(桩代码) | docsui-statsd(生成物) | DocumentsStatsLog |
4. 代码结构地图
4.1 包职责一览
| 包 | 文件数 | 职责 | 代表类 |
|---|---|---|---|
(根) documentsui | 68 | 应用骨架、跨包共享的控制器与工具 | DocumentsApplication、BaseActivity、Injector、AbstractActionHandler、Model、DirectoryLoader、NavigationViewManager |
dirlist | 37 | 目录内容渲染核心:RecyclerView、Adapter、ViewHolder、手势、拖拽、选择 | DirectoryFragment、DocumentsAdapter、DocumentHolder、DocsSelectionHelper |
base | 31 | 无 UI 依赖的基础类型与工具(可被任意层引用) | State、DocumentInfo、DocumentStack、RootInfo、UserId、Durable、Features |
inspector | 18 | 文件详情页 | InspectorActivity、InspectorController、DetailsView、MediaView、MetadataLoader |
picker | 16 | 选择器特化:保存/确认/最近访问记录 | PickActivity、PickFragment、SaveFragment、LastAccessedProvider |
sidebar | 15 | 侧栏导航项与列表 | RootsFragment、RootsAdapter、Item/RootItem/AppItem、UserItemsCombiner |
archives | 11 | 压缩包 → 虚拟文档树(本应用唯一自实现的 Provider) | ArchivesProvider、ReadableArchive、WriteableArchive、Proxy |
services | 10 | 后台文件操作(独立进程) | FileOperationService、Job、CopyJob、DeleteJob、CompressJob |
roots | 8 | 存储后端(Root)的发现、缓存、加载 | ProvidersCache、RootsLoader、RootCursorWrapper、BootReceiver |
files | 8 | 文件管理器特化 | FilesActivity、ActionHandler、LauncherActivity、Config |
ui | 7 | 通用 UI 组件与对话框 | DialogController、MessageBuilder、Snackbars、OperationProgressDialog |
sorting | 7 | 排序模型与表头交互 | SortModel、SortDimension、SortController、SortingCursorWrapper |
queries | 6 | 搜索 UI 与搜索数据源 | SearchViewManager、SearchFragment、SearchChipViewManager、SearchHistoryManager |
clipping | 6 | 剪贴板(复制/剪切)持久化 | ClipStorage、DocumentClipper、UrisSupplier |
prefs | 4 | 本地偏好与备份 | LocalPreferences、PreferencesMonitor、BackupAgent |
util | 3 | 跨 profile / 格式化 / 版本工具 | CrossProfileUtils、FormatUtils、VersionUtils |
theme | 1 | 运行时主题 RRO 管理 | ThemeOverlayManager |
selection | 1 | 选择框架示例(桩) | selection/demo/SelectionDemoActivity |
modules/utils/build | 1 | 并入的 SdkLevel | SdkLevel |
4.2 依赖关系图
flowchart TD
ROOT["(根) documentsui<br/>骨架与控制器"]
BASE["base<br/>基础类型(无 UI 依赖)"]
DIRLIST["dirlist<br/>列表渲染"]
FILES["files / picker<br/>两个特化入口"]
SIDEBAR["sidebar<br/>侧栏"]
ROOTS["roots<br/>Root 缓存"]
SERVICES["services<br/>后台任务"]
QUERIES["queries / sorting / clipping / ui / util / theme / prefs<br/>横切能力"]
ARCHIVES["archives<br/>压缩包 Provider"]
INSPECTOR["inspector<br/>详情页"]
BASE --> ROOT
BASE --> DIRLIST
BASE --> ROOTS
BASE --> SIDEBAR
BASE --> SERVICES
BASE --> QUERIES
BASE --> INSPECTOR
ROOT --> FILES
ROOT --> DIRLIST
ROOT --> SIDEBAR
ROOT --> ROOTS
ROOT --> QUERIES
DIRLIST --> SIDEBAR
ROOTS --> SIDEBAR
DIRLIST --> SERVICES
ROOT --> SERVICES
ARCHIVES -.->|"伪 URI 交给 DirectoryFragment 渲染"| DIRLIST
INSPECTOR -.->|"复用 DocumentsAccess"| ROOT
关键观察:base 包零依赖其他业务包(只有 java.* / android.* / androidx.*),是整个工程的稳定内核;根包是事实上的"共享控制器层",被所有人依赖 —— 这也是为什么它有 68 个文件却难以再拆分。
5. 运行时骨架
5.1 Application 层 —— 全局单例容器
DocumentsApplication 是唯一的进程级容器,onCreate 时构造所有跨 Activity 存活的重对象:
flowchart TB
APP["DocumentsApplication.onCreate()"]
APP --> C1["ProvidersCache<br/>(Root 缓存 + 变更观察者)"]
APP --> C2["ThumbnailCache<br/>(LRU,按 Uri+尺寸索引)"]
APP --> C3["ClipStorage<br/>(剪贴板 URI 落盘)"]
APP --> C4["DocumentClipper<br/>(RuntimeDocumentClipper)"]
APP --> C5["DragAndDropManager<br/>(Ctrl 键态 + 默认操作判定)"]
APP --> C6["UserIdManager<br/>(在哪些 profile 上工作)"]
APP --> C7["FileTypeMap<br/>(MIME → 友好类型名)"]
APP --> R1["注册 PACKAGE_ADDED/CHANGED/REMOVED/DATA_CLEARED<br/>→ 刷新 Provider 缓存"]
APP --> R2["注册 MANAGED_PROFILE_ADDED/REMOVED/UNLOCKED/UNAVAILABLE<br/>→ 刷新跨 profile 状态"]
APP --> L1["LocalBroadcastManager<br/>(应用内事件总线)"]
style APP fill:#e8f0fe,stroke:#4285f4
| 静态访问器 | 用途 |
|---|---|
getProvidersCache(ctx) | 取 Root 缓存(几乎每个 Activity 都用) |
getThumbnailCache(ctx) | 取缩略图缓存 |
acquireUnstableProviderOrThrow(resolver, authority) | 获取 ContentProviderClient,带 20 秒 ANR 超时 |
设计要点:这些对象之所以放 Application 而非 Activity,是因为切换目录、切换 Activity 时它们必须存活(缓存不能每次重建)。
DocumentsApplication用静态方法 + 强转 Context 暴露它们,属于 AOSP 内部的简化 DI,不走任何框架。
5.2 Activity 层 —— 模板方法模式
整个应用只有 2 个 BaseActivity 子类(其余 Activity 是独立小页面:LauncherActivity、ScopedAccessActivity、InspectorActivity):
classDiagram
class AppCompatActivity
class BaseActivity {
<<abstract>>
#State mState
#Injector mInjector
#ProvidersCache mProviders
#DocumentsAccess mDocs
#DrawerController mDrawer
#NavigationViewManager mNavigator
#SortController mSortController
#SearchViewManager mSearchManager
#AppsRowManager mAppsRowManager
+onCreate(Bundle)
+onSaveInstanceState(Bundle)
#refreshDirectory(int anim)*
#includeState(State)*
#onDirectoryCreated(DocumentInfo)*
#getInjector()* Injector
}
class FilesActivity {
+getInjector()
#refreshDirectory(int)
#includeState(State)
}
class PickActivity {
+getInjector()
#refreshDirectory(int)
#includeState(State)
}
AppCompatActivity <|-- BaseActivity
BaseActivity <|-- FilesActivity
BaseActivity <|-- PickActivity
BaseActivity.onCreate() 的固定装配顺序(这是整个应用的启动骨架):
① applyStyle(DocumentsDefaultTheme) ← 先套主题,避免资源找不到
② setContentView(mLayoutId) ← 子类构造时传入的布局
③ setContainer() ← 建立 Navigator/Drawer 的容器
④ getInjector() ← 子类返回自己的 Injector(含 actions/config/menuManager)
⑤ getState(savedInstanceState) ← 恢复或新建 State(ACTION_* / 过滤条件 / 排序)
⑥ DrawerController.create(this, config) ← 侧栏(管理器:可拖拽;选择器:简化)
⑦ Metrics.logActivityLaunch() ← 埋点
⑧ ProvidersCache = DocumentsApplication.getProvidersCache()
DocumentsAccess.create(this, state) ← 两个数据入口
⑨ setSupportActionBar(Toolbar)
⑩ NavigationViewManager(...) ← 面包屑 + profile Tab + 标题
⑪ 注册 SearchManagerListener ← 搜索变化 → 重新加载目录
⑫ RootsMonitor / PreferencesMonitor ← 监听 Root 与偏好变化
子类只需要填 3 个钩子:getInjector()(我要用哪套动作实现)、refreshDirectory(int)(怎么重载当前目录)、includeState(State)(我的 Intent 语义是什么)。这是整个架构最漂亮的地方 —— 文件管理器与选择器共享 95% 的代码。
5.3 Injector —— 极简 DI 容器
Injector<T extends ActionHandler> 是手工装配的依赖容器,不走注解处理器:
| 字段 | 生命周期 | 说明 |
|---|---|---|
features | App | 来自 config.xml 的功能开关(Features) |
config | App | ActivityConfig 子类,行为特化 |
messages | App | MessageBuilder,统一文案 |
fileTypeLookup | App | FileTypeMap,MIME → 类型名 |
shortcutsUpdater | App | 动态快捷方式更新 |
menuManager | Activity | 菜单构造(files / picker 两套) |
dialogs | Activity | 对话框工厂 |
searchManager | Activity | 搜索状态机 |
appsRowManager | Activity | 搜索页"应用"横向条 |
pickResult | Activity | 选择结果累积(仅 picker 非空) |
@ContentScoped actions | 内容 | ★动作实现(files / picker 各一套) |
@ContentScoped actionModeController | 内容 | 选择态 |
@ContentScoped profileTabsController | 内容 | 跨 profile tab |
@ContentScoped focusManager | 内容 | 键盘/焦点导航 |
@ContentScoped selectionMgr | 内容 | DocsSelectionHelper |
mModel | 内容 | 当前目录的 Model |
@ContentScoped(自定义注解,纯文档语义)标记"切目录时要重置"的字段。Injector.reset(...)负责在根切换时用ContentLock挂起重置,避免 loading 期间 UI 抖动。
5.4 三大控制器
| 控制器 | 职责 | 与谁协作 |
|---|---|---|
NavigationViewManager | 面包屑(HorizontalBreadcrumb)、profile Tab、标题、Drawer 开关 | DrawerController、State、ProfileTabs |
SortController + SortModel | 排序维度与方向,持久化到偏好 | DirectoryLoader(生成 SortingCursorWrapper) |
ActionModeController | 选择态生命周期(长按进入、多选、退出) | DocsSelectionHelper、MenuManager |
5.5 Fragment 层
| Fragment | 宿主 | 职责 |
|---|---|---|
DirectoryFragment(1500 行) | Files + Pick | 核心 UI:RecyclerView、拖拽、手势缩放、空态/错误态、缩略图调度 |
RootsFragment | 侧栏 Drawer | Root 列表(RootsAdapter) |
PickFragment / SaveFragment | Pick 底部栏 | 选择确认条 / 文件名编辑 |
SearchFragment | Files | 搜索态内容 |
| 对话框 Fragment | 两者 | CreateDirectoryFragment、RenameDocumentFragment、DeleteDocumentFragment、SortListFragment、ConfirmFragment、OperationDialogFragment |
6. 核心数据流
6.1 启动链路
sequenceDiagram
participant L as 桌面 / 其他应用
participant LA as LauncherActivity<br/>(Theme.NoDisplay)
participant FA as FilesActivity
participant BA as BaseActivity
participant DF as DirectoryFragment
participant DL as DirectoryLoader
participant PC as ProvidersCache
participant P as DocumentsProvider
L->>LA: MAIN/LAUNCHER 或 OPEN_DOCUMENT
Note over LA: 跳板:查找已存在的同 URI task<br/>找到则 bringToFront,否则转发
LA->>FA: 携带 pseudo URI 的 Intent
FA->>BA: onCreate(BaseActivity 装配)
BA->>BA: getInjector / getState / DrawerController
BA->>PC: getProvidersCache
BA->>PC: getDefaultRootBlocking(state) / getRootsBlocking
PC->>P: queryRoots(首次/失效时)
P-->>PC: Root 游标
BA->>DF: showDirectory(stack)
DF->>DL: DirectoryLoader(stack)
DL->>P: queryChildDocuments
P-->>DL: Cursor
DL-->>DF: DirectoryResult
DF->>DF: Model.update(result) → Adapter 刷新
LauncherActivity 的特殊价值:FilesActivity 声明了 documentLaunchMode="intoExisting",配合伪文档 URI(com.android.documentsui.launchControl),可以让多次"打开文件管理器"复用同一个 task,而不是叠加无数个窗口。
6.2 Root 发现与缓存链路
ProvidersCache 是存储后端注册表,也是应用里最核心的缓存。
flowchart TB
subgraph TRIG["触发源"]
T1["Application.onCreate<br/>updateAsync(forceRefreshAll)"]
T2["包变更广播<br/>PACKAGE_ADDED/REMOVED"]
T3["Profile 变更广播"]
T4["ContentObserver<br/>RootsChangedObserver(每用户一个)"]
T5["BootReceiver / PreBootReceiver"]
T6["RootsLoader(侧栏加载)"]
end
subgraph CORE["核心"]
U["updateAsync / updatePackageAsync / updateAuthorityAsync"]
SEM["Semaphore(1)<br/>保证同一时刻只有一次全量更新"]
MPT["MultiProviderUpdateTask"]
LOAD["loadRootsForAuthority()"]
ROOTS[("mRoots<br/>Multimap")]
REC[("mRecentsRoots<br/>Map")]
STOP["mStoppedAuthorities<br/>(Provider 已停用)"]
LATCH["mFirstLoad CountDownLatch<br/>FIRST_LOAD_TIMEOUT_MS = 5000"]
end
subgraph QOUT["查询出口"]
Q1["getRootsBlocking()"]
Q2["getMatchingRootsBlocking(state)"]
Q3["getDefaultRootBlocking(state)"]
Q4["getRootOneshot / getRootBlocking(userId, authority, rootId)"]
Q5["getRecentsRoot(userId) / isRecentsRoot(root)"]
Q6["getApplicationName(userId, authority)"]
end
T1 & T2 & T3 & T4 & T5 & T6 --> U
U --> SEM --> MPT --> LOAD
LOAD -->|"queryRoots"| ROOTS
LOAD -->|"已停用"| STOP
U --> REC
U --> LATCH
ROOTS --> Q1 & Q2 & Q3 & Q4
REC --> Q5
| 关键机制 | 说明 |
|---|---|
| 按用户分桶 | 所有缓存键都是 UserAuthority(UserId + authority)或 UserId,天然支持多用户/工作资料 |
| 首次加载闩锁 | CountDownLatch(1) + 5 秒超时:避免 UI 在 Provider 还没应答时拿到空列表 |
| Recents 伪 Root | 每个用户一个合成 Root(3 个 flag:LOCAL_ONLY | SUPPORTS_IS_CHILD | SUPPORTS_SEARCH)—— 该 flag 与断言的漂移正是启动闪退的根因(README §4.1) |
| ContentObserver | 每个用户一个 RootsChangedObserver,监听 DocumentsContract.buildRootsUri,Provider 变化时主动失效 |
| 并发保护 | Semaphore(1) 串行化全量刷新;mLock 保护缓存读写 |
6.3 目录内容加载链路(最核心)
flowchart LR
subgraph UITH["UI 线程"]
DF["DirectoryFragment"]
MDL["Model"]
ADP["ModelBackedDocumentsAdapter"]
RV["RecyclerView"]
end
subgraph BGH["后台线程"]
DL["DirectoryLoader<br/>AsyncTaskLoader"]
PE["ProviderExecutor<br/>(按 authority 串行)"]
end
subgraph SYSH["系统"]
P["DocumentsProvider"]
end
DF -->|"1. restartLoader(stack, mime, state)"| DL
DL -->|"2. getExecutor()"| PE
DL -->|"3. queryChildDocuments"| P
P -->|"4. Cursor"| DL
DL --> DL2["5. 包装游标链"]
DL2 --> DL3["RootCursorWrapper<br/>补 rootId 列"]
DL3 --> DL4["FilteringCursorWrapper<br/>按 mime / 隐藏文件过滤"]
DL4 --> DL5["SortingCursorWrapper<br/>按 SortModel 排序"]
DL5 --> DR["6. DirectoryResult<br/>(AutoCloseable)"]
DR -->|"7. deliverResult"| DF
DF -->|"8. Model.update(result)"| MDL
MDL -->|"9. notifyUpdateListeners"| ADP
ADP -->|"10. notifyDataSetChanged"| RV
游标链(Cursor Wrapper Chain)是本架构最具特色的设计:不改动 Provider 返回的原始 Cursor,而是逐层包装,每层只负责一件事:
| Wrapper | 职责 |
|---|---|
RootCursorWrapper | 为跨 Root 查询结果补上 rootId 列,使多个 Provider 的结果可以合并成一张表 |
FilteringCursorWrapper | 按 State(acceptMimes / showHiddenFiles / openableOnly)过滤 |
SortingCursorWrapper | 按 SortModel 的维度与方向排序(内存排序) |
DirectoryResult 实现 AutoCloseable,持有游标与异常,close() 时统一释放 —— 避免游标泄漏(Android 上最常见的资源问题)。
Model 的职责不是"存数据",而是建立 Model ID ↔ 游标位置的映射:
Map<String, Integer> mPositions; // ModelID → cursor position
Set<String> mFileNames; // 用于重名检测(重命名/新建时)
Cursor mCursor;
String[] mIds; // position → ModelID
Model ID 由 ModelId 生成,格式为 userId|authority|docId(如 0|com.android.externalstorage.documents|primary:Download)—— 天然带用户维度,是跨 profile 场景下唯一稳定的标识。RecyclerView 的选择状态在列表刷新后仍能保持,靠的就是它 —— 这是与 recyclerview-selection 集成的关键。
6.4 动作分发链路
flowchart TB
SRC["事件源"]
SRC --> S1["菜单 / Toolbar 点击"]
SRC --> S2["键盘快捷键<br/>ActivityInputHandler / KeyInputHandler"]
SRC --> S3["拖拽 Drop"]
SRC --> S4["ActionMode 顶部栏"]
SRC --> S5["Fragment 内部回调<br/>双击打开、长按选择"]
S1 & S2 & S3 & S4 & S5 --> AA["AbstractActionHandler(抽象动作总线)<br/>openContainerDocument / deleteSelectedDocuments<br/>pasteIntoFolder / refreshDocument / loadRoot ..."]
AA --> SUB{"getInjector().actions<br/>是哪一套实现?"}
SUB -->|"FilesActivity"| FH["files/ActionHandler<br/>580 行 · 完整文件管理动作"]
SUB -->|"PickActivity"| PH["picker/ActionHandler<br/>471 行 · 仅选择相关动作<br/>(delete/move 等被禁)"]
FH & PH --> DATA["数据层<br/>DocumentsAccess / FileOperations / DocumentClipper"]
DATA --> BG["后台任务 / Provider"]
AbstractActionHandler 的 1000 行里,大部分动作是"共享实现"(刷新、导航、打开、弹窗),子类只需覆写差异化的那几个(比如 picker 里"删除"要禁用)。ActionHandler 接口(根包)定义了动作契约,ActionModeAddons 定义了 ActionMode 需要的额外能力。
6.5 文件操作链路(跨进程)
复制/移动/删除/压缩/解压不在 UI 进程执行,而是走独立进程的 FileOperationService:
sequenceDiagram
participant AH as ActionHandler
participant FO as FileOperations
participant FOS as FileOperationService<br/>(:com.android.documentsui.services)
participant JOB as Job<br/>(CopyJob/MoveJob/DeleteJob/CompressJob)
participant P as 源/目标 DocumentsProvider
participant UI as OperationProgressDialog
AH->>FO: deleteSelectedDocuments(...) / pasteIntoFolder(...)
FO->>FOS: FileOperations.start(context, FileOperation, callback)<br/>→ startService(Intent)
Note over FO,FOS: FileOperation 实现 Parcelable<br/>携带 UrisSupplier / 目标目录 / 操作类型
FOS->>JOB: 构造对应 Job
JOB->>JOB: ResolvedResourcesJob<br/>解析 URI → DocumentInfo
JOB->>P: openDocument / createDocument / deleteDocument
loop 逐个文件
JOB-->>FOS: onProgress(Job)
FOS-->>UI: 进度通知(通知栏 + 对话框)
end
JOB-->>FOS: onFinished(Job)
FOS->>FOS: 停止服务 / 更新通知
| 类 | 角色 |
|---|---|
FileOperation(abstract, Parcelable) | 操作描述对象,跨进程传递 |
UrisSupplier(abstract, Parcelable) | 待操作 URI 的提供者(小批量直接带,大批量走 ClipStorage 落盘) |
Job(abstract, Runnable) | "工作单元 + 进度更新工厂",同时是 Job.Listener 的驱动对象 |
ResolvedResourcesJob | 中间层:把所有 URI 解析成 mResolvedDocs |
CopyJob → MoveJob / CompressJob | 复制语义的三种变体 |
DeleteJob | 删除 |
FileOperationService | Service + 通知栏进度 + 取消入口 |
为什么必须独立进程:
ClipStorage需要在读剪贴板内容前确认"巨量剪贴板(jumbo clip)"已写盘完成,用FileLock跨进程等待。Manifest 里对此有明确注释。
6.6 压缩包浏览链路(ArchivesProvider)
这是本应用唯一的 Provider 实现,把压缩包伪装成目录树:
flowchart LR
ZIP["/sdcard/foo.zip<br/>content://.../document/xxx"]
AP["ArchivesProvider<br/>authority: com.android.documentsui.archives"]
REG["ArchiveRegistry<br/>按后缀选择实现"]
RA["ReadableArchive / WriteableArchive"]
AH["ArchiveHandle<br/>ZipFile / SevenZFile / ArchiveInputStream"]
PROXY["Proxy<br/>ProxyFileDescriptorCallback"]
META["MetadataReader<br/>读文件元数据"]
FRAG["DirectoryFragment<br/>(当作普通目录渲染)"]
ZIP -->|"DOCUMENTS_PROVIDER"| AP
AP --> REG --> RA --> AH
AP --> PROXY
AP --> META
AP -->|"archive:// 伪 URI"| FRAG
| 类 | 职责 |
|---|---|
ArchivesProvider | DocumentsProvider 实现:queryRoots / queryDocument / queryChildDocuments / openDocument / openDocumentThumbnail |
ArchiveRegistry | 注册表:决定某文件用哪种 ArchiveHandle(zip / 7z / tar+压缩) |
ArchiveHandle<T> | 统一封装 ZipFile / SevenZFile / ArchiveInputStream 的差异 |
Proxy | ProxyFileDescriptorCallback:让压缩包内文件可以像普通文件一样被 openFileDescriptor 打开(供媒体播放器等使用) |
MetadataReader | 从压缩条目读尺寸/时间等元数据(压缩包内文件没有真实 stat) |
6.7 跨用户 / 跨 Profile 链路
车机与手机上"工作资料(Work Profile)"是同一个机制,架构上体现为一切按 UserId 分桶:
| 组件 | 作用 |
|---|---|
UserId(base) | UserHandle 的不可变包装,作 Map 键 |
UserIdManager | 决定当前在哪些用户上工作(getUserIds()) |
ProfileTabs / ProfileTabsAddons / ProfileTabsController | 顶部 profile 切换 Tab 的显示与联动 |
State.canShareAcrossProfile / supportsCrossProfile | 意图是否允许跨 profile |
CrossProfileException 家族 | QuietModeException(工作资料已暂停)/ NoPermissionException |
CrossProfileUtils(util) | 判断应用是否跨 profile 可见 |
requestQuietModeDisabled | 主动请求解除工作资料暂停状态 |
AbstractActionHandler.loadCrossProfileRoot | 切到另一 profile 的 Root |
选择器场景下,跨 profile 需要 IntentForwarderActivity(系统侧)转发 Intent —— 这是为什么 com.android.documentsui 必须平台签名 + priv-app 预置。
6.8 搜索链路
flowchart TB
SV["SearchViewManager<br/>(688 行,搜索状态机)"]
SV --> UI1["SearchView 展开/收起"]
SV --> UI2["SearchChipViewManager<br/>筛选 chip(图片/视频/文档…)"]
SV --> UI3["SearchHistoryManager<br/>搜索历史持久化"]
SV -->|"onSearchChanged"| BA["BaseActivity<br/>loadDocumentsForCurrentStack"]
BA --> L{"搜索模式?"}
L -->|"有 query"| GS["GlobalSearchLoader<br/>extends MultiRootDocumentsLoader"]
L -->|"无 query"| RC["RecentsLoader"]
GS & RC --> MR["MultiRootDocumentsLoader<br/>并发查多个 Root + RootCursorWrapper 合并"]
MR --> DL["DirectoryLoader 同一套下游<br/>Model → Adapter → RecyclerView"]
CI["CommandInterceptor<br/>调试命令拦截(默认关闭)"]
MultiRootDocumentsLoader 的难点:把多个 Provider 的结果合并成一张按时间排序的表 —— 靠 RootCursorWrapper 补 rootId 列 + 统一时间列,再交给同一个 Model。
6.9 剪贴板与拖拽链路
| 链路 | 组件 | 说明 |
|---|---|---|
| 复制/剪切 | DocumentClipper.getClipDataForDocuments → ClipStorage | 判据是 Shared.MAX_DOCS_IN_INTENT:不超过则走 createStandardClipData(URI 直接放进 ClipData.Item);超过则走 createJumboClipData(jumbo clip) —— 整个 URI 列表写盘到 ClipStorage,剪贴板里只保留前 MAX_DOCS_IN_INTENT 个 Item 作为 MIME 类型提示 |
| 粘贴 | AbstractActionHandler.pasteIntoFolder → DocumentClipper.copyFromClipData → FileOperations | 从剪贴板还原 URI 列表(标准 clip 直接读,jumbo clip 从盘上读)再发起复制 |
| 拖拽 | DragAndDropManager(Ctrl 键态、默认操作 move/copy 判定)→ DragStartListener / DirectoryDragListener / ItemDragListener → AbstractDragHost → DropBadgeView / DragOverTextView | 车机/桌面模式下的关键交互 |
ClipStorage 用同一个文件 + FileLock 在 UI 进程与 FileOperationService 进程间传递数据 —— 这正是它必须独立进程的原因(Manifest 中有明确注释)。
6.10 选择链路
flowchart LR
G["手势:长按 / 单击"]
G --> IPH["InputHandlers / SharedInputHandler"]
IPH --> DSL["DocsSelectionHelper<br/>extends SelectionTracker"]
DSL --> DSP["DocsSelectionPredicate<br/>(哪些 item 可选)"]
DSL --> DIDL["DocsItemDetailsLookup<br/>(手势位置 → item 详情)"]
DSL --> DSIP["DocsStableIdProvider / ModelId<br/>(刷新后保持选择)"]
DSL --> AMC["ActionModeController<br/>SelectionObserver"]
AMC --> AM["ActionMode 顶部栏<br/>计数 + 可用动作"]
DSL --> SM["SelectionMetadata<br/>聚合选中项元数据(决定菜单可用性)"]
SM --> MM["MenuManager"]
DSL -.->|"选择中禁止刷新"| CL["ContentLock + LockingContentObserver"]
ContentLock 的设计很关键:选择进行中时,如果目录刷新会导致选中项失效,因此把刷新挂起,等选择结束再放行。
6.11 缩略图与图标链路
flowchart LR
VH["DocumentHolder<br/>(按类型选 Holder)"]
VH --> IH["dirlist/IconHelper<br/>load(uri, userId, mimeType, docFlags, docIcon...)"]
IH --> JUDGE{"MimeTypes.mimeMatches<br/>(VISUAL_MIMES, mimeType)?"}
JUDGE -->|"是(图片/视频)"| TC["ThumbnailCache<br/>LRU · 按 Uri+UserId+尺寸索引"]
JUDGE -->|"否"| MIME["IconUtils.loadMimeIcon<br/>按 MIME 给矢量图标"]
TC --> TL["ThumbnailLoader<br/>AsyncTask · Preemptable"]
TC --> RES["Result 四态<br/>MISS / HIT_EXACT<br/>HIT_SMALLER / HIT_LARGER"]
RES -->|"小图放大或大图缩小"| ANIM["淡入替换 MIME 图标"]
TL --> PBM["DocumentsContract.getDocumentThumbnail<br/>(底层即 DocumentsProvider.openDocumentThumbnail)"]
VH --> TT["GridItemThumbnail<br/>强制正方形"]
ThumbnailCache 不只是"存图",它的 Result 有四种命中状态:精确命中、命中更小的尺寸(放大用)、命中更大的尺寸(缩小用)。这样在不同视图模式/缩放下,同一张缩略图可以被复用而不必重新向 Provider 请求 —— 这是列表滚动流畅的关键。
ThumbnailLoader 实现 ProviderExecutor.Preemptable 接口:列表快速滚动时可以抢占已排队的加载任务,避免为已经滚出屏幕的项浪费 IO。此外它还负责把加载结果回填进 ThumbnailCache。
6.12 状态与持久化
| 状态 | 存储 | 生命周期 |
|---|---|---|
State(ACTION_* / 过滤 / 排序 / profile 开关) | Parcelable + Bundle | Activity 级,配置变更/进程重建可恢复 |
DocumentStack(当前路径栈) | Durable + Parcelable,序列化到 Shared.EXTRA_STACK | 跨 Activity 传递 |
PickResult(选择结果累积) | Parcelable | 选择器 Activity 级 |
| 上次访问路径 | picker/LastAccessedProvider(com.android.documentsui.lastAccessed) | 跨进程、跨启动(按调用方包名记录) |
| 选择次数 | picker/PickCountRecordProvider(com.android.documentsui.pickCountRecord) | 跨启动(用于"是否显示最近"等策略) |
| 用户偏好(视图模式、排序、显示隐藏文件) | LocalPreferences + SharedPreferences,PreferencesMonitor监听变化 | 永久 |
| 搜索历史 | SearchHistoryManager | 永久 |
| 剪贴板 URI 列表 | ClipStorage 落盘文件 | 直到被覆盖/清空 |
| 偏好备份 | BackupAgent → PrefsBackupHelper | 系统备份 |
| 主题 | ThemeOverlayManager(RRO overlay 包) | 永久(运行时切换) |
Durable / DurableUtils:AOSP 自研的轻量序列化协议 —— 写版本号 + 内容,读时校验版本,不兼容就丢弃(DurableUtils.readFromParcel 返回 null 时上层回退到默认值)。这比 Parcelable 的 BadParcelableException 更可控。
6.13 其余横切链路
| 能力 | 链路 |
|---|---|
| 排序 | SortModel(列 + 方向 + 是否可排序)→ SortController → TableHeaderController / HeaderCell(表头点击)→ SortingCursorWrapper(实际排序)→ 持久化到偏好 |
| 主题 | BaseActivity.getTheme().applyStyle(DocumentsDefaultTheme) + ThemeOverlayManager(按版本校验 RRO overlay,不匹配则重置为默认,防止资源找不到) |
| 埋点 | Metrics + MetricConsts + DocumentsStatsLog(本项目为桩)→ DevicePolicyMetricConsts / ScopedAccessMetrics 分领域常量 |
| 详情页 | InspectorActivity(独立于 BaseActivity)→ InspectorController → MetadataLoader(AsyncTaskLoader<Bundle>)→ DetailsView / MediaView / DebugView / HeaderView;GpsCoordinatesTextClassifier 识别经纬度并提供"打开地图" |
| 限定目录访问 | ScopedAccessActivity 响应 android.os.storage.action.OPEN_EXTERNAL_DIRECTORY(StorageVolume.createAccessIntent),配合 ScopedAccessMetrics |
| 动态快捷方式 | ShortcutsUpdater(监听 Root 变化,更新 launcher shortcuts,对应 res/xml/shortcuts.xml) |
| 线程异常 | ThreadHelper(Android 后台线程异常会被静默吞掉,这里统一捕获上报) |
7. 组件清单
7.1 Activity
| 组件 | 类 | 关键属性 / Intent | 说明 |
|---|---|---|---|
| activity-alias | .LauncherActivity | MAIN + LAUNCHER + APP_FILES,带 android.app.shortcuts | 桌面入口(Nougat 起保留的别名) |
| activity | .files.LauncherActivity | Theme.NoDisplay | task 聚合跳板,复用已有 task |
| activity | .files.FilesActivity | documentLaunchMode="intoExisting";VIEW + vnd.android.document/root | /directory | 文件管理器主体 |
| activity-alias | .ViewDownloadsActivity | VIEW_DOWNLOADS,enabled=@bool/handle_view_downloads_intent | 下载入口(可被厂商 RRO 关闭) |
| activity | .picker.PickActivity | OPEN_DOCUMENT / CREATE_DOCUMENT / GET_CONTENT / OPEN_DOCUMENT_TREE,均 priority=100 | 选择器主体 |
| activity | .ScopedAccessActivity | OPEN_EXTERNAL_DIRECTORY,Theme.Translucent.NoTitleBar | 存储卷限定目录授权 |
| activity | .inspector.InspectorActivity | — | 文件详情 |
| activity | .selection.demo.SelectionDemoActivity | MAIN | 选择框架示例(0629 树已裁,本工程为防 manifest 引用崩溃而补的桩) |
7.2 Provider
| 组件 | authorities | 权限 | 说明 |
|---|---|---|---|
.archives.ArchivesProvider | com.android.documentsui.archives | MANAGE_DOCUMENTS,exported=true,声明 DOCUMENTS_PROVIDER | 本应用唯一自实现的 DocumentsProvider |
.picker.LastAccessedProvider | com.android.documentsui.lastAccessed | 私有 | 记录各调用方上次访问路径 |
.picker.PickCountRecordProvider | com.android.documentsui.pickCountRecord | 私有 | 记录选择次数 |
7.3 Receiver
| 组件 | 监听 | 说明 |
|---|---|---|
.PackageReceiver | PACKAGE_FULLY_REMOVED / PACKAGE_DATA_CLEARED | 清理 LastAccessedProvider 中已卸载包的记录 |
.roots.BootReceiver | BOOT_COMPLETED(enabled=false) | 预热 ProvidersCache(默认关闭,由 RRO 决定是否启用) |
.PreBootReceiver | PRE_BOOT_COMPLETED | 按 RRO 决定组件启用/禁用(handle_view_downloads_intent 等) |
7.4 Service
| 组件 | 进程 | 说明 |
|---|---|---|
.services.FileOperationService | :com.android.documentsui.services(独立进程) | 所有复制/移动/删除/压缩/解压,带通知栏进度 |
7.5 权限
| 权限 | 级别 | 用途 |
|---|---|---|
MANAGE_DOCUMENTS | signature | 唯一的文档中介能力(ArchivesProvider 也用它保护自己) |
INTERACT_ACROSS_USERS | signature | 跨用户/跨 profile 访问 |
MODIFY_QUIET_MODE | signature | 解除工作资料暂停 |
CHANGE_OVERLAY_PACKAGES | signature | ThemeOverlayManager 切换 RRO |
REMOVE_TASKS | — | LauncherActivity 清理残留 task |
FOREGROUND_SERVICE / WAKE_LOCK / START_FOREGROUND_SERVICES_FROM_BACKGROUND | normal | 文件操作前台服务 |
CACHE_CONTENT | signature | 内容缓存 |
RECEIVE_BOOT_COMPLETED | normal | 开机预热 |
QUERY_ALL_PACKAGES | normal | 包可见性(枚举能处理某文档的应用) |
POST_NOTIFICATIONS | runtime | 文件操作通知(Android 13) |
LOG_COMPAT_CHANGE / READ_COMPAT_CHANGE_CONFIG | — | 兼容性变更日志 |
8. 关键设计模式
| 模式 | 落点 | 价值 |
|---|---|---|
| 模板方法 | BaseActivity + FilesActivity / PickActivity | 管理器与选择器共享 95% 代码,差异只在 3 个钩子 |
| 策略 + 工厂 | ActivityConfig 子类、ActionHandler 两套实现、DrawerController.create | 行为特化不污染基类 |
| 轻量 DI | Injector<T> + @ContentScoped | 不引入 Dagger/Hilt,手工装配但职责清晰 |
| 装饰器链 | RootCursorWrapper → FilteringCursorWrapper → SortingCursorWrapper | 每个关注点独立一层,可自由组合顺序 |
| 观察者 | Model.addUpdateListener(EventListener<Update>)、ContentObserver、LocalBroadcastManager、SelectionObserver | 数据层与 UI 层解耦,UI 只订阅不拉取 |
| 命令 + 队列 | Job 家族 + FileOperationService | 长任务可取消、可进度反馈、可跨进程 |
| 代理 | ArchivesProvider + Proxy(ProxyFileDescriptorCallback) | 压缩包内文件对系统呈现为普通文件 |
| 值对象 + 分桶缓存 | UserId / UserAuthority 作 Map 键 | 多用户/工作资料天然隔离,无需分支判断 |
| 带守卫的异步任务 | CheckedTask → PairedTask(绑定 Activity 存活)→ TimeoutTask | 防止异步回调打到已销毁的 Activity(Android 经典崩溃源) |
| 版本化序列化 | Durable / DurableUtils | 数据格式升级时优雅降级,而非抛异常 |
9. 扩展点:如何加功能
9.1 加一个"文件操作"(如重命名)
- 在
services/新增RenameJob extends ResolvedResourcesJob(或复用CopyJob的模式); - 在
FileOperationService的分发处注册该操作类型; - 在
FileOperation/FileOperations加启动入口; - 在
AbstractActionHandler加动作入口,files/ActionHandler实现; MenuManager加菜单项,ActionModeController决定何时可用。
9.2 加一种"存储后端"
通常不需要改 DocumentsUI —— 新写一个 DocumentsProvider 并在它的 manifest 里声明 android.content.action.DOCUMENTS_PROVIDER 即可,ProvidersCache 通过 PackageManager 自动发现。只有在需要特殊展示逻辑时才改 sidebar/RootItemListBuilder 或 ActivityConfig。
9.3 加一个"特化行为"(如 car 模式)
继承 ActivityConfig,覆写 canSelectType / isDocumentEnabled / managedModeEnabled / dragAndDropEnabled,然后在对应 Injector 里替换 —— 这正是 files 与 picker 的差异化方式。
9.4 加一个"入口 Activity"
继承 BaseActivity,实现 getInjector() / refreshDirectory(int) / includeState(State) 三个钩子,再写一份 ActionHandler 子类(可空实现),并在 Manifest 注册。注意同步更新 BaseActivity 里 DrawerController.create 的配置分支。
9.5 换主题 / 关闭某些内置入口(车机常见)
通过 RRO overlay(ThemeOverlayManager + res/values/bools.xml 里的 handle_view_downloads_intent 等开关)实现,无需改代码。PreBootReceiver 会在开机早期按 overlay 结果启用/禁用组件。
10. 移植改造点
本工程与 AOSP 原版的差异只在工程侧,应用逻辑基本保持 1:1:
| 差异点 | AS 工程侧 | AOSP 侧 | 影响 |
|---|---|---|---|
DocumentsStatsLog.java | 手写桩:write() 不上报,atom ID 为占位值 | stats-log-api-gen 生成物 | 仅埋点丢失,功能无影响 |
SdkLevel.java | 源码并入 com/android/modules/utils/build/ | 来自 frameworks/libs/modules-utils(构建依赖) | 无 |
selection/demo/SelectionDemoActivity.java | 最小桩类 | 0629 树已裁该目录,但 manifest 仍引用 | 防运行时崩溃 |
AndroidManifest.xml | 删除 package= / uses-sdk(AGP 8 强制) | 需保留这两个属性 | 回树前须手工恢复 |
ProvidersCache.updateAsync 断言 | 已修正为 3 个 flag(补齐 FLAG_SUPPORTS_SEARCH) | 上游是漏项的过期断言 | ★真 bug,回树时建议一并带过去 |
res/values/mimes.xml | 缺失(0629 树只有语言目录的陈旧翻译) | 完整 | aapt 警告,相关字符串已无引用,无害 |
| 构建方式 | Gradle(assembleDebug) | Soong(Android.bp,sdk_version: "system_current") | 隐藏 API 需靠全量框架 jar(README §3) |
为什么 debug 包要特别注意断言:AGP 8 对 debuggable 变体保留并强制开启 Java 断言($assertionsDisabled 在 DEX 里是常量 false),而量产链路(D8 默认 / R8)会把 assert 整条剥掉。这个差异让一个上游存在多年的过期断言只在移植 debug 包上暴露 —— 详见 README §4.1。
附:推荐阅读路线
按理解成本从低到高,建议顺序:
| 顺序 | 文件 | 为什么先读它 |
|---|---|---|
| 1 | base/State.java | 搞清楚"我是谁"(ACTION_*)和"有什么约束"(过滤条件) |
| 2 | base/DocumentInfo.java + base/DocumentStack.java + base/RootInfo.java | 三个核心值对象,贯穿全工程 |
| 3 | DocumentsApplication.java | 全局单例容器,知道有哪些重对象 |
| 4 | ActivityConfig.java + Injector.java | 理解"特化"与"装配"两条主线 |
| 5 | BaseActivity.java | 启动骨架,串起所有控制器 |
| 6 | AbstractActionHandler.java | 动作总线,看一遍就知道应用能做哪些事 |
| 7 | roots/ProvidersCache.java | 存储后端的来龙去脉 |
| 8 | DirectoryLoader.java → Model.java → dirlist/DirectoryFragment.java | 目录渲染主链路(最关键) |
| 9 | dirlist/ModelBackedDocumentsAdapter.java + DocumentHolder 家族 | 列表如何渲染不同视图类型 |
| 10 | services/FileOperationService.java + services/CopyJob.java | 长任务如何跨进程执行 |
文档基于工程实际源码(258 个 Java 文件 / 约 46,100 行)基于 Android13 源码树核对整理。