# phone_hook **Repository Path**: chengdu-xiaochen/phone_hook ## Basic Information - **Project Name**: phone_hook - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-06-10 - **Last Updated**: 2026-06-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # phone_hook — 通话界面号码隐藏 将 Pine Hook 注入系统电话 APK(`com.android.dialer`),在来电浮窗与 **InCallActivity 通话中界面** 按策略隐藏号码;是否隐藏由配套通讯录 App **VertuContacts**(`com.vertu.vertucontacts`)调用后端接口 `POST /api/app/call-list-items/check-hidden` 决定。 本项目为**自包含**工程:签名证书、`main.py` 中间产物在目录内;**原始电话 APK 由命令行传入**,不随仓库分发。通讯录 App 为独立工程(`vertucontactsapp`),需单独编译安装。 ## 目录结构 ``` phone_hook/ ├── main.py # 编译 → 注入 → 系统安装 ├── README.md ├── assets/ │ └── keystore/ # 签名证书(platform / shared) ├── work/ # 注入中间产物(gitignore,运行 main.py 生成) │ ├── hook/ │ ├── decoded/ │ ├── full_decode/ │ └── output/phone-signed.apk ├── app/ # Hook 库源码(Gradle 工程) ├── gradlew └── ... ``` ## 使用 ### 1. 安装 VertuContacts 并禁用系统联系人 电话 Hook 通过 `ContentProvider.call()` 跨进程询问 VertuContacts 是否隐藏号码,**必须先安装并登录 VertuContacts**,且让系统联系人入口落到 VertuContacts。 ```bash # 在 vertucontactsapp 工程编译安装 ./gradlew assembleDebug adb install -r app/build/outputs/apk/debug/app-debug.apk ``` 禁用系统自带联系人 App(`com.android.contacts`): ```bash # 禁用(仅当前用户 user 0;重启后仍生效) adb shell pm disable-user --user 0 com.android.contacts # 确认已禁用 adb shell pm list packages -d | grep com.android.contacts # 确认联系人入口已切到 VertuContacts(应看到 com.vertu.vertucontacts/.MainActivity) adb shell cmd package resolve-activity -a com.android.contacts.action.LIST_DEFAULT ``` 恢复系统联系人(如需回滚): ```bash adb shell pm enable com.android.contacts ``` > **注意** > > - 只禁用 `com.android.contacts`(UI 入口),**不要**禁用 `com.android.providers.contacts`(联系人数据 Provider,系统仍需要)。 > - VertuContacts 需已登录;未登录或接口失败时默认 **隐藏** 号码。 ### 2. 注入并系统安装电话 APK 在 **phone_hook 目录内** 执行,**必须传入原始系统电话 APK 路径**: ```bash conda activate py312 python main.py /path/to/电话_13.0.000.000.2311021620.apk ``` 一次跑完全流程:编译 hook 库 → 解包/注入/重打包/签名 → `adb root` + `remount` 推送到 `/system/priv-app/Dialer_MFV/` → 清理 update 层与缓存 → 重启。 安装完成后 `main.py` 会打印 `codePath`;首行应为 `package:/system/priv-app/Dialer_MFV/Dialer_MFV.apk`。 ### 3. 验证与日志 **通话 Hook 侧**(tag 统一便于过滤): ```bash adb logcat -s InCallUiHelper IncomingCallFloatHelper VertuContactsClient HookBootstrap HookEngine ``` **`check-hidden` 接口**(在 VertuContacts 进程内发起,HTTP 明细由 `HttpTraceLogger` 打出): ```bash # 推荐:只看 VertuContacts,并过滤 check-hidden 路径 adb logcat -s VertuContacts | grep check-hidden ``` 典型成功日志示例: ```text I VertuContacts: HTTP >>> POST https://your-host/api/app/call-list-items/check-hidden auth=true req={"phones":["13812341234"]} I VertuContacts: HTTP <<< POST https://your-host/api/app/call-list-items/check-hidden code=200 duration=120ms resp={"ok":true,"hidden":[{"phone":"13812341234","phone_masked":"138****1234","customer_name":"王总","list_item_id":101}]} I VertuContacts: isHide phone=138****1234 api hide=true I VertuContactsClient: isHide bridge phone=138****1234 hide=true I InCallUiHelper: setPrimaryName shouldHidePhone text=138****1234 isNumber=true hide=true ``` 来电浮窗拦截: ```text I IncomingCallFloatHelper: prepareIncomingDisplay phone=138****1234 hide=true I IncomingCallFloatHelper: intercept float name_number text=快递送餐 phone=138****1234 hide=true ``` 接口失败或未登录: ```text W VertuContacts: isHide api fail phone=138****1234 default hide=true I VertuContacts: isHide loggedIn=false phone=138****1234 hide=true E VertuContacts: HTTP !!! POST .../check-hidden ... code=401 ... ``` ## 协作架构 ``` com.android.dialer (注入 hooklib) └── VertuContactsClient.isHide(phone) └── ContentResolver.call(content://com.vertu.vertucontacts.bridge) └── VertuContacts / CallHideHelper └── POST /api/app/call-list-items/check-hidden Authorization: Bearer {token} Body: { "phones": ["13812341234"] } Response: { "ok": true, "hidden": [{ "phone", ... }] } ``` - 电话 Manifest 会自动注入 `com.vertu.vertucontacts.permission.BRIDGE` 与 ``(Android 11+ 包可见性,缺了会报 `Unknown authority com.vertu.vertucontacts.bridge`)。 - 号码在 `hidden` 数组内 → 隐藏;不在 → 显示。空号 / 未登录 / 请求失败 → 默认隐藏。 ## 问题背景与排查过程 > 以下为 Hook 实现层面的排查记录,供理解 UI 结构与注入细节;联调流程与接口日志见上文「使用」「验证与日志」。 ### 目标 用户要在 **通话中界面(InCallActivity)** 隐藏大号号码,AutoJS 实测控件为 `@id/name`(hex `0x7f0a029f`),同屏还有 `@id/location`、`@id/featuremode_call_info` 等,**只隐藏 name,不动其它控件**。 ### 走过的弯路 1. **误 Hook 其它字段** 早期按截图猜测,Hook 了 `mPrimaryNumber`、副号、Feature 模式多个字段,范围过大,容易误伤归属地、计时等 UI。 2. **只 Hook `setPrimaryName` 仍显示号码** 反编译确认号码确实写入 `mPrimaryName`(`@id/name`),但界面仍可见,原因包括: - 号码可能通过 `TextView.setText` 直接赋值,不经过 `setPrimaryName` - `updateElapsedTimeParams` / `updatePrimaryNameSize` 会把 `mPrimaryName` 设回 `VISIBLE` - Activity 生命周期切换后 View 状态被刷新 3. **同一资源 id 多处复用** `@id/name`(`0x7f0a029f`)在多个 layout 里出现(`primary_call_info`、`secondary_call_info`、`floating_incoming_card`,portrait/landscape 均有)。若只按 id 隐藏,会误伤副线卡片或旧版顶部浮窗。 4. **Pine Hook 注入成功但界面无效果** logcat 完全没有 `InCallUiHelper` 日志,说明运行的不是注入后的 APK。根因见下文「系统安装失效」。 5. **`TextView.setText` Hook 递归** Hook 内部调用 `setText("")` 清空文本时再次触发 Hook,logcat 出现大量 `Pine.handleCall` 栈。通过 `ThreadLocal` 重入保护解决。 6. **曾考虑改 smali** 在 `CallCardFragment.setPrimaryName` 末尾直接插 smali 指令,用户明确要求 **不改 smali,继续 Pine**。 ### 最终方案 纯 Pine Hook,多层兜底: | 层级 | Hook 点 | 作用 | |------|---------|------| | Fragment | `CallCardFragment.setPrimaryName` | 拦截参数 + 事后隐藏 | | Fragment | `updateElapsedTimeParams` / `updatePrimaryNameSize` / `onViewCreated` | 刷新后重新隐藏 | | Activity | `InCallActivity.onResume` / `showCallCard` | 从 Activity 扫描 `primary_call_info → name`,延迟多次重试 | | 全局 | `TextView.setText` | 拦截写入 `primary_call_info/@id/name` 的号码文本 | | Feature 模式 | `featuremode_name` / `featuremode_firstNameNumber` | ZTE Feature 模式下的同名控件 | 隐藏逻辑:内容为号码(`isNumber=true` 或 ≥7 位数字)时 `setText("")` + `setVisibility(GONE)`;联系人姓名保留。 ## 反编译后的 UI 层级理解 ``` InCallActivity (com.android.incallui.InCallActivity) └── CallCardFragment (com.android.incallui.CallCardFragment) ├── mPrimaryInfo → @id/primary_call_info (0x7f0a02f5) │ ├── @id/name → mPrimaryName (0x7f0a029f) ★ 目标:隐藏号码 │ ├── @id/location → mPrimaryLocation 保留 │ └── @id/elapsedTime → 通话计时 保留 ├── mSecondaryCallInfo → @id/secondaryCallInfo │ └── @id/name → 同 id,副线卡片 不处理 └── Feature 模式 └── mFeatureModeCallInfo → @id/featuremode_call_info (0x7f0a01c1) ├── @id/featuremode_name (0x7f0a01cd) └── @id/featuremode_firstNameNumber (0x7f0a01c7) ``` 关键 smali 逻辑(`CallCardFragment`): - `setPrimaryName(String name, boolean isNumber)`:`isNumber=true` 时把号码写入 `mPrimaryName` - `updateElapsedTimeParams()`:刷新计时后会 `mPrimaryName.setVisibility(VISIBLE)`,导致此前 GONE 被覆盖 layout 文件(`res/layout/primary_call_info.xml`)中 `@id/name` 即界面大号文字位;`@id/location` 在其下方显示标签/归属地。 **为何必须按父容器区分 `@id/name`:** | 父容器 | 资源 id | 是否处理 | |--------|---------|----------| | `primary_call_info` | `0x7f0a029f` | **隐藏号码** | | `secondaryCallInfo` | `0x7f0a029f` | 不处理 | | `floating_incoming_card` | `0x7f0a029f` | 不处理(v12 顶部浮窗用 `name_number`) | 实现上通过向上遍历 View 树,确认 ancestor 是否为 `primary_call_info`(`0x7f0a02f5`)。 ## 系统级安装「失效」的原因 把 patched APK push 到 `/system/priv-app/Dialer_MFV/Dialer_MFV.apk` 后,界面行为仍像原版,常见原因: 1. **`/data/app` update 层覆盖** 系统 priv-app 曾被 `adb install -r` 或 OTA 更新过,PackageManager 优先加载 `/data/app/~~xxx/com.android.dialer-xxx/base.apk`,而不是 `/system/priv-app/` 里的版本。`pm path com.android.dialer` 若首行是 `package:/data/app/...` 即属此情况。 2. **oat / dalvik 缓存** 旧 dex 编译产物缓存导致进程仍跑旧代码。需清理 `/system/priv-app/Dialer_MFV/oat`、`/data/dalvik-cache/*com.android.dialer*` 并 `cmd package compile --reset`。 3. **Dialer 是 persistent 进程** push 后必须 **reboot**,否则 InCallUI 进程不会重新加载 APK。 4. **root / remount 未成功** 部分 production build 默认 `adbd cannot run as root`;本机型可用 `adb root` + `adb remount`。若 `/system` 只读,push 会静默失败或写到错误位置。 `main.py` 安装阶段会自动:`pm uninstall` 去掉 update 层 → `pm install-existing` 恢复系统包 → 清缓存 → reboot → 打印 `codePath` 与 `PhoneHookProvider` 校验。 ## 注入架构 ``` InCallApp.attachBaseContext() └── HookBootstrap.init() ← smali 注入点 └── Pine HookEngine ├── InCallUiRegistry ← 通话页号码/Feature 模式隐藏 └── IncomingCallFloatRegistry ← 顶部来电浮窗「来电请接听」 ``` - Hook 代码以 smali 合并进 Dialer APK,native 库 `libpine.so` 从 hook APK 注入 - `PhoneHookProvider`(`initOrder=100`)保证 Application 之前完成 Hook 注册 - 重打包时用 `-r` 保留原始 `resources.arsc`,再从原始 APK 还原 `res/`,避免资源 id 漂移 - 隐藏决策统一走 `VertuContactsClient.isHide()`,不在电话进程内直连 HTTP ## 依赖 - Python 3.12(conda `py312`) - apktool、Android SDK build-tools(zipalign / apksigner) - JDK / keytool(环境变量或 PATH) - 原始 APK:命令行第一个参数(meta2 渠道 `com.android.dialer` 系统包) - **VertuContacts**(`com.vertu.vertucontacts`):已安装、已登录、系统 `com.android.contacts` 已禁用 - 签名(Dialer 注入默认):`assets/keystore/platform_都是android.jks`(alias `platform`,密码 `android`) - 签名(shared,其它模块可用):`assets/keystore/别名_shared_密码android.jks`(alias `shared`,密码 `android`) - 设备:`adb root` + `adb remount` 首次编译前需配置 Android SDK(`local.properties` 或环境变量 `ANDROID_HOME`)。