Skip to content

修复 Android 16 运营商配置写入并加固 instrumentation 回退路径(realme 真机验证) - #127

Draft
ziren28 wants to merge 2 commits into
Ackites:masterfrom
ziren28:codex/android16-realme-carrier-fallback
Draft

ziren28 wants to merge 2 commits into
Ackites:masterfrom
ziren28:codex/android16-realme-carrier-fallback

Conversation

@ziren28

@ziren28 ziren28 commented Jul 13, 2026

Copy link
Copy Markdown

关联与提交关系

本 PR 承接并完整保留 @a5533348#122 中提出的 instrumentation 回退方案:

感谢 @a5533348 对新版 Android 兼容路径的分析和初始实现。

问题与根因

Nrfr 原有写入路径通过 Shizuku 包装 ICarrierConfigLoader

carrierConfigLoader.overrideConfig(subId, bundle, true)

在部分新版 Android / 厂商 ROM 上,即使 Shizuku 已正常运行并向 Nrfr 授权,TeleService 仍会根据 Binder 实际调用方拒绝 shell UID,错误为:

overrideConfig cannot be invoked by shell

因此这里不是普通的 Shizuku 授权缺失,也不能通过重复授权解决。本 PR 的目标是:

  1. 保留旧设备已经可以工作的直接 Shizuku 路径;
  2. 只在识别到上述 shell 调用限制时启用 instrumentation 回退;
  3. 收窄 instrumentation 的输入和权限边界;
  4. 不再把异步请求被接受误报为“写入已经成功”;
  5. 如实记录 realme UI 7.0 上的临时覆盖行为和验证范围。

调用链

MainScreen:保存 / 还原
  │
  └─ CarrierConfigManager
       │
       ├─ 直接调用 ICarrierConfigLoader.overrideConfig(..., persistent = true)
       │    └─ 成功:保持原有路径并立即刷新
       │
       └─ 仅当 SecurityException 包含
            "cannot be invoked by shell"
            │
            └─ PrivilegedCarrierConfigRunner
                 ├─ 在应用进程保存配置副本
                 ├─ 生成 10 秒有效的一次性 UUID token
                 ├─ 通过 Shizuku IActivityManager 启动同包 instrumentation
                 └─ PrivilegedCarrierConfigInstrumentation
                      ├─ 原子消费 token,并核对 PID
                      ├─ 仅委派 MODIFY_PHONE_STATE
                      ├─ 尝试 persistent = true
                      ├─ 已知厂商持久化限制 → persistent = false
                      └─ 释放本次取得的委派并结束 instrumentation

还原操作沿用同一调用链,以 bundle = null 表示清除 override。

逐文件改动

文件 改动 目的
app/src/main/AndroidManifest.xml 注册 PrivilegedCarrierConfigInstrumentation,目标包为当前 applicationId 为受控的应用内回退路径提供入口
CarrierConfigManager.kt 写入与还原接口接收 Context;先尝试原有直接调用,只对明确的 shell 拒绝切换回退 不改变旧系统路径,避免吞掉无关 SecurityException
PrivilegedCarrierConfigRunner.kt 新增请求队列、UUID token、10 秒 TTL、原子消费、最小权限委派、持久/临时覆盖回退和异常清理 避免把任意配置载荷暴露给 instrumentation 参数,并收窄临时权限
MainScreen.kt 回退路径延迟 800 ms 后刷新;提示由“已保存”改为“请求已提交” 避免把异步启动等同于最终写入成功
README.md 修正“一定永久保持”的表述,增加 realme UI 兼容性与真机验证边界 避免对不同厂商的持久性作错误承诺

本 PR 没有增加网络请求、动态代码加载或新的 Gradle 依赖,也不需要 Root。

关键实现

1. 只对明确的 shell 限制启用回退

try {
    carrierConfigLoader.overrideConfig(subId, bundle, true)
    return false
} catch (e: SecurityException) {
    if (e.message?.contains("cannot be invoked by shell") == true) {
        PrivilegedCarrierConfigRunner.overrideConfig(context, subId, bundle)
        return true
    }
    throw e
}

其他安全异常继续向上传递,不会被 instrumentation 路径掩盖。

2. 配置不进入 instrumentation 参数

val token = UUID.randomUUID().toString()
pendingRequests[token] = PendingCarrierConfigRequest(
    subId = subId,
    bundle = bundle?.let { PersistableBundle(it) },
    createdAtElapsedMs = SystemClock.elapsedRealtime()
)

