Android13 原生 DocumentsUI 应用架构

35 阅读20分钟

DocumentsUI 架构文档

对象:com.android.documentsui(AOSP Android 13 / TP1A) 工程:C:\code\DocumentsUIStudio(Android Studio / Gradle 移植工程) 源码基线:packages\apps\DocumentsUI

本文档描述应用自身架构(分层、模块、运行时骨架、数据流、扩展点)。 移植过程、隐藏 API 编译方案、签名与装机等工程侧内容见 README.md。


目录

  1. 全局视图
  2. 分层架构
  3. 工程与构建架构
  4. 代码结构地图
  5. 运行时骨架
  6. 核心数据流
  7. 组件清单
  8. 关键设计模式
  9. 扩展点:如何加功能
  10. 移植改造点

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[&#34;Intent: OPEN_DOCUMENT<br/>CREATE_DOCUMENT<br/>GET_CONTENT<br/>OPEN_DOCUMENT_TREE&#34;]
        A2[&#34;返回 content:// URI<br/>+ 持久化授权&#34;]
    end

    subgraph DOCSUI[&#34;DocumentsUI(本应用)&#34;]
        P[&#34;PickActivity<br/>选择器入口&#34;]
        F[&#34;FilesActivity<br/>文件管理器&#34;]
        DIR[&#34;DirectoryFragment<br/>目录内容渲染&#34;]
        PC[&#34;ProvidersCache<br/>存储后端缓存&#34;]
        FOS[&#34;FileOperationService<br/>跨进程复制/移动/删除&#34;]
        AR[&#34;ArchivesProvider<br/>压缩包 DocumentsProvider&#34;]
    end

    subgraph FW[&#34;Android Framework&#34;]
        PMS[&#34;PackageManagerService<br/>解析 DOCUMENTS_PROVIDER 接口&#34;]
        DMS[&#34;ActivityTaskManagerService<br/>+ DocumentsContract<br/>(URI 授权的校验与分发)&#34;]
        ACT[&#34;IntentForwarderActivity<br/>跨 profile Intent 转发&#34;]
    end

    subgraph PROV[&#34;各存储后端 DocumentsProvider&#34;]
        P1[&#34;ExternalStorageProvider(本机存储/SD)&#34;]
        P2[&#34;DownloadStorageProvider(下载)&#34;]
        P3[&#34;MediaDocumentsProvider(图片/视频/音频)&#34;]
        P4[&#34;MtpDocumentsProvider(USB)&#34;]
        P5[&#34;私有云盘/第三方 Provider&#34;]
    end

    A1 --> P
    A2 -.-> P
    F --> DIR
    P --> DIR
    DIR --> PC
    PC -->|&#34;queryRoots / queryDocument&#34;| PROV
    PC -->|&#34;PackageManager queryIntentContentProviders&#34;| PMS
    F --> FOS
    FOS --> PROV
    DIR --> AR
    AR -->|&#34;archive:// 伪 URI&#34;| DIR
    P -->|&#34;跨 profile&#34;| ACT
    DMS -.->|&#34;MANAGE_DOCUMENTS 校验&#34;| 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 行
最小 SDK29(Android 10)
语言纯 Java(零 Kotlin),依赖 AndroidX + Guava + commons-compress

2. 分层架构

2.1 分层视图

flowchart TB
    subgraph L1[&#34;① 入口层 Entry&#34;]
        E1[&#34;PickActivity<br/>(选择器)&#34;]
        E2[&#34;FilesActivity<br/>(文件管理器)&#34;]
        E3[&#34;LauncherActivity<br/>(task 聚合跳板)&#34;]
        E4[&#34;ScopedAccessActivity&#34;]
        E5[&#34;InspectorActivity&#34;]
    end

    subgraph L2[&#34;② 宿主基类层 Host&#34;]
        B1[&#34;BaseActivity(967 行)<br/>模板方法:onCreate/onSaveInstanceState<br/>装配所有 Controller&#34;]
        B2[&#34;ActivityConfig<br/>行为特化(可选中/可拖拽/管理模式)&#34;]
        B3[&#34;Injector<br/>运行时依赖容器&#34;]
    end

    subgraph L3[&#34;③ 动作与状态层 Action&#34;]
        A1[&#34;AbstractActionHandler(1000 行)<br/>动作总线&#34;]
        A2[&#34;files/ActionHandler<br/>picker/ActionHandler&#34;]
        A3[&#34;State(Parcelable)<br/>ACTION_* / MODE_* / 过滤条件&#34;]
        A4[&#34;ActionModeController<br/>选择态管理&#34;]
        A5[&#34;MenuManager / DialogController<br/>菜单与对话框&#34;]
    end

    subgraph L4[&#34;④ 数据加载层 Data&#34;]
        D1[&#34;ProvidersCache<br/>Root 缓存(Multimap)&#34;]
        D2[&#34;DirectoryLoader<br/>AsyncTaskLoader&#34;]
        D3[&#34;MultiRootDocumentsLoader<br/>RecentsLoader / GlobalSearchLoader&#34;]
        D4[&#34;Model<br/>cursor ↔ ModelID 映射&#34;]
        D5[&#34;DocumentsAccess<br/>DocumentInfo 同步查询&#34;]
        D6[&#34;RootCursorWrapper / FilteringCursorWrapper<br/>SortingCursorWrapper&#34;]
        D7[&#34;DocumentsApplication<br/>App 级单例容器&#34;]
    end

    subgraph L5[&#34;⑤ 展示层 Presentation&#34;]
        V1[&#34;DirectoryFragment(1500 行)<br/>RecyclerView + 手势 + 拖拽&#34;]
        V2[&#34;RootsFragment / DrawerController<br/>侧栏导航&#34;]
        V3[&#34;DocumentsAdapter 家族<br/>List/Grid/Photo Holder&#34;]
        V4[&#34;SelectionMetadata / DocsSelectionHelper<br/>recyclerview-selection&#34;]
        V5[&#34;SortController / SearchViewManager<br/>排序、搜索&#34;]
        V6[&#34;ThumbnailCache / IconHelper<br/>缩略图与图标&#34;]
    end

    subgraph L6[&#34;⑥ 后台任务层 Background&#34;]
        G1[&#34;FileOperationService<br/>独立进程&#34;]
        G2[&#34;Job 家族<br/>Copy/Move/Delete/Compress&#34;]
        G3[&#34;ProviderExecutor<br/>Provider 线程池&#34;]
        G4[&#34;CheckedTask / PairedTask / TimeoutTask<br/>带校验与生命周期守卫的异步任务&#34;]
    end

    subgraph L7[&#34;⑦ 平台资源层 Platform&#34;]
        R1[&#34;ArchivesProvider<br/>压缩包 DocumentsProvider&#34;]
        R2[&#34;ClipStorage / DocumentClipper<br/>剪贴板&#34;]
        R3[&#34;LocalPreferences / PreferencesMonitor / BackupAgent&#34;]
        R4[&#34;ThemeOverlayManager / DevicePolicyResources&#34;]
        R5[&#34;UserId / UserIdManager / ProfileTabs*<br/>多用户与跨 profile&#34;]
    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 / JDK8.1.2 / 8.2 / 17JDK 21 会让 AGP 8.1.2 的 JdkImageTransform(jlink)失败
compileSdk33对齐 TP1A
minSdk / targetSdk29 / 30对齐 AOSP documentsui_defaults
sourceCompatibility11AOSP 该模块的 javac 级别
android.nonFinalResIdsfalse★AGP 8 默认 true 会让 R.id.x 非常量 → @IntDef/case R.id.* 全挂
android.nonTransitiveRClassfalse保持 AOSP 的 R 类可见性语义
versionCode33★对齐设备预置版,低于它会被 INSTALL_FAILED_VERSION_DOWNGRADE 拒绝
signingConfigsplatform(debug + release)★与 /system/priv-app/DocumentsUI 同签名才能覆盖安装并拿到签名权限
lint.abortOnErrorfalseAOSP 源码里大量 deprecated/隐藏用法是预期行为
isMinifyEnabledfalse(release 保留 proguard.flags)需要瘦身时再打开

3.3 隐藏 API 编译方案(架构级影响)

DocumentsUI 在 AOSP 中以 sdk_version: "system_current" 编译,源码合法使用了 @SystemApi / @hide 符号。移植到 Gradle 后,标准 compileSdk 的 android.jar 里没有这些符号。解法:

flowchart LR
    subgraph CP[&#34;javac 编译 classpath(顺序即优先级)&#34;]
        J1[&#34;framework-13-android-all.jar<br/>178MB · 66510 条目 · 含隐藏 API&#34;] --> J2[&#34;platforms/android-33/android.jar<br/>仅公开 API&#34;]
        J2 --> J3[&#34;androidx / material / guava / commons-compress ...&#34;]
    end
    J1 -.->|&#34;android.* 类型命中此份&#34;| OUT[&#34;编译产物<br/>class 文件&#34;]
    J2 -.->|&#34;java.* 类型仍在标准库&#34;| OUT
    J1 -.->|&#34;❌ 只参与编译&#34;| APK[&#34;app-debug.apk<br/>不含框架类&#34;]

三个必须遵守的点(详见 README §3):

  1. 前置到 classpath,不是 bootstrapClasspath —— AGP 8 把 android.jar 放在常规编译 classpath 上,只设 bootclasspath 等于没设。
  2. 在 doFirst {} / gradle.projectsEvaluated {} 里改写 —— AGP 会在配置阶段覆盖顶层 configureEach。
  3. 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.1androidx.appcompat_appcompatAppCompatActivity、主题
com.google.android.material:material:1.9.0com.google.android.material_materialToolbar、TabLayout、Snackbar
androidx.recyclerview:recyclerview:1.3.0androidx.recyclerview_recyclerview目录列表
androidx.recyclerview:recyclerview-selection:1.1.0androidx.recyclerview_recyclerview-selectionSelectionTracker 选择框架
androidx.transition:transition:1.4.1androidx.transition_transition进入/退出转场
androidx.legacy:*androidx.legacy_legacy-support-*旧 API 兼容
androidx.core / annotation / loader / localbroadcastmanager / swiperefreshlayout上述库的传递依赖(此处显式声明)核心工具、AsyncTaskLoader、应用内广播、下拉刷新
com.google.guava:guava:31.1-androidexternal/guavaMultimap、Lists 等集合增强
org.apache.commons:commons-compress:1.21external/apache-commons-compresszip/7z/tar 解析
com.google.code.findbugs:jsr305:3.0.2external/jsr305@Nullable / @GuardedBy
—(源码并入)modules-utils-build_systemSdkLevel
—(桩代码)docsui-statsd(生成物)DocumentsStatsLog

4. 代码结构地图

4.1 包职责一览

包文件数职责代表类
(根) documentsui68应用骨架、跨包共享的控制器与工具DocumentsApplication、BaseActivity、Injector、AbstractActionHandler、Model、DirectoryLoader、NavigationViewManager
dirlist37目录内容渲染核心:RecyclerView、Adapter、ViewHolder、手势、拖拽、选择DirectoryFragment、DocumentsAdapter、DocumentHolder、DocsSelectionHelper
base31无 UI 依赖的基础类型与工具(可被任意层引用)State、DocumentInfo、DocumentStack、RootInfo、UserId、Durable、Features
inspector18文件详情页InspectorActivity、InspectorController、DetailsView、MediaView、MetadataLoader
picker16选择器特化:保存/确认/最近访问记录PickActivity、PickFragment、SaveFragment、LastAccessedProvider
sidebar15侧栏导航项与列表RootsFragment、RootsAdapter、Item/RootItem/AppItem、UserItemsCombiner
archives11压缩包 → 虚拟文档树(本应用唯一自实现的 Provider)ArchivesProvider、ReadableArchive、WriteableArchive、Proxy
services10后台文件操作(独立进程)FileOperationService、Job、CopyJob、DeleteJob、CompressJob
roots8存储后端(Root)的发现、缓存、加载ProvidersCache、RootsLoader、RootCursorWrapper、BootReceiver
files8文件管理器特化FilesActivity、ActionHandler、LauncherActivity、Config
ui7通用 UI 组件与对话框DialogController、MessageBuilder、Snackbars、OperationProgressDialog
sorting7排序模型与表头交互SortModel、SortDimension、SortController、SortingCursorWrapper
queries6搜索 UI 与搜索数据源SearchViewManager、SearchFragment、SearchChipViewManager、SearchHistoryManager
clipping6剪贴板(复制/剪切)持久化ClipStorage、DocumentClipper、UrisSupplier
prefs4本地偏好与备份LocalPreferences、PreferencesMonitor、BackupAgent
util3跨 profile / 格式化 / 版本工具CrossProfileUtils、FormatUtils、VersionUtils
theme1运行时主题 RRO 管理ThemeOverlayManager
selection1选择框架示例(桩)selection/demo/SelectionDemoActivity
modules/utils/build1并入的 SdkLevelSdkLevel

4.2 依赖关系图

flowchart TD
    ROOT[&#34;(根) documentsui<br/>骨架与控制器&#34;]
    BASE[&#34;base<br/>基础类型(无 UI 依赖)&#34;]
    DIRLIST[&#34;dirlist<br/>列表渲染&#34;]
    FILES[&#34;files / picker<br/>两个特化入口&#34;]
    SIDEBAR[&#34;sidebar<br/>侧栏&#34;]
    ROOTS[&#34;roots<br/>Root 缓存&#34;]
    SERVICES[&#34;services<br/>后台任务&#34;]
    QUERIES[&#34;queries / sorting / clipping / ui / util / theme / prefs<br/>横切能力&#34;]
    ARCHIVES[&#34;archives<br/>压缩包 Provider&#34;]
    INSPECTOR[&#34;inspector<br/>详情页&#34;]

    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 -.->|&#34;伪 URI 交给 DirectoryFragment 渲染&#34;| DIRLIST
    INSPECTOR -.->|&#34;复用 DocumentsAccess&#34;| ROOT

关键观察:base 包零依赖其他业务包(只有 java.* / android.* / androidx.*),是整个工程的稳定内核;根包是事实上的"共享控制器层",被所有人依赖 —— 这也是为什么它有 68 个文件却难以再拆分。


5. 运行时骨架

5.1 Application 层 —— 全局单例容器

DocumentsApplication 是唯一的进程级容器,onCreate 时构造所有跨 Activity 存活的重对象:

flowchart TB
    APP[&#34;DocumentsApplication.onCreate()&#34;]
    APP --> C1[&#34;ProvidersCache<br/>(Root 缓存 + 变更观察者)&#34;]
    APP --> C2[&#34;ThumbnailCache<br/>(LRU,按 Uri+尺寸索引)&#34;]
    APP --> C3[&#34;ClipStorage<br/>(剪贴板 URI 落盘)&#34;]
    APP --> C4[&#34;DocumentClipper<br/>(RuntimeDocumentClipper)&#34;]
    APP --> C5[&#34;DragAndDropManager<br/>(Ctrl 键态 + 默认操作判定)&#34;]
    APP --> C6[&#34;UserIdManager<br/>(在哪些 profile 上工作)&#34;]
    APP --> C7[&#34;FileTypeMap<br/>(MIME → 友好类型名)&#34;]

    APP --> R1[&#34;注册 PACKAGE_ADDED/CHANGED/REMOVED/DATA_CLEARED<br/>→ 刷新 Provider 缓存&#34;]
    APP --> R2[&#34;注册 MANAGED_PROFILE_ADDED/REMOVED/UNLOCKED/UNAVAILABLE<br/>→ 刷新跨 profile 状态&#34;]
    APP --> L1[&#34;LocalBroadcastManager<br/>(应用内事件总线)&#34;]

    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> 是手工装配的依赖容器,不走注解处理器:

字段生命周期说明
featuresApp来自 config.xml 的功能开关(Features)
configAppActivityConfig 子类,行为特化
messagesAppMessageBuilder,统一文案
fileTypeLookupAppFileTypeMap,MIME → 类型名
shortcutsUpdaterApp动态快捷方式更新
menuManagerActivity菜单构造(files / picker 两套)
dialogsActivity对话框工厂
searchManagerActivity搜索状态机
appsRowManagerActivity搜索页"应用"横向条
pickResultActivity选择结果累积(仅 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侧栏 DrawerRoot 列表(RootsAdapter)
PickFragment / SaveFragmentPick 底部栏选择确认条 / 文件名编辑
SearchFragmentFiles搜索态内容
对话框 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[&#34;触发源&#34;]
        T1[&#34;Application.onCreate<br/>updateAsync(forceRefreshAll)&#34;]
        T2[&#34;包变更广播<br/>PACKAGE_ADDED/REMOVED&#34;]
        T3[&#34;Profile 变更广播&#34;]
        T4[&#34;ContentObserver<br/>RootsChangedObserver(每用户一个)&#34;]
        T5[&#34;BootReceiver / PreBootReceiver&#34;]
        T6[&#34;RootsLoader(侧栏加载)&#34;]
    end

    subgraph CORE[&#34;核心&#34;]
        U[&#34;updateAsync / updatePackageAsync / updateAuthorityAsync&#34;]
        SEM[&#34;Semaphore(1)<br/>保证同一时刻只有一次全量更新&#34;]
        MPT[&#34;MultiProviderUpdateTask&#34;]
        LOAD[&#34;loadRootsForAuthority()&#34;]
        ROOTS[(&#34;mRoots<br/>Multimap&#34;)]
        REC[(&#34;mRecentsRoots<br/>Map&#34;)]
        STOP[&#34;mStoppedAuthorities<br/>(Provider 已停用)&#34;]
        LATCH[&#34;mFirstLoad CountDownLatch<br/>FIRST_LOAD_TIMEOUT_MS = 5000&#34;]
    end

    subgraph QOUT[&#34;查询出口&#34;]
        Q1[&#34;getRootsBlocking()&#34;]
        Q2[&#34;getMatchingRootsBlocking(state)&#34;]
        Q3[&#34;getDefaultRootBlocking(state)&#34;]
        Q4[&#34;getRootOneshot / getRootBlocking(userId, authority, rootId)&#34;]
        Q5[&#34;getRecentsRoot(userId) / isRecentsRoot(root)&#34;]
        Q6[&#34;getApplicationName(userId, authority)&#34;]
    end

    T1 & T2 & T3 & T4 & T5 & T6 --> U
    U --> SEM --> MPT --> LOAD
    LOAD -->|&#34;queryRoots&#34;| ROOTS
    LOAD -->|&#34;已停用&#34;| 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[&#34;UI 线程&#34;]
        DF[&#34;DirectoryFragment&#34;]
        MDL[&#34;Model&#34;]
        ADP[&#34;ModelBackedDocumentsAdapter&#34;]
        RV[&#34;RecyclerView&#34;]
    end

    subgraph BGH[&#34;后台线程&#34;]
        DL[&#34;DirectoryLoader<br/>AsyncTaskLoader&#34;]
        PE[&#34;ProviderExecutor<br/>(按 authority 串行)&#34;]
    end

    subgraph SYSH[&#34;系统&#34;]
        P[&#34;DocumentsProvider&#34;]
    end

    DF -->|&#34;1. restartLoader(stack, mime, state)&#34;| DL
    DL -->|&#34;2. getExecutor()&#34;| PE
    DL -->|&#34;3. queryChildDocuments&#34;| P
    P -->|&#34;4. Cursor&#34;| DL
    DL --> DL2[&#34;5. 包装游标链&#34;]
    DL2 --> DL3[&#34;RootCursorWrapper<br/>补 rootId 列&#34;]
    DL3 --> DL4[&#34;FilteringCursorWrapper<br/>按 mime / 隐藏文件过滤&#34;]
    DL4 --> DL5[&#34;SortingCursorWrapper<br/>按 SortModel 排序&#34;]
    DL5 --> DR[&#34;6. DirectoryResult<br/>(AutoCloseable)&#34;]
    DR -->|&#34;7. deliverResult&#34;| DF
    DF -->|&#34;8. Model.update(result)&#34;| MDL
    MDL -->|&#34;9. notifyUpdateListeners&#34;| ADP
    ADP -->|&#34;10. notifyDataSetChanged&#34;| 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[&#34;事件源&#34;]
    SRC --> S1[&#34;菜单 / Toolbar 点击&#34;]
    SRC --> S2[&#34;键盘快捷键<br/>ActivityInputHandler / KeyInputHandler&#34;]
    SRC --> S3[&#34;拖拽 Drop&#34;]
    SRC --> S4[&#34;ActionMode 顶部栏&#34;]
    SRC --> S5[&#34;Fragment 内部回调<br/>双击打开、长按选择&#34;]

    S1 & S2 & S3 & S4 & S5 --> AA[&#34;AbstractActionHandler(抽象动作总线)<br/>openContainerDocument / deleteSelectedDocuments<br/>pasteIntoFolder / refreshDocument / loadRoot ...&#34;]
    AA --> SUB{&#34;getInjector().actions<br/>是哪一套实现?&#34;}
    SUB -->|&#34;FilesActivity&#34;| FH[&#34;files/ActionHandler<br/>580 行 · 完整文件管理动作&#34;]
    SUB -->|&#34;PickActivity&#34;| PH[&#34;picker/ActionHandler<br/>471 行 · 仅选择相关动作<br/>(delete/move 等被禁)&#34;]
    FH & PH --> DATA[&#34;数据层<br/>DocumentsAccess / FileOperations / DocumentClipper&#34;]
    DATA --> BG[&#34;后台任务 / Provider&#34;]

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删除
FileOperationServiceService + 通知栏进度 + 取消入口

为什么必须独立进程:ClipStorage 需要在读剪贴板内容前确认"巨量剪贴板(jumbo clip)"已写盘完成,用 FileLock 跨进程等待。Manifest 里对此有明确注释。

6.6 压缩包浏览链路(ArchivesProvider)

这是本应用唯一的 Provider 实现,把压缩包伪装成目录树:

flowchart LR
    ZIP[&#34;/sdcard/foo.zip<br/>content://.../document/xxx&#34;]
    AP[&#34;ArchivesProvider<br/>authority: com.android.documentsui.archives&#34;]
    REG[&#34;ArchiveRegistry<br/>按后缀选择实现&#34;]
    RA[&#34;ReadableArchive / WriteableArchive&#34;]
    AH[&#34;ArchiveHandle<br/>ZipFile / SevenZFile / ArchiveInputStream&#34;]
    PROXY[&#34;Proxy<br/>ProxyFileDescriptorCallback&#34;]
    META[&#34;MetadataReader<br/>读文件元数据&#34;]
    FRAG[&#34;DirectoryFragment<br/>(当作普通目录渲染)&#34;]

    ZIP -->|&#34;DOCUMENTS_PROVIDER&#34;| AP
    AP --> REG --> RA --> AH
    AP --> PROXY
    AP --> META
    AP -->|&#34;archive:// 伪 URI&#34;| FRAG
类职责
ArchivesProviderDocumentsProvider 实现:queryRoots / queryDocument / queryChildDocuments / openDocument / openDocumentThumbnail
ArchiveRegistry注册表:决定某文件用哪种 ArchiveHandle(zip / 7z / tar+压缩)
ArchiveHandle<T>统一封装 ZipFile / SevenZFile / ArchiveInputStream 的差异
ProxyProxyFileDescriptorCallback:让压缩包内文件可以像普通文件一样被 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[&#34;SearchViewManager<br/>(688 行,搜索状态机)&#34;]
    SV --> UI1[&#34;SearchView 展开/收起&#34;]
    SV --> UI2[&#34;SearchChipViewManager<br/>筛选 chip(图片/视频/文档…)&#34;]
    SV --> UI3[&#34;SearchHistoryManager<br/>搜索历史持久化&#34;]
    SV -->|&#34;onSearchChanged&#34;| BA[&#34;BaseActivity<br/>loadDocumentsForCurrentStack&#34;]
    BA --> L{&#34;搜索模式?&#34;}
    L -->|&#34;有 query&#34;| GS[&#34;GlobalSearchLoader<br/>extends MultiRootDocumentsLoader&#34;]
    L -->|&#34;无 query&#34;| RC[&#34;RecentsLoader&#34;]
    GS & RC --> MR[&#34;MultiRootDocumentsLoader<br/>并发查多个 Root + RootCursorWrapper 合并&#34;]
    MR --> DL[&#34;DirectoryLoader 同一套下游<br/>Model → Adapter → RecyclerView&#34;]
    CI[&#34;CommandInterceptor<br/>调试命令拦截(默认关闭)&#34;]

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[&#34;手势:长按 / 单击&#34;]
    G --> IPH[&#34;InputHandlers / SharedInputHandler&#34;]
    IPH --> DSL[&#34;DocsSelectionHelper<br/>extends SelectionTracker&#34;]
    DSL --> DSP[&#34;DocsSelectionPredicate<br/>(哪些 item 可选)&#34;]
    DSL --> DIDL[&#34;DocsItemDetailsLookup<br/>(手势位置 → item 详情)&#34;]
    DSL --> DSIP[&#34;DocsStableIdProvider / ModelId<br/>(刷新后保持选择)&#34;]
    DSL --> AMC[&#34;ActionModeController<br/>SelectionObserver&#34;]
    AMC --> AM[&#34;ActionMode 顶部栏<br/>计数 + 可用动作&#34;]
    DSL --> SM[&#34;SelectionMetadata<br/>聚合选中项元数据(决定菜单可用性)&#34;]
    SM --> MM[&#34;MenuManager&#34;]
    DSL -.->|&#34;选择中禁止刷新&#34;| CL[&#34;ContentLock + LockingContentObserver&#34;]

ContentLock 的设计很关键:选择进行中时,如果目录刷新会导致选中项失效,因此把刷新挂起,等选择结束再放行。

6.11 缩略图与图标链路

flowchart LR
    VH[&#34;DocumentHolder<br/>(按类型选 Holder)&#34;]
    VH --> IH[&#34;dirlist/IconHelper<br/>load(uri, userId, mimeType, docFlags, docIcon...)&#34;]
    IH --> JUDGE{&#34;MimeTypes.mimeMatches<br/>(VISUAL_MIMES, mimeType)?&#34;}
    JUDGE -->|&#34;是(图片/视频)&#34;| TC[&#34;ThumbnailCache<br/>LRU · 按 Uri+UserId+尺寸索引&#34;]
    JUDGE -->|&#34;否&#34;| MIME[&#34;IconUtils.loadMimeIcon<br/>按 MIME 给矢量图标&#34;]
    TC --> TL[&#34;ThumbnailLoader<br/>AsyncTask · Preemptable&#34;]
    TC --> RES[&#34;Result 四态<br/>MISS / HIT_EXACT<br/>HIT_SMALLER / HIT_LARGER&#34;]
    RES -->|&#34;小图放大或大图缩小&#34;| ANIM[&#34;淡入替换 MIME 图标&#34;]
    TL --> PBM[&#34;DocumentsContract.getDocumentThumbnail<br/>(底层即 DocumentsProvider.openDocumentThumbnail)&#34;]
    VH --> TT[&#34;GridItemThumbnail<br/>强制正方形&#34;]

ThumbnailCache 不只是"存图",它的 Result 有四种命中状态:精确命中、命中更小的尺寸(放大用)、命中更大的尺寸(缩小用)。这样在不同视图模式/缩放下,同一张缩略图可以被复用而不必重新向 Provider 请求 —— 这是列表滚动流畅的关键。

ThumbnailLoader 实现 ProviderExecutor.Preemptable 接口:列表快速滚动时可以抢占已排队的加载任务,避免为已经滚出屏幕的项浪费 IO。此外它还负责把加载结果回填进 ThumbnailCache。

6.12 状态与持久化

状态存储生命周期
State(ACTION_* / 过滤 / 排序 / profile 开关)Parcelable + BundleActivity 级,配置变更/进程重建可恢复
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.LauncherActivityMAIN + LAUNCHER + APP_FILES,带 android.app.shortcuts桌面入口(Nougat 起保留的别名)
activity.files.LauncherActivityTheme.NoDisplaytask 聚合跳板,复用已有 task
activity.files.FilesActivitydocumentLaunchMode="intoExisting";VIEW + vnd.android.document/root | /directory文件管理器主体
activity-alias.ViewDownloadsActivityVIEW_DOWNLOADS,enabled=@bool/handle_view_downloads_intent下载入口(可被厂商 RRO 关闭)
activity.picker.PickActivityOPEN_DOCUMENT / CREATE_DOCUMENT / GET_CONTENT / OPEN_DOCUMENT_TREE,均 priority=100选择器主体
activity.ScopedAccessActivityOPEN_EXTERNAL_DIRECTORY,Theme.Translucent.NoTitleBar存储卷限定目录授权
activity.inspector.InspectorActivity—文件详情
activity.selection.demo.SelectionDemoActivityMAIN选择框架示例(0629 树已裁,本工程为防 manifest 引用崩溃而补的桩)

7.2 Provider

组件authorities权限说明
.archives.ArchivesProvidercom.android.documentsui.archivesMANAGE_DOCUMENTS,exported=true,声明 DOCUMENTS_PROVIDER本应用唯一自实现的 DocumentsProvider
.picker.LastAccessedProvidercom.android.documentsui.lastAccessed私有记录各调用方上次访问路径
.picker.PickCountRecordProvidercom.android.documentsui.pickCountRecord私有记录选择次数

7.3 Receiver

组件监听说明
.PackageReceiverPACKAGE_FULLY_REMOVED / PACKAGE_DATA_CLEARED清理 LastAccessedProvider 中已卸载包的记录
.roots.BootReceiverBOOT_COMPLETED(enabled=false)预热 ProvidersCache(默认关闭,由 RRO 决定是否启用)
.PreBootReceiverPRE_BOOT_COMPLETED按 RRO 决定组件启用/禁用(handle_view_downloads_intent 等)

7.4 Service

组件进程说明
.services.FileOperationService:com.android.documentsui.services(独立进程)所有复制/移动/删除/压缩/解压,带通知栏进度

7.5 权限

权限级别用途
MANAGE_DOCUMENTSsignature唯一的文档中介能力(ArchivesProvider 也用它保护自己)
INTERACT_ACROSS_USERSsignature跨用户/跨 profile 访问
MODIFY_QUIET_MODEsignature解除工作资料暂停
CHANGE_OVERLAY_PACKAGESsignatureThemeOverlayManager 切换 RRO
REMOVE_TASKS—LauncherActivity 清理残留 task
FOREGROUND_SERVICE / WAKE_LOCK / START_FOREGROUND_SERVICES_FROM_BACKGROUNDnormal文件操作前台服务
CACHE_CONTENTsignature内容缓存
RECEIVE_BOOT_COMPLETEDnormal开机预热
QUERY_ALL_PACKAGESnormal包可见性(枚举能处理某文档的应用)
POST_NOTIFICATIONSruntime文件操作通知(Android 13)
LOG_COMPAT_CHANGE / READ_COMPAT_CHANGE_CONFIG—兼容性变更日志

8. 关键设计模式

模式落点价值
模板方法BaseActivity + FilesActivity / PickActivity管理器与选择器共享 95% 代码,差异只在 3 个钩子
策略 + 工厂ActivityConfig 子类、ActionHandler 两套实现、DrawerController.create行为特化不污染基类
轻量 DIInjector<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 加一个"文件操作"(如重命名)

  1. 在 services/ 新增 RenameJob extends ResolvedResourcesJob(或复用 CopyJob 的模式);
  2. 在 FileOperationService 的分发处注册该操作类型;
  3. 在 FileOperation / FileOperations 加启动入口;
  4. 在 AbstractActionHandler 加动作入口,files/ActionHandler 实现;
  5. 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。


附:推荐阅读路线

按理解成本从低到高,建议顺序:

顺序文件为什么先读它
1base/State.java搞清楚"我是谁"(ACTION_*)和"有什么约束"(过滤条件)
2base/DocumentInfo.java + base/DocumentStack.java + base/RootInfo.java三个核心值对象,贯穿全工程
3DocumentsApplication.java全局单例容器,知道有哪些重对象
4ActivityConfig.java + Injector.java理解"特化"与"装配"两条主线
5BaseActivity.java启动骨架,串起所有控制器
6AbstractActionHandler.java动作总线,看一遍就知道应用能做哪些事
7roots/ProvidersCache.java存储后端的来龙去脉
8DirectoryLoader.java → Model.java → dirlist/DirectoryFragment.java目录渲染主链路(最关键)
9dirlist/ModelBackedDocumentsAdapter.java + DocumentHolder 家族列表如何渲染不同视图类型
10services/FileOperationService.java + services/CopyJob.java长任务如何跨进程执行

文档基于工程实际源码(258 个 Java 文件 / 约 46,100 行)基于 Android13 源码树核对整理。