突破 Android 11~17 音频隔离:剖析 ShizuCallRecorder 的免 Root 通话录音、FakeContext 伪装与 AudioPolicy 环回架构

突破 Android 11~17 音频隔离:剖析 ShizuCallRecorder 的免 Root 通话录音、FakeContext 伪装与 AudioPolicy 环回架构

长期以来,在未解锁 Bootloader、未获取 Root 权限的现代 Android 设备上实现清晰的双向通话录音,几乎被开发者与极客圈公认为“不可能完成的任务”。从 Android 4.4 KitKat 到 Android 6 开始,Google 逐步收紧应用沙盒;到了 Android 9 与 10,官方更是全面封杀了第三方应用录制通话下行音频的公共通道。尽管 Android 11 预览版中曾短暂出现过系统权限 ACCESS_CALL_AUDIO,但随后被官方迅速雪藏。面对各手机厂商(OEM)在官方拨号器中严苛的地区限制、刺耳的高音量录音提示广播以及泛滥的后台遥测,普通用户在遭遇商务沟通纠纷或电信骚扰时,往往处于举证无门的被动窘境。

开源项目 ShizuCallRecorder(由开发者 kitsumed 主导)以一种极为硬核的系统级逆向工程思维打破了这一死局:它完全不需要修改内核或刷入 Magisk/KernelSU,而是巧借 Shizuku 桥接的 ADB Shell(UID 2000)原生特权,通过内存级 FakeContext 欺骗系统运行环境、瞬时拉起系统 Activity 绕过 Android 11 的前台校验、跨代反射 JNI native_setup 修复国产 ROM 崩溃,并结合底层 AudioPolicy 环回渲染机制攻克了长期困扰社区的 VoIP 通话录制难题。在这篇文章中,我将深入其底层源码,全景拆解其双轨音频捕获管线、实时 DSP 混音与零 IPC 硬件编码架构。


0. 实机架构与捕获演示视频

在展开深奥的 AOSP 底层源码与反射逻辑之前,我先放上在真机环境下的功能演示与架构流转视频。视频直观展现了从沙盒权限拦截、Shizuku 特权进程接管、双轨音频频域分离到实时 DSP 降噪落盘的全生命周期:

通过下面这张全局拓扑图,我们可以更清晰地看清 ShizuCallRecorder 是如何将“宿主应用层”、“ADB Shell 特权守护层”与“Android 内核音频栈”三者精密咬合在一起的:

flowchart TD
    subgraph UserSpaceApp ["宿主用户层应用 (com.kitsumed.shizucallrecorder, UID 10xxx)"]
        UI["Compose 交互界面 / 过滤规则配置"]
        Detector["通话状态监听 (InCallService / PhoneStateReceiver)"]
        SAF["存储访问框架 (SAF / ParcelFileDescriptor)"]
        Decision["RecordingDecisionEngine 决策引擎"]
        UI --> Decision
        Detector --> Decision
        Decision -->|提供只写 fd 与参数| BindService["Shizuku.bindUserService()"]
    end

    subgraph PrivilegedDaemon ["Shizuku 特权服务进程 (app_process, UID 2000)"]
        ShellService["ShellService (IShellService.Stub)"]
        FakeCtx["FakeContext (伪装 com.android.shell)"]
        Workarounds["ShellWorkarounds (修补 ActivityThread 与 AppInfo)"]
        Engine["ShellRecordingEngine 音频核心引擎"]
        DSP["PcmVadDuckingProcessor (VAD + AGC + 软削波)"]
        Encoder["AudioEncoder (硬件 MediaCodec + MediaMuxer)"]

        ShellService --> FakeCtx & Workarounds
        ShellService --> Engine
        Engine --> DSP --> Encoder
    end

    subgraph AndroidAudioHAL ["AOSP 底层系统与硬件音频栈"]
        VoLTE["蜂窝通信 / 基带调制解调器"]
        AudioFlinger["AudioFlinger 系统音频混音服务"]
        AudioPolicy["AudioPolicyManager 路由引擎"]
        
        Capture1["AudioRecord (VOICE_CALL / VOICE_DOWNLINK)"]
        Capture2["AudioMixingRule (ROUTE_FLAG_LOOP_BACK_RENDER)"]
        
        VoLTE --> Capture1
        AudioFlinger --> Capture2
        AudioPolicy --> Capture1 & Capture2
    end

    BindService -->|跨进程 AIDL 调度| ShellService
    Capture1 -->|蜂窝链路| Engine
    Capture2 -->|VoIP 环回| Engine
    SAF -. 句柄透传 .-> Encoder
    Encoder -->|直写目标文件| Storage[(".ogg / .m4a 物理存储")]

    style UserSpaceApp fill:#1e293b,stroke:#3b82f6,stroke-width:2px,color:#fff
    style PrivilegedDaemon fill:#0f2e24,stroke:#10b981,stroke-width:2px,color:#fff
    style AndroidAudioHAL fill:#3b1e2b,stroke:#f43f5e,stroke-width:2px,color:#fff