val args = Bundle().apply {
    putInt(ARG_CALLER_PID, Process.myPid())
    putString(ARG_REQUEST_TOKEN, token)
}

subIdPersistableBundle 只保存在应用进程内。instrumentation 参数中仅传 PID 与随机 token,外部启动方不能通过参数直接构造待写入的配置对象。

消费请求使用 ConcurrentHashMap.remove(token)

val request = pendingRequests.remove(token) ?: return null
if (SystemClock.elapsedRealtime() - request.createdAtElapsedMs > REQUEST_TTL_MS) {
    return null
}

随机 UUID 是请求的主要一次性凭据;PID 校验用于拒绝跨进程或应用进程重启后的请求,不把可查询的 PID 描述为独立鉴权秘密。token 过期、未知或重复使用都会 fail closed。

3. 最小权限委派

manager.startDelegateShellPermissionIdentity(
    Os.getuid(),
    arrayOf(Manifest.permission.MODIFY_PHONE_STATE)
)

原始方案传入 null,权限范围过宽。本 PR 明确限制为本功能需要的 MODIFY_PHONE_STATE

4. 受限的持久化降级

try {
    overrideCarrierConfig(request, persistent = true)
    persistent = true
} catch (e: SecurityException) {
    if (isKnownPersistentOverrideRestriction(e)) {
        overrideCarrierConfig(request, persistent = false)
    } else {
        throw e
    }
}

只有异常信息表明 persistent=true 或 system app 持久覆盖限制时才降级为 persistent=false;未知异常不会被当作兼容问题继续执行。

5. 失败与权限清理

  • startInstrumentation() 抛异常或返回 false 时立即删除 pending token;
  • 只有本次成功取得委派后才执行 stopDelegateShellPermissionIdentity()
  • 清理异常不会跳过 finish()
  • instrumentation 结果中记录 successpersistenterror

当前 UI 尚未接入 IInstrumentationWatcher,所以“instrumentation 已启动”不等于“最终写入已确认”。本 PR 将提示改为“请求已提交”,没有继续显示误导性的“设置已保存”。

“权限监控”说明

在本次测试的真我 GT8 Pro / realme UI 7.0 开发者选项中,没有找到旧教程所称的“权限监控”或“禁止权限监控”。英文界面中可以看到 Disable system optimization,但没有证据证明它与旧入口是同一功能,因此本 PR:

  • 不要求用户切换 Disable system optimization
  • 不把缺少旧开关判断为配置错误;
  • 不依赖厂商开发者选项绕过 TeleService 检查;
  • 在代码中处理独立的 shell 调用方限制。

该结论只对应本次系统版本,不外推到所有 realme / ColorOS 设备。

真机验证

