DocumentsUIStudio ------ AOSP DocumentsUI 的 Android Studio 移植工程
将源码树 C:\ZQ\0629\packages\apps\DocumentsUI(Android 13 / TP1A,255 个 Java 文件) 移植为标准 Gradle 工程,可直接用 Android Studio 打开开发调试,改动可通过脚本同步回源码树。
📐 想看应用自身是怎么设计的? 见
ARCHITECTURE.md------ 分层架构、18 个包地图、运行时骨架、13 条核心数据流、组件清单、扩展点。 本文档只讲移植过程(隐藏 API 编译方案、编译报错、签名与装机)。
1. 工程信息
| 项 | 值 |
|---|---|
| 包名 | com.android.documentsui |
| 工具链 | AGP 8.1.2 / Gradle 8.2 / JDK 17(与 CarScreensaverKotlin 一致) |
| compileSdk | 33 |
| minSdk / targetSdk | 29 / 30(对齐 AOSP documentsui_defaults) |
| 语言 | 纯 Java,零 Kotlin,AndroidX |
| 位置 | C:\code\DocumentsUIStudio |
2. 目录结构
bash
DocumentsUIStudio/
├── settings.gradle.kts / build.gradle.kts / gradle.properties
├── gradle/wrapper/gradle-wrapper.properties
├── scripts/sync_back.sh ← 改动同步回 0629 源码树
└── app/
├── build.gradle.kts ← 依赖 + 隐藏 API 编译方案
├── proguard.flags ← 原 AOSP 混淆规则
├── libs/framework-13-android-all.jar ← 全量框架 jar(见 §3)
└── src/main/
├── AndroidManifest.xml ← 已删 package=/uses-sdk(AGP 8 要求)
├── java/com/android/documentsui/ ← 原 src/ 全量(256 个文件)
│ ├── DocumentsStatsLog.java ← 开发版桩(见 §4)
│ └── ...(files/ picker/ dirlist/ roots/ ...)
├── java/com/android/modules/utils/build/SdkLevel.java ← 并入的 modules-utils 源
└── res/ ← 原 res/ 全量
3. 隐藏 API 编译方案(核心机制)
AOSP 中 DocumentsUI 以 sdk_version: "system_current" 编译,源码使用了 @SystemApi/@hide 符号(UserHandle.of、UserManager.isManagedProfile(int)、android.os.FileUtils 等), 标准 compileSdk 的 android.jar 里没有,直接编译会报"找不到符号"。
解法 :app/libs/framework-13-android-all.jar(Robolectric android-all 13, 178MB / 66510 条目,来自 Maven Central,含全部 框架类含隐藏 API) 被前置到 javac 的编译 classpath (见 app/build.gradle.kts):
- 同名类按 classpath 顺序命中 →
android.*类型解析自全量 jar; java.*仍来自标准 android.jar(全量 jar 不含它们,正好互补);- 全量 jar 仅参与编译、不打入 APK(实测 APK 17.6MB / 框架 jar 178MB), 运行时用的仍是设备真实 framework。
⚠️ 三个必须遵守的点(否则编译必挂)
-
★ 必须前置到【classpath】,不是 bootclasspath。 实测 AGP 8.1.2 把 compileSdk 的 android.jar 放在常规编译 classpath 上 (
JavaCompile.classpath的第一项就是platforms/android-33/android.jar),options.bootstrapClasspath并不参与本次编译。 只设 bootclasspath 的现象就是"方案没生效"------满屏cannot find symbol: UserHandle.of / myUserId / Uri.toSafeString / DocumentsContract.setManageMode / ContentResolver.getCache / Root.FLAG_HAS_SETTINGS / StatsLog.write(StatsEvent)。 -
时序:在
doFirst {}(或gradle.projectsEvaluated {})里改写。 脚本顶层configureEach {}会在 AGP 之后被覆盖。 -
gradle.properties必须设android.nonFinalResIds=false。 AGP 8 起默认为 true → R 字段不再 final → AOSP 源码里public static final int SORT_DIMENSION_ID_SIZE = R.id.size;不再是常量表达式, 于是@IntDef({...})报element value must be a constant expression, 所有case R.id.xxx:报constant expression required(一次报 100+ 条,看着吓人, 实际就这一个开关)。
实测:修完 1+3 后
:app:compileDebugJavaWithJavac一次通过,:app:assembleDebug产出app-debug.apk(17.6MB)。
⚠️ 运行时注意:debug 包直接安装时隐藏 API 可能被设备的 hidden-API 检测拦截 (logcat 有 warning,个别功能异常)。完整功能需按原 AOSP 方式预置 (platform 签名 + priv-app,签名平台应用自动豁免检查)。
4. 与 AOSP 的差异(同步回树时注意)
| 差异点 | AS 工程侧 | AOSP 侧 |
|---|---|---|
DocumentsStatsLog.java |
手写桩:write() 不上报,atom ID 为占位值 | stats-log-api-gen 生成,真实 atom ID |
SdkLevel.java |
并入 java/com/android/modules/utils/build/ |
来自 frameworks/libs/modules-utils(构建依赖) |
SelectionDemoActivity |
最小桩类(0629 树裁了 demo 目录但 manifest 仍引用,补齐防运行时崩溃) | 位于 src/.../selection/demo/(0629 已裁) |
AndroidManifest.xml |
已删 package=/uses-sdk(AGP 8 强制) |
需保留这两个属性 |
ProvidersCache.updateAsync 的断言 |
已修复(见 §4.1,这是真 bug,回树时建议一并带过去) | 上游仍是漏掉 FLAG_SUPPORTS_SEARCH 的过期断言 |
4.1 ★ debug 包启动闪退:上游过期断言(已修复)
现象 :adb install -r 装上后一启动就崩, java.lang.AssertionError at ProvidersCache.updateAsync(ProvidersCache.java:211), 调用链 DocumentsApplication.onCreate → ProvidersCache.updateAsync。
根因(不是移植引入的,源码与 0629 树 1:1) :AOSP 上游 generateRecentsRoot() 给 Recents 根设了 3 个 flag,而 updateAsync() 里的断言只比对 2 个:
java
// generateRecentsRoot()(第 138 行)
flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_SEARCH;
// updateAsync() 的断言(原第 211 行)------ 少了 FLAG_SUPPORTS_SEARCH
assert (recentRoot.flags == (Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD));
为什么量产包从不崩、debug 包必崩 :AGP 8 对 debuggable 变体会保留并强制开启 Java 断言 ------ 实测反汇编已装 APK,ProvidersCache 的 static final synthetic boolean $assertionsDisabled 在 DEX 里是常量 false (即断言恒执行);而 release/量产链路(D8 默认 + R8)会把整条 assert 剥掉 (用最小样例 + d8 实测验证过),所以上游这个 bug 在量产机上从来没暴露过。
修复 :把断言补齐成与实现一致的 3 个 flag(本工程已改)。 全工程共 58 处 assert,只有这一处是"常量与实现漂移",其余都是真正的空值/状态不变量检查, debug 下照常生效 ------ 以后若再爆 AssertionError,多半是挖到了真 bug,按业务判断处理即可。
排查时注意:adb shell dumpsys package com.android.documentsui 的 flags 里会有 DEBUGGABLE(debug 变体所致)+ UPDATED_SYSTEM_APP(覆盖安装系统应用所致),属正常。
5. 使用
bash
# Android Studio:Open → 选 C:\code\DocumentsUIStudio → Sync → Run
# ⚠️ Gradle JDK 必须选 17(不能是 21):AGP 8.1.2 的 JdkImageTransform(jlink)
# 在 JDK 21 上会失败(`androidJdkImage` 转换报错)。
# 命令行实编(本机验证用):
JAVA_HOME="C:/Users/ZQ/.jdks/jbr-17.0.14" gradle :app:assembleDebug
依赖版本速查:appcompat 1.6.1 / material 1.9.0 / recyclerview 1.3.0 / recyclerview-selection 1.1.0 / transition 1.4.1 / guava 31.1-android / commons-compress 1.21 / jsr305 3.0.2(对应 AOSP documentsui_defaults 的 static_libs)。
5.1 装机验证:已接入 platform 签名,可直接覆盖安装
设备(uis7870_3h10_car_native / userdebug / test-keys)上 com.android.documentsui 已作为 /system/priv-app/DocumentsUI 预置且为 platform 签名。工程已把平台密钥接进 Gradle(见 §5.2),debug / release 均用 platform 签名,且 versionCode 对齐到 设备上的 33,因此不必再改包名绕路:
| 路径 | 做法 | 说明 |
|---|---|---|
| A. 覆盖安装(快,推荐迭代) | adb install -r app/build/outputs/apk/debug/app-debug.apk |
签名一致 + 版本号不回退即可覆盖;APK 落在 /data/app,privapp 白名单按包名仍生效 |
| B. 推送替换 /system(等价预置形态) | adb root && adb remount → adb push app-debug.apk /system/priv-app/DocumentsUI/DocumentsUI.apk → 清 /data/app 残留 → adb reboot |
最接近量产形态 |
| C. AOSP 集成编译(最终验证) | bash scripts/sync_back.sh C:/ZQ/0629 → m DocumentsUI → 刷机 |
最慢但最真实 |
5.2 平台签名配置
密钥文件 app/keys/platform.jks(PKCS12,alias = platform),SHA256 指纹 C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8 ------ 已从设备拉取 /system/priv-app/DocumentsUI/DocumentsUI.apk 实测比对,与系统平台签名完全一致。
口令集中在 gradle.properties 的 PLATFORM_KEYSTORE_* 四项(路径默认 app/keys/platform.jks, 相对工程根目录 )。密钥文件本身已进 .gitignore(keys/、*.jks)。
⚠️
gradle.properties是入库文件 (里面还有android.nonFinalResIds=false等必需开关), 所以口令也会一起提交。当前用的是 AOSP platform 测试密钥 ,私钥与口令本就是公开的, 可接受。若换成客户的量产平台密钥,请把口令挪到不入库的位置,构建脚本会按-P命令行参数 →~/.gradle/gradle.properties→ 工程gradle.properties的顺序取值:
bash# 例:口令放用户级配置,不随工程提交 # %USERPROFILE%\.gradle\gradle.properties PLATFORM_KEYSTORE_PASSWORD=xxx PLATFORM_KEY_PASSWORD=xxx
app/build.gradle.kts 中 signingConfigs.create("platform") 同时挂到 debug 与 release (debugAndroidTest 也继承),开启 v1/v2/v3 签名、关闭 v4(.idsig 仅用于增量安装)。 密钥换位置只需改 PLATFORM_KEYSTORE_FILE;密钥缺失时构建不失败,但会回退 debug 签名并打印 警告 ------ 那种 APK 装不进设备。
验证签名是否真的生效(注意加 --min-sdk-version 23):
bash
keytool -printcert -jarfile app/build/outputs/apk/debug/app-debug.apk | grep SHA256
# 期望:C8:A2:E9:BC:...:51:19:2A:B8(与设备平台证书一致)
"$ANDROID_HOME/build-tools/<ver>/apksigner" verify -v --min-sdk-version 23 \
app/build/outputs/apk/debug/app-debug.apk
为什么必须加
--min-sdk-version 23:apksigner 只校验当前 minSdk 仍然需要的签名方案 。 本工程minSdk = 29,直接apksigner verify -v会打印v1: false / v2: false / v3: true,看起来像只签了 v3 ------ 其实 v1+v2+v3 三个块都在 , 只是 apksig 认为 minSdk≥24 时 v1 无需校验、minSdk≥28 时 v2 已被 v3 取代。 用--min-sdk-version 23复验即可看到v1/v2/v3 全部 true(实测结论)。
6. 同步改动回源码树
bash
bash scripts/sync_back.sh C:/ZQ/0629
脚本只同步 documentsui 包源码与 res/(增量、带 diff 提示),并自动跳过 DocumentsStatsLog 桩与 SdkLevel;Manifest 不自动同步 (属性差异见 §4,需手工处理)。 同步后在源码树里正常单编:m DocumentsUI。
7. 已知限制 / 排坑
-
已实编验证通过 (
:app:compileDebugJavaWithJavac+:app:assembleDebug均 BUILD SUCCESSFUL,APK 17.6MB)。 命令行复现(必须 JDK 17,不能是 AS 自带的 JBR 21):bashJAVA_HOME="C:/Users/ZQ/.jdks/jbr-17.0.14" \ "C:/Users/ZQ/.gradle/wrapper/dists/gradle-8.2-bin/bbg7u40eoinfdyxsxr3z4i7ta/gradle-8.2/bin/gradle" \ :app:assembleDebug -
构建期 warning 可忽略 :
processDebugResources会打出一批removing resource .../string/xxx_file_type without required default value------ 这是 0629 树缺res/values/mimes.xml(只剩各语言目录的陈旧翻译)导致的, 这些键在代码/res 里已无任何引用 (已核实),不影响功能。 (如要消除,可从上游 AOSP 补回res/values/mimes.xml。) -
gradle-wrapper 只附 properties,无
gradlew/gradle-wrapper.jar------AS 打开自动补齐, 或用本地 Gradle 8.2。 -
DocumentsStatsLog桩使统计不上报(开发无影响);需要真实统计时从 AOSP 编译产物取回生成文件替换。 -
系统功能依赖:
MANAGE_DOCUMENTS等签名权限、IntentForwarderActivity跨 profile 转发、privapp 白名单(privapp_whitelist_com.android.documentsui) ------ 已配置 platform 签名,覆盖安装即可拿到签名权限;priv-app 白名单在/data/app覆盖形态下同样按包名生效。 -
混淆:release 配置沿用原
proguard.flags,默认isMinifyEnabled=false; 需要瘦身时自行打开。