1. 痛点根源:Android 系统音频隔离的三重铁幕

要想真正理解 ShizuCallRecorder 的巧妙之处,必须先看清普通第三方应用在通话录音领域遭遇的“系统级死锁”:

1.1 系统签名权限的不可逾越壁垒

在 Android 架构设计中,想要捕获全局系统扬声器输出或电话线路下行音频,核心必须持有声明在 frameworks/base/core/res/AndroidManifest.xml 中的核心特权:

  • android.permission.CAPTURE_AUDIO_OUTPUT
  • android.permission.MODIFY_AUDIO_ROUTINGS

这两个权限的保护级别均为 signature|privileged。这意味着除了预装在系统分区(/system/priv-app)并持有厂商私钥签名的官方应用外,任何从应用商店下载的普通第三方应用(UID 落在 10000~19999)只要在 Manifest 中声明并尝试使用,底层 AudioPolicyManager 都会在 native C++ 层直接抛出 SecurityException,断开连接。

1.2 并发录音的“静音置零”惩罚(Client Silenced)

很多开发者曾尝试使用公共 API 的 MediaRecorder.AudioSource.VOICE_COMMUNICATION 或 MIC 在通话时强行录音。然而从 Android 10 起,系统严格推行“并发音频捕获限制”(Concurrent Audio Restrictions)。

当系统电话(Telecom)激活时,电话信道享有最高音频优先级。此时任何普通应用若发起麦克风录制,AOSP 音频服务并不会直接让其崩溃,而是采取了极具迷惑性的“静音惩罚”:

// AOSP frameworks/av/services/audiopolicy/service/AudioPolicyInterfaceImpl.cpp
if (!isPrivilegedOrSystemApp(clientUid)) {
    client->setSilenced(true); // 将数据缓冲区直接刷为全 0
}

此时你的应用虽然看起来录音状态正常,读出来的 PCM 字节却全是冷酷的纯平静音(Silence)。

1.3 VoIP 软件流与物理信道的架构割裂

传统蜂窝电话(Cellular / VoLTE)通常走基带硬件 DSP 的专用音频混音总线,而微信、WhatsApp、Telegram、Skype 等网络通话则完全走用户态软件协议栈,将音频作为常规音频流(AudioTrack)抛给系统的 AudioFlinger 混音。

由于信道异构,即使开发者设法录到了本地麦克风的声音,对方通过扬声器传过来的声音在未经特殊路由拦截的情况下,根本无法回流到任何合法的物理输入流中。


2. 突破点:ADB Shell(UID 2000)的特权借道与环境伪装

既然常规应用沙盒是死局,那么破解的唯一钥匙就是:跳出 UID 10xxx 的牢笼,进入持有合法特权的命名空间。

2.1 被遗忘的特权金矿:com.android.shell