项目 结果
设备 真我 GT8 Pro(RMX5200
Android Android 16 / SDK 36
系统 realme UI 7.0、Oplus ROM V16.1.0、RMX5200_16.0.7.205(CN01)
权限环境 Shizuku 通过 ADB 启动并向 Nrfr 授权;未使用 Root 或 Device Owner
安装与启动 通过
SIM 1 写入 通过
SIM 2 写入 通过
服务侧回读 最终只读检查中,两个 phone block 均存在 active override
电话服务事件链 两个 phone index 均观察到 OVERRIDE → callback → CARRIER_CONFIG_CHANGED
稳定性 测试窗口内未观察到 Nrfr 崩溃或 ANR
持久性 服务侧不存在 persistent override;本机实际使用 persistent=false 回退
还原 未测试
重启 / 电话进程重启 未测试

这里的“写入成功”指 Android CarrierConfig 服务中的 override 已生效,不表示修改了实体 SIM 卡。

用户反馈:双卡都应用配置后,TikTok 在该设备上可以正常使用。这只是本次设备上的用户观察,不是自动化验收条件,也不构成对 TikTok、其他应用或其他网络环境的兼容性承诺。

报告和附件中未包含手机号、ICCID、订阅标识、设备序列号或本次测试使用的具体 override 值。

真机测试 APK 及制作过程

为方便维护者复现,已将真机使用的 APK 放到个人 fork 的 Pre-release;二进制文件不进入上游 PR 的 Git 历史:

制作链

  1. 使用官方 Nrfr v1.0.3 APK 作为二进制基线:

    SHA-256: A612088132E62763F31D6C1C79B40C89E940D8E87FF26ED943D50602FF3D2B3B
    
  2. 回退基线固定到 Fix carrier config saving on newer Android #122 提交 6c05f224dc802d0df952db93a6d2852a91c9edb0;加固设计对应当前提交 ae849997c98a26fab9af2650e2c19b32996a535f

  3. 使用 Apktool 2.10.0 解包官方 APK,将设计手工移植到 Smali。

  4. 使用 Apktool 内置 aapt2 重建资源和 8 个 DEX。

  5. 使用 Android Build Tools 33.0.2 zipalign 对齐。

  6. 使用仓库外本地测试证书签名,并验证 APK Signature Scheme v1、v2、v3。

  7. 安装到 RMX5200 后,核对手机内 base.apk 与附件 SHA-256 完全一致。

工件信息

文件: Nrfr-v1.0.3-pr127-realme-android16-test-repack.apk
大小: 7,075,594 bytes
APK SHA-256: 3C33663214C9717076F25D9639D1E4A7059DC8A0E283A80EC0253DAEDB8A5CE1
测试证书: 自签 CN=Android Debug
测试证书 SHA-256: 1e08a903aef9c3a721510b64ec764d01d3d094eb954161b62544ea8f187b5953
包名: com.github.nrfr
版本: 1.0.3 (versionCode 3)
minSdk / targetSdk: 26 / 34
debuggable: false

Manifest 未新增 uses-permission,无 INTERNET 权限。新增的是同包 instrumentation;运行时通过 Shizuku 临时委派 MODIFY_PHONE_STATE

该 APK 是按照 PR 设计手工移植的 patched-repack 测试件,不是 Gradle 从当前 Kotlin 源码直接生成的 APK,也不能替代源码编译验证。可审查实现以本 PR 的源码差异为准。

安装注意

测试证书与官方 APK 不同,不能直接覆盖官方版本。需要先卸载旧签名版本(会清除 Nrfr 应用数据),安装后重新授予 Shizuku 权限。卸载应用不等于清除系统侧已经生效的 CarrierConfig override;返回官方版时也需要先卸载测试包,再安装官方签名 APK。本附件不是官方发布,不建议普通用户作为长期版本使用。

本测试件派生自采用 Apache License 2.0 的 Ackites/Nrfr。修改源码见 ae849997c98a26fab9af2650e2c19b32996a535f,许可证见 LICENSE。该测试发布不代表上游认可或背书。

已知限制

  • instrumentation 回退是异步请求,UI 尚未接收最终 result bundle;
  • 800 ms 延迟刷新用于降低首次读取旧值的概率,不是可靠的完成事件;
  • 当前请求过期后会失去执行资格,但最后一个未抵达 instrumentation 的对象只会在下次操作时清理;
  • 持久化限制识别依赖已知异常文本,其他厂商使用不同文案时会把异常返回 UI;
  • 本机只能使用临时 override,重启手机或电话服务后可能失效;
  • 还原路径和重启后的行为尚未完成真机验证;
  • 当前只验证了一个机型和一个系统构建,不能外推到全部 Android 16 或 realme 设备;
  • fork APK 使用本地测试证书,不能替代正式签名版本;
  • patched-repack 真机验证不等于当前 Kotlin 源码已经完成 Gradle 构建。

Reviewer checklist

  • 旧系统的直接 ICarrierConfigLoader 路径是否保持不变;
  • instrumentation 是否只能消费应用进程生成的一次性 token;
  • 过期、重复、缺失 token 是否全部 fail closed;
  • PersistableBundle 是否在入队前复制;
  • shell 权限是否仅限 MODIFY_PHONE_STATE
  • 是否只对已知持久化限制降级为 persistent=false
  • 启动失败、执行异常和权限释放是否都有清理路径;
  • UI 是否避免把异步请求误报为最终成功;
  • 是否需要在后续提交中接入 IInstrumentationWatcher 或目标键回读;
  • 是否由维护者环境补充 Gradle Debug / Release 构建。

再次感谢 @a5533348#122 中提供新版 Android instrumentation 回退思路与初始实现。本 PR 的加固和真机验证均建立在其工作之上。

@ziren28
ziren28 force-pushed the codex/android16-realme-carrier-fallback branch from c379ac6 to ae84999 Compare July 13, 2026 13:12
@ziren28 ziren28 changed the title Harden the Android 16 carrier config fallback and document realme UI compatibility 修复 Android 16 运营商配置写入并加固 instrumentation 回退路径(realme 真机验证) Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants