我懒,所以README.md是大肥鱼写的。
用 PUI 设计系统重写的系统安装器,替换 ColorOS 17 / Android 17 上的
com.android.packageinstaller(AOSP PackageInstaller 17.7.4)。
界面全部是 Compose + PUI 组件(灰底白卡、非凡洋红、20dp 圆角、悬浮胶囊导航栏),
安装能力走 PackageInstaller 会话,系统安装器身份带来静默安装 / 静默卸载。
PUIInstaller/
├── composeApp/src/commonMain/kotlin/com/panl/pui/ ← PUI 库(从 PUIDemo 1.0.92 原样搬入)
├── composeApp/src/androidMain/
│ ├── kotlin/com/panl/installer/
│ │ ├── core/ APK 解析、签名识别、安装引擎、设置、通知
│ │ └── ui/ 首页 / 安装页 / 卸载页 / 设置 / 签名检测 / 应用商店能力
│ └── res/ 自适应图标(含 -night 深色渐变底)、主题
└── tools/build.sh 构建 → zipalign → AOSP testkey 签名
设备上的系统安装器位于 /system_ext/priv-app/PackageInstaller/,签名是 AOSP testkey
(CN=Android, O=Android,SHA-256 a40da80a…)。这份 APK 用同一把公开测试密钥签名,
因此可以:
- 以「系统应用更新」的身份覆盖安装(
UPDATED_SYSTEM_APP),保留PRIVILEGED标志; - 继续持有
INSTALL_PACKAGES/DELETE_PACKAGES,实现静默安装、静默卸载; - 通过 KernelSU 模块直接替换
/system_ext/priv-app/PackageInstaller/PackageInstaller.apk。
换 ROM / 换签名后需要重新确认:
apksigner verify --print-certs对比系统 APK 的证书。 签名不一致时只能用模块覆盖(且要清掉旧包数据),不能adb install -r。
- 自适应图标:
res/mipmap-anydpi-v26/ic_launcher.xml- 图形:官方
Directbox_Receive.svg原样搬入(24×24 视图,group 放大 3.2 倍居中), 四个 path 全部改成白色;托盘与槽口保留原图 40% 的透明度,叠出来仍有层次 - 亮色底:洋红斜向渐变(
drawable/ic_launcher_background.xml) - 暗色底:深色渐变 + 左上柔光(
drawable-night/ic_launcher_background.xml), 前景同样是白色,深底上对比度反而更强,因此不再单独做夜间前景 - 单色层:Android 13+ 主题图标(只保留箭头 + 盒身剪影)
- 图形:官方
- 首页(
ui/main/HomeScreen.kt):Hero 状态卡 + 两个快捷入口 + 功能清单。 新增功能只要往features列表里加一条,样式与点击行为统一。
ui/components/Widgets.kt 的 VersionCompareCard:
当前 1.0.91 (92) → 新版本 1.0.92 (93)
从 1.0.91 更新到 1.0.92,版本号 92 → 93
- 数据来自
ApkInspector:getPackageArchiveInfo读 APK 的 versionName/versionCode, 再与getPackageInfo的已安装版本比对,得到NEW / UPDATE / DOWNGRADE / REINSTALL四种关系; - 未安装时左侧显示「未安装」,降级时整卡转错误色并提示去设置里开「允许降级安装」;
- 已安装应用列表(应用页)也逐行显示版本号。
- 设置 → 安装 → 安装后删除安装包(另有「删除前确认」二级开关);
- 安装成功后:先删暂存副本,再按开关删源文件;源是
content://时依次尝试ContentResolver.delete→DocumentsContract.deleteDocument→ 真实路径删除; - 结果页会明确显示「已删除安装包」或「安装包未能删除(没有写入权限)」, 设置关掉时结果页也提供手动「删除安装包」按钮;
- 已声明
MANAGE_EXTERNAL_STORAGE,设置页有「文件访问权限」入口, 用于处理file://形式的安装包(公共目录直读直删)。
签名检测(core/ApkSignature.kt):
-
读取 APK 的 v2/v3 签名块,解析 X.509 证书,输出主题、颁发者、SHA-256/SHA-1、算法、有效期;
-
内置 AOSP 四把公开密钥的证书指纹,命中即标记:
密钥 SHA-256 testkey a40da80a59d170caa950cf15c18c454d47a39b26989d8b640ecd745ba71bf5dcplatform c8a2e9bccf597c2fb6dc66bee293fc13f2fc47ec77bc6b2b0d52c11f51192ab8shared 28bbfe4a7b97e74681dc55c2fbb6ccb8d6c74963733f6a46ae74d8c3a6e879fdmedia 465983f7791f2abeb43ea2cbdc7f21a8260b72bc08a55c839fc1a43bc741a81e -
另外识别 Android 调试密钥(
CN=Android Debug); -
安装页给出红色警告卡,独立的「签名检测」页列出全部签名者详情。
SHOW_APP_INFO(ui/UninstallActivity.kt):
<intent-filter>
<action android:name="android.intent.action.SHOW_APP_INFO" />
<data android:scheme="package" />
</intent-filter>LibChecker 里点应用图标 → 用本安装器打开 → 直接进「应用信息 + 卸载」页。
同一页面还接 ACTION_DELETE / ACTION_UNINSTALL_PACKAGE / APPLICATION_DETAILS_SETTINGS。
参考 https://developer.android.com/registered-app-stores/overview, 代码侧要求都落在清单与实现里:
- 声明
android.permission.INSTALL_PACKAGES_AS_REGISTERED_APP_STORE与android.permission.REQUEST_INSTALL_PACKAGES; - 处理安装 / 卸载 / 应用信息三类必需 Intent;
- 安装会话设置
SessionParams.USER_ACTION_NOT_REQUIRED(Android 16+ 的一键安装语义); - 安装页展示名称、开发者(签名者)、版本、大小、权限 —— RAS 对「清晰的应用信息」的要求;
- 「应用商店能力」页(
ui/AppStoreActivity.kt)做 9 项自检 + 注册流程说明。
注册本身是 Google 的运营计划(企业资质、年费、MASA 认证、目录上报),代码无法替代; 页面只做代码侧自检与流程说明。
设置 → 外观 / 导航栏,对应 PUI 令牌页那组控件(全部实时生效、落盘保存):
| 控件 | 范围 | 作用 |
|---|---|---|
| 顶部模糊与毛玻璃 | 开关 | 整屏是否挂 AGSL 逐像素采样;关掉最省 GPU |
| 顶部模糊深度 | 2–100 px | 顶部渐进模糊的最强半径 |
| 卡片展开模糊 | 0–120 px | 对话框(PUI 卡片)弹出时,背后页面的模糊半径 |
| 导航栏 · 模糊半径 | 2–64 px | 悬浮胶囊的毛玻璃半径 |
| 导航栏 · 背景不透明度 | 0–100 % | 玻璃底色 alpha,0 为全透 |
实现上沿用 PUI 的两条路径:顶部 + 胶囊在同一个 AGSL 里算(puiProgressiveTopBlur),
对话框背后的模糊走 puiRealtimeBlur(RenderEffect,进度 0→1 动画)。
设置 → 安装界面:
| 项 | 选项 | 说明 |
|---|---|---|
| 样式 | 浮窗 / 全屏 | 浮窗是底部弹出的卡片,身后界面保留并做背景模糊;全屏是整页样式 |
| 浮窗位置 | 居中 / 底部 | 贴底时只圆上面两角,居中时四角全圆、四周留 14dp |
实现要点(踩过的坑):
- 窗口半透明必须在清单里定死。
ActivityManager在启动时就根据清单主题判断任务是否translucent,代码里setTheme()已经来不及 —— 那样会出现「背景全黑、后面的应用被隐藏」。 所以InstallActivity清单主题挂Theme.PuiInstaller.Sheet,全屏模式在onCreate里 再setTheme(Theme.PuiInstaller)换回不透明。 - 模糊是系统做的:
FLAG_BLUR_BEHIND+LayoutParams.setBlurBehindRadius(26dp), 依赖ro.surface_flinger.supports_background_blur=1。系统不支持时只剩一层 18% 黑遮罩。 - 浮窗与全屏共用同一个
InstallController状态机,只有容器与信息密度不同; 浮窗里签名只保留「AOSP 公开密钥」风险提示,完整证书信息在全屏版。 - 浮窗头部的「⋯」可以直接切换样式与位置;改样式会
recreate()(窗口属性不能热改)。
对外 versionName 从 2.0.0 起迭代:2.0.1 / 2.1.0 …
versionCode 不能跟着从 20000 起:这个包在系统镜像里有预装版本(模块里的 17700004),
PMS 明确拒绝把系统应用降到预装版本以下(System app cannot be downgraded to older than its preloaded version)。所以保留 17700000 前缀:
versionCode = 17700000 + major*100 + minor*10 + patch
2.0.0 → 17700200
2.0.1 → 17700201
2.1.0 → 17700210
安装过程中会发一条进度型实时活动,被系统提升后在通知栏顶部常驻、并在状态栏显示胶囊。
| 项 | 做法 |
|---|---|
| 权限 | 清单声明 POST_PROMOTED_NOTIFICATIONS(普通权限,装包即授予) |
| 样式 | Notification.ProgressStyle + 两段进度(复制 30 / 安装 70),无进度时 setProgressIndeterminate(true) |
| 提升 | 请求 requestPromotedOngoing(见下方注意),并满足 ongoing / 有 contentTitle / 无自定义 RemoteViews / 渠道非 MIN |
| 胶囊文字 | setShortCriticalText:有进度给 86%,不确定时给「安装中」 |
| 渠道 | 专用 install_live(IMPORTANCE_DEFAULT);结果通知仍走 install_status(LOW,发完即走) |
| 开关 | 设置 → 通知 → 安装进度实时活动 |
实测(ColorOS 17 / Android 17):
Notification(channel=install_live ... flags=ONGOING_EVENT|ONLY_ALERT_ONCE|PROMOTED_ONGOING)
PROMOTED_ONGOING 出现即代表系统已把它当实时活动处理。
注意: Notification.Builder#setRequestPromotedOngoing() 到 API 37 才进公开 SDK,
本项目 compileSdk 是 36,所以直接写 extra:
builder.addExtras(Bundle().apply {
putBoolean("android.requestPromotedOngoing", true) // = EXTRA_REQUEST_PROMOTED_ONGOING
})关于流体云:ColorOS 16 起,流体云直接消费 Android 16 的 Live Updates, 不需要接 OPPO 私有 SDK —— 按上面这套标准实现即可上云。 OPPO 开放平台那份文档(id=12658)是 JS 渲染的,抓不到正文;如果个别机型上云不显示, 大概率是 OPPO 侧对应用做了白名单或额外条件,需要在开放平台申请,代码侧无需改动。
# 需要 JDK 17+;SDK 路径写在 local.properties
./tools/build.sh
# 产物:out/PUIInstaller-<versionName>-<versionCode>.apk脚本做三件事:assembleRelease → zipalign -p 4 → apksigner 用
keys/testkey.pk8 + keys/testkey.x509.pem 签名(v1+v2+v3)。
密钥来自 AOSP build/target/product/security/,是公开的;不要用它签任何要分发的应用。
方式 A:覆盖安装(当前使用)
adb install -r out/PUIInstaller-2.0.0-17700200.apk系统会把它当作系统安装器的更新,保留 PRIVILEGED 与安装权限。
方式 B:KernelSU 模块
把 APK 放到模块的 system/system_ext/priv-app/PackageInstaller/PackageInstaller.apk,
重启后由 magic mount 覆盖系统分区里的原 APK。模块目录:
/data/adb/modules/PUIAOSPPackageInstall/
└── system/system_ext/priv-app/PackageInstaller/PackageInstaller.apk
- Intent filter 必须带 mimeType:
ACTION_INSTALL_PACKAGE的 intent 如果带了type=application/vnd.android.package-archive,清单里只写<data android:scheme="content"/>是匹配不上的(酷安就是这种调用,会抛ActivityNotFoundException,表现是「点安装没反应」)。 清单里因此同时保留了「scheme + mimeType」和「仅 scheme」两组 filter。 - 应用归档(unarchive):AOSP 的
UNARCHIVE_DIALOG由原 PackageInstaller 处理, 本实现暂未接管;已归档应用恢复时可能提示找不到处理程序。 - 降级安装:
SessionParams.setRequestDowngrade是@SystemApi,公开 SDK 里没有, 代码用反射调用;被隐藏 API 策略拦下时系统会返回INSTALL_FAILED_VERSION_DOWNGRADE。 - RAS 一键安装:
USER_ACTION_NOT_REQUIRED只有在商店通过 Google 审核后才生效, 普通签名下系统会忽略该标志,仍走正常安装流程。 - 静默能力依赖系统身份:以普通应用安装时没有
INSTALL_PACKAGES, 会话提交后系统会弹确认(本应用会把系统的确认 Intent 原样转交)。
| 关注点 | 文件 |
|---|---|
| APK 解析(label/icon/版本/权限/签名) | core/ApkInspector.kt |
| 签名与 AOSP 密钥识别 | core/ApkSignature.kt |
| 安装 / 卸载会话 | core/InstallEngine.kt |
| 安装包清理 | core/InstallEngine.kt 的 SourceCleaner |
| 结果广播与通知 | core/InstallResultReceiver.kt、core/UninstallResultReceiver.kt、core/Notifications.kt |
| 安装页状态机 | ui/install/InstallController.kt |
| 安装页界面 | ui/install/InstallScreen.kt |
| 卸载 / 应用信息 | ui/uninstall/UninstallScreen.kt |
| 首页 / 应用 / 设置 | ui/main/*.kt |
| 签名检测页 | ui/SignatureActivity.kt |
| 应用商店能力页 | ui/AppStoreActivity.kt |
| 通用卡片与图标 | ui/components/Widgets.kt |