大多数人仅将 ADB 视为调试命令工具,却忽略了 Android 系统为 ADB Shell 进程(UID 2000)赋予的庞大特权。查看 AOSP 源码中的 packages/Shell/AndroidManifest.xml,你会发现其内置了几乎所有与系统维护相关的顶层权限:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.shell">
    <uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTINGS" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.MANAGE_ONGOING_CALLS" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
</manifest>

只要程序能够以 UID 2000 的身份启动,它就天生合法拥有规避并发录音惩罚与捕获系统音频输出的全部凭据!这正是 Shizuku 框架所提供的核心能力——通过 app_process 动态派生出具备 Shell UID 的独立宿主进程。

2.2 伪装艺术:FakeContext 的系统欺骗

然而,在 UID 2000 的独立进程中编写代码绝非易事。传统的 Android Java 组件必须依赖 Context。当你的服务代码由 app_process 拉起时,当前进程中既没有 Application 对象,也没有被初始化的 ActivityThread。

如果你直接调用 Android Framework API,系统内部会调用 context.getOpPackageName() 或 context.getAttributionSource() 进行鉴权:

  1. 若传入主应用的 Context,底层 Binder 会发现当前的物理进程是 UID 2000,而传入的包名属于 UID 10176,直接触发身份不符异常;
  2. 若通过反射调用 ActivityThread.systemMain().getSystemContext(),其返回的包名固定为 "android"(UID 1000),依然与当前的 UID 2000 发生冲突!

ShizuCallRecorder 设计了教科书级别的 FakeContext.kt:

@SuppressLint("PrivateApi", "DiscouragedPrivateApi")
class FakeContext private constructor() : ContextWrapper(ShellWorkarounds.getSystemContext()) {

    companion object {
        const val PACKAGE_NAME = "com.android.shell"
        val instance: FakeContext by lazy { FakeContext() }
        fun get(): FakeContext = instance
    }

    override fun getPackageName(): String = PACKAGE_NAME
    override fun getOpPackageName(): String = PACKAGE_NAME
    override fun getBasePackageName(): String = PACKAGE_NAME

    // 在 Android 12+ (API 31+) 中精准注入 AttributionSource
    @RequiresApi(Build.VERSION_CODES.S)
    override fun getAttributionSource(): AttributionSource {
        return AttributionSource.Builder(Process.SHELL_UID)
            .setPackageName(PACKAGE_NAME)
            .build()
    }
}

不仅如此,由于某些系统服务会隐式调用 ContentResolver 去读取联系人或设置项,FakeContext 还在内部重写了 acquireProvider 逻辑,利用隐藏的 ActivityManager.getContentProviderExternal 动态拿到系统级的 IContentProvider 代理,完美伪装成一个五脏俱全的原生系统 Shell 应用。

2.3 消除 Binder 身份穿透坑(clearCallingIdentity)

当主应用通过 AIDL 跨进程调用 IShellService 时,Binder 驱动默认会保留调用方(主 App,UID 10xxx)的 CallingUid 与 CallingPid。

此时如果直接在方法内创建 AudioRecord,AOSP 底层的 AudioPolicyService 在做二次校验时会打印出致命警告:

"createRecord uid 2000 tried to pass itself off as 10176"

在 ShellService.kt 中,所有特权方法都被置于一个极其严谨的内联包装器中:

private inline fun <T> clearIdentity(block: () -> T): T {
    val identityToken = Binder.clearCallingIdentity()
    val workSourceToken = Binder.clearCallingWorkSource()
    try {
        return block()
    } finally {
        Binder.restoreCallingWorkSource(workSourceToken)
        Binder.restoreCallingIdentity(identityToken)
    }
}

通过 Binder.clearCallingIdentity(),当前工作线程的权限上下文被彻底还原为纯粹的 UID 2000,扫平了进入底层音频服务前的最后一道关卡。


3. 攻克代际封锁:Android 11 前台弹射与跨代 JNI 反射

解决了身份问题,在具体机型和各 Android 大版本落地时,系统又布下了层层暗桩。

3.1 Android 11 的“可见前台”存活铁律与弹射绕过

在 Android 11(API 30)中,Google 对麦克风权限执行了一条铁律:捕获音频的 UID 必须在屏幕上拥有一个可见的前台 Activity,否则视为恶意后台偷听,底层直接抛出 UnsupportedOperationException。

对于一个常驻在后台的 ADB Shell 守护进程而言,这原本是死局。但在 AudioRecordCapture.kt 中,作者运用了极具极客色彩的“弹射战术”:

private fun startWorkaroundAndroid11() {
    try {
        val intent = Intent(Intent.ACTION_MAIN).apply {
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            addCategory(Intent.CATEGORY_LAUNCHER)
            component = ComponentName(
                FakeContext.PACKAGE_NAME,
                "com.android.shell.HeapDumpActivity" // Shell 自带的合法 Activity
            )
        }
        ActivityManager.startActivity(intent)
        // 睡眠 150ms 等待窗口管理器完成焦点感知
        SystemClock.sleep(150)
    } catch (e: Exception) {
        AppLogger.e("Failed to launch Android 11 foreground workaround", e)
    }
}
  1. 利用 UID 2000 权限,瞬时拉起 Shell 包自带的合法系统组件 com.android.shell.HeapDumpActivity;
  2. 窗口管理器(WMS)与音频管理器(AMS)立即感知到 UID 2000 已经处于前台活跃状态;
  3. 紧接着立刻调用 AudioRecord.startRecording() 建立合法的原生音频流;
  4. 捕获通道成功开启后,立即调用 ActivityManager.forceStopPackage("com.android.shell") 关掉该 Activity。

由于 AOSP 内部对已初始化的活动 Session 具有会话保持机制,后续的音频读取将平稳持续下去,整个过程用户甚至还未看清屏幕变动,权限拦截已被穿透!

3.2 绕过厂商魔改崩溃:跨代 JNI native_setup 手动装配

在 Vivo(FuntouchOS / OriginOS)等国产厂商定制机上,厂商深度修改了 AOSP 的音频框架,直接使用标准 AudioRecord.Builder 构造对象时,会因为某些未注入的专有 Binder Token 频繁触发 NullPointerException。

为了彻底摆脱厂商对 Java 层 Builder 的魔改污染,ShizuCallRecorder 在 ShellWorkarounds.kt 中直接绕过了所有上层逻辑,直接反射调用底层的 C++ JNI 函数 native_setup!

为了支持从 Android 11 到最新的 Android 17 R1,作者根据 AOSP 源码的演进历史,精确还原了不同代际 JNI 方法的参数签名差异:

// Android 14~16 (API 34~36 Baklava) 的 native_setup 签名映射
val nativeSetupMethod = audioRecordClass.getDeclaredMethod(
    "native_setup",
    Any::class.java,               // WeakReference<AudioRecord>
    Any::class.java,               // AudioAttributes
    IntArray::class.java,          // sampleRate
    Int::class.javaPrimitiveType,  // channelMask
    Int::class.javaPrimitiveType,  // channelIndexMask
    Int::class.javaPrimitiveType,  // audioFormat
    Int::class.javaPrimitiveType,  // bufferSizeInBytes
    IntArray::class.java,          // sessionId
    Parcel::class.java,            // attributionSourceParcel
    Long::class.javaPrimitiveType, // nativeRecordInJavaObj
    Int::class.javaPrimitiveType,  // maxSharedAudioHistoryMs
    Int::class.javaPrimitiveType   // halOpResult
).apply { isAccessible = true }

这种直接在 JNI 边界手动序列化 AttributionSourceParcel 并完成 native 实例绑定的能力,让该应用具备了极强的跨设备与跨 ROM 鲁棒性。


4. 双轨捕获管线:传统蜂窝直通与 VoIP 环回渲染

在解决了权限与环境之后,如何把声音清晰录下来?不同类型的电话走的是完全不同的技术路线:

flowchart LR
    CallType{"通话协议识别<br/>(AudioModeResolver)"}

    subgraph CellularMode ["传统蜂窝 / VoLTE 直通信道"]
        direction TB
        C_In["AudioRecord (VOICE_CALL / VOICE_UPLINK)<br/>麦克风上行"]
        C_Out["AudioRecord (VOICE_DOWNLINK)<br/>基带硬件混音下行"]
    end

    subgraph VoIpMode ["网络通话 / VoIP 环回信道 (Android 14+)"]
        direction TB
        V_In["AudioRecord (VOICE_COMMUNICATION)<br/>本地人声捕获"]
        V_Out["AudioMixingRule + ROUTE_FLAG_LOOP_BACK_RENDER<br/>AudioPolicyManager 虚拟槽直截"]
    end

    CallType -->|MODE_IN_CALL| CellularMode
    CallType -->|MODE_IN_COMMUNICATION| VoIpMode

4.1 传统蜂窝 / VoLTE 捕获:双信道硬件直通

对于标准的 SIM 卡运营商通话(2G/3G/4G/5G VoLTE),声音直接经由手机 Baseband 调制解调器进行物理流控。

RecordingPlanResolver 会分配两个并行的原生捕获实例:

  • Primary Capture:配置为 MediaRecorder.AudioSource.VOICE_UPLINK(或 VOICE_CALL),负责纯净采集机主自身的麦克风语音;
  • Secondary Capture:配置为 MediaRecorder.AudioSource.VOICE_DOWNLINK,直接截取硬件基带解码后送往听筒的远端声音。

由于两路捕获全部走的是硬件总线分流,没有任何第三方软件在中间插手,底噪极低且延迟仅有 20ms。

4.2 VoIP 与 Wi-Fi Calling:解密低层 AudioMixingRule 环回截流

微信通话或 WhatsApp 网络电话不经过基带下行,如果使用 VOICE_DOWNLINK 会录到一片空白。Android 10 虽然推出了公共的“音频回放捕获”(Audio Playback Capture)API,但它要求应用申请危险的 MediaProjection(并在屏幕上弹出醒目的录屏警告弹窗),且大量第三方社交 App 都在 Manifest 中设置了 android:allowAudioPlaybackCapture="false" 主动屏蔽录制。

ShizuCallRecorder 在 AudioPlaybackCapture.kt 中展现了其最具技术含量的神操作——利用 Shell 权限直接穿透 AOSP 底层的 AudioPolicy 引擎:

  1. 构造私有混音规则(AudioMixingRule):
    通过反射直接实例化隐藏类 android.media.audiopolicy.AudioMixingRule,添加匹配规则:
    • 匹配使用模式:AudioAttributes.USAGE_VOICE_COMMUNICATION
    • 匹配流类型:AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING
  2. 激活环回且同时扬声器渲染(关键黑科技):
    在公共 API 中,一旦创建环回流,声音往往会被劫持而导致扬声器静音。ShizuCallRecorder 读取了 AudioMix 类中隐藏的常量字段:
    val routeFlags = audioMixClass.getField("ROUTE_FLAG_LOOP_BACK_RENDER").getInt(null)
    audioMixBuilder.setRouteFlags(routeFlags)
    
    这个标志告诉 AOSP 混音器:既要把 PCM 数据拷贝一份送入环回接收槽,又要确保扬声器/听筒继续正常物理发声!
  3. 静态注入策略管理器:
    反射调用 AudioManager.registerAudioPolicyStatic(audioPolicy),并立即从 audioPolicy.createAudioRecordSink(audioMix) 中拿到合法的 AudioRecord 接收端。

这一套连招下来,不仅完全不需要用户点击烦人的屏幕捕获确认弹窗,更强行无视了任何第三方应用在 Manifest 中设置的禁止录制标记!


5. 实时 DSP、双声道分离与零 IPC 硬件编码

截取到了两路原始 PCM16 数据流之后,如何将其高保真、低开销地封装存储?

5.1 专业级音频 DSP 处理流(PcmVadDuckingProcessor)

通话过程中最忌讳的是我方说话时被对方的声音覆盖,或者两路音量相差悬殊。该项目实现了一个高度工程化的 DSP 处理管道:

flowchart LR
    subgraph StreamProcess ["双路并行 DSP 管道 (16kHz PCM16)"]
        RawPCM["原始 PCM 块 (20ms / 320 采样点)"] --> RMS["RMS 实时能量积分"]
        RMS --> VAD{"自适应 VAD 判定<br/>(SNR > 3.5dB)"}
        VAD -->|对方说话| Ducker["动态避让衰减 (-7.0 dB Ducking)"]
        VAD -->|常态| AGC["自动增益补偿 (-8dB ~ +18dB)"]
        Ducker & AGC --> Limiter["瞬态软削波限幅 (Soft Clipping)"]
    end

    Limiter --> SplitMode{"声道合并策略"}
    SplitMode -->|开启双声道分离| Interleave["左右声道物理交织<br/>(L: 我方, R: 对方)"]
    SplitMode -->|单声道混合| SoftMix["非线性软削波混音公式"]
  • 动态增益补偿(AGC):将目标能量恒定在 -22.3 dBFS(RMS 2500),遇到低声耳语时提供高达 +18 dB 的线性增益,遇到大声喧哗时提供 -8 dB 的压制;
  • 声控避让(Ducking):当检测到对端正在高能量输出时,自动将本地麦克风的环境背景音按 20ms 攻击、400ms 保持、200ms 释放的物理曲线平滑下压 -7.0 dB,杜绝回声干扰;
  • 双声道立体声隔离(Stereo Separation):
    在 PcmProcessor.kt 中,两路单声道 PCM 数据被以极高的性能直接在直接缓冲区(Direct ByteBuffer)中交织拼装:左声道为本地机主声音,右声道为远端通话人声音。这为后期使用 AI 听写转录、角色分离声纹识别提供了近乎完美的物理素材;
  • 防爆音非线性软削波(Soft Clipping):
    若用户选择混合为单声道,传统直接相加除以二会导致音量减半,直接相加又极易在峰值处发生硬削波(Hard Clipping)破音。作者采用了一套优美的非线性平滑公式:
    val mixed = if (primarySample < 0 && secondarySample < 0) {
        (primarySample + secondarySample) - ((primarySample * secondarySample) / Short.MIN_VALUE.toInt())
    } else if (primarySample > 0 && secondarySample > 0) {
        (primarySample + secondarySample) - ((primarySample * secondarySample) / Short.MAX_VALUE.toInt())
    } else {
        primarySample + secondarySample
    }
    

5.2 零 IPC 拷贝性能优化:MediaCodec 与文件句柄直写

在架构设计上,最容易掉入的性能陷阱是:在 Shell 进程中捕获音频,然后通过 Binder IPC 把海量的 PCM 流传回主应用进行编码。在 16kHz 立体声下,每秒高达数十 KB 的 Binder 数据往返不仅会迅速击穿系统的 Binder 共享内存池,还会导致 CPU 占用急剧飙升、设备严重发烫。

ShizuCallRecorder 的架构极其克制:

  1. 主应用仅负责“建文件”:通过 Android 原生 Storage Access Framework(SAF),在用户指定的存储目录下创建一个空的待录音频文件,拿到只写文件描述符 ParcelFileDescriptor;
  2. 句柄跨进程单次透传:通过 AIDL 的 startRecording(outputFile, plan, callback) 方法,把这个文件句柄一次性交给 ShellService;
  3. Shell 内部闭环硬件编码与落盘:
    在 Shell 进程内,直接通过系统 MediaCodec 驱动硬件编解码器(支持 Opus、AAC、HE-AAC),并由 MediaMuxer 直接将编码后的音轨数据帧灌入 outputFile.fileDescriptor。

整场通话下来,进程间除了偶尔汇报状态变动的几个轻量 Binder 事务外,没有任何原始音频数据发生跨进程拷贝,整套服务的 CPU 占用率常年维持在 1% 以下,近乎零功耗运行!


6. 通话状态机与隐私闭环设计

在通话检测与自动化层面,项目并没有采用简单粗暴的后台死循环轮询,而是根据系统版本动态分流:

flowchart TD
    Init["应用启动 / 系统就绪"] --> CheckSDK{"系统版本检测"}
    CheckSDK -->|Android 12+| InCall["动态注册 InCallService<br/>(系统原生 Telecom 事件回调)"]
    CheckSDK -->|Android 11 / 特殊 ROM| PhoneState["动态注册 PhoneStateReceiver<br/>(ACTION_PHONE_STATE_CHANGED 广播)"]

    InCall --> StateEngine["RecordingDecisionEngine 决策引擎"]
    PhoneState --> StateEngine

    StateEngine --> RuleFilter{"白名单与隐私过滤"}
    RuleFilter -->|号码属于排除联系人| Ignore["忽略并丢弃"]
    RuleFilter -->|匿名号码且开启忽略| Ignore
    RuleFilter -->|命中录音规则| StartShell["拉起 ShellService 启动录音"]

    StartShell --> OnGoing["通话持续进行中..."]
    OnGoing --> CallEnd["监听到通话挂断事件"]
    CallEnd --> StopRecord["停止录音 + MediaCodec 写入 EOS + 关闭句柄"]
    StopRecord --> SecOpt{"安全策略配置"}
    SecOpt -->|开启自动防御| KillShizuku["主动向 Shizuku 发送退出 Intent,切断无线调试"]
    SecOpt -->|常驻模式| Keep["保持待命"]
  • Android 12+ 优先绑定 InCallService:深度接入 Android Telecom 框架,能够在通话响铃、接通、保持的毫秒级瞬间拿到原生回调与结构化数据;
  • 旧系统兼容双广播机制(Double Broadcast):针对旧系统,PhoneStateReceiver 精确处理了 ACTION_PHONE_STATE_CHANGED 第一次广播号码为空、第二次广播才携带真实号码的时序竞态问题;
  • 主动式安全防御开关:许多安全敏感型用户(或特定银行类 App)对常开“无线调试”或 Shizuku 极为反感。应用内置了安全联动机制,当录音结束且闲置时,可主动向 Shizuku 发送挂断信号甚至关闭无线调试,最大限度收敛系统的攻击面。

7. 结语:开源工程与用户主权的极致博弈

回顾 Android 系统演进史,Google 收紧应用沙盒与音频权限的初衷是出于保护大众免受恶意流氓软件的暗中窥探。然而,“一刀切”地全面封杀通话录音,其客观后果往往是将守法用户推向更加危险的深渊——为了自卫维权,许多非专业用户被迫在网上搜寻来路不明的 Root 提权脚本或刷入充斥后门的修改版 ROM。

ShizuCallRecorder 为整个移动开源社区树立了一个教科书级的工程典范:

  1. 不破坏安全边界:不需要通过破坏 Linux 安全沙盒(Root)来实现功能,所有操作严格锚定在系统原本为调试设计的 ADB Shell 白名单权限之内;
  2. 高超的逆向工程还原度:从 FakeContext 伪装、ActivityThread 内存修补、JNI native_setup 跨代反射,到低层 AudioMixingRule 环回渲染,处处展现了对 AOSP 内部运行机制的高超解构;
  3. 克制高效的架构美学:全链路硬件编解码、零 IPC 内存拷贝与严格的生命周期管理,打破了“特权应用必定臃肿卡顿”的固有偏见。

它不仅是一款功能完备的通话录音工具,更是一面技术旗帜——证明了在封闭化浪潮日益加剧的现代移动操作系统中,真正的开源黑客依然能够凭借智慧与工程执念,为用户守住属于自己的数字主权。


原文链接与参考资料