如何处理 AUDIOFOCUS_GAIN_TRANSIENT 和 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK?

解读

国内面试官问这道题,并不是想听你背官方文档,而是考察三件事:

  1. 是否真正在播放器、语音助手、车载导航等场景里踩过“抢焦点”的坑;
  2. 能否把“暂停/降低音量/恢复”做成可灰度、可配置、可监控的闭环;
  3. 是否了解 Android 10 以后“强制静音”与“隐私权限”对音频焦点的新限制。

一句话:要证明你既懂“规矩”(系统流程),也懂“变通”(业务体验)。

知识点

  1. 音频焦点三件套:请求(requestAudioFocus)、监听(AudioFocusCallback/Listener)、释放(abandonAudioFocus)。
  2. 8.0 以后必须用 AudioFocusRequest.Builder,显式传入 AudioAttributes 与 WillPauseWhenDucked 标志,否则系统直接拒掉。
  3. 两种瞬时焦点差异:
    • TRANSIENT:对方要求“独占”,你必须立即暂停,并在失焦结束(GAIN)后主动恢复;
    • TRANSIENT_MAY_DUCK:系统只让你“降低音量”,不应暂停,但国内 ROM(华米 OV)可能直接给你强制静音,需要兼容。
  4. duck 策略:推荐把音量压到 20 %~30 % 并关掉重低音,避免用户听不清导航提示;恢复时做 200 ms 淡入动画,防止“啪”一声。
  5. 生命周期联动:要在 onPause/onStop 里 abandon,防止应用退后台仍占焦点被系统拉黑名单;车载场景下还要监听 UXRestrictions(驾驶模式),禁止用户手动恢复。
  6. 灰度与监控:用 APM 埋点记录“请求→失焦→恢复”整条链路的耗时与失败码,国内厂商 ROM 经常返回 AUDIOFOCUS_REQUEST_FAILED(-1),需要降级到静音而不是崩溃。
  7. Android 12+ 新增“App 录音指示器”与“隐私看板”,若你的 duck 音量过低被用户投诉“偷录”,需要在设置里提供“忽略焦点”开关,否则应用商店审核会被卡。

答案

以在线音乐播放器为例,给出可直接落地的代码骨架与注意事项:

class PlayerRepository(
    private val ctx: Context,
    private val exoPlayer: ExoPlayer
) : AudioManager.OnAudioFocusChangeListener {

    private val audioManager = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
    private val focusLock = Any()
    private var playbackDelayed = false
    private var resumeOnFocusGain = false

    private val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
        .setAudioAttributes(
            AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                .build()
        )
        .setWillPauseWhenDucked(true)   // 8.0+ 必须显式声明
        .setOnAudioFocusChangeListener(this)
        .build()

    fun play() {
        synchronized(focusLock) {
            val ret = audioManager.requestAudioFocus(focusRequest)
            when (ret) {
                AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
                    resumeOnFocusGain = false
                    exoPlayer.playWhenReady = true
                }
                AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
                    playbackDelayed = true
                    resumeOnFocusGain = false
                }
                else -> {
                    // 国内 ROM 常见 -1,直接静音提示用户
                    exoPlayer.volume = 0f
                    Toast.makeText(ctx, "与其他应用冲突,已静音", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    override fun onAudioFocusChange(focusChange: Int) {
        synchronized(focusLock) {
            when (focusChange) {
                AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
                    exoPlayer.playWhenReady = false
                    resumeOnFocusGain = true
                }
                AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
                    // 降低音量,不暂停
                    exoPlayer.volume = 0.2f
                }
                AudioManager.AUDIOFOCUS_GAIN -> {
                    if (resumeOnFocusGain) {
                        exoPlayer.playWhenReady = true
                        resumeOnFocusGain = false
                    }
                    // 淡入恢复音量
                    ValueAnimator.ofFloat(exoPlayer.volume, 1f).apply {
                        duration = 200
                        addUpdateListener { exoPlayer.volume = it.animatedValue as Float }
                        start()
                    }
                }
                AudioManager.AUDIOFOCUS_LOSS -> {
                    abandonFocus()
                    exoPlayer.playWhenReady = false
                }
            }
        }
    }

    fun abandonFocus() {
        audioManager.abandonAudioFocusRequest(focusRequest)
    }
}

使用要点:

  1. 在 Activity/Service onPause() 里一定调用 abandonFocus(),否则后台被系统拉黑,下次请求直接失败。
  2. 车载、手表等低内存设备上,把“音量降低”阈值做成云端配置,防止 ROM 差异导致体验不一致。
  3. 若业务允许后台播放,需启动前台服务并挂“媒体样式”通知,否则 Android 12 会抛出 ForegroundServiceStartNotAllowedException。

拓展思考

  1. 多音频会话场景:如果 App 内同时存在音乐、音效、TTS 三条播放轨道,建议为每条轨道独立申请 AudioFocusRequest,并设置不同的 AudioAttributes.usage,让系统帮你做“分层 duck”。例如 TTS 用 USAGE_ASSISTANCE_NAVIGATION_GUIDANCE,系统会强制压低音乐而不压低通知铃音。
  2. 与通话状态耦合:国内运营商定制机常把电话状态广播延迟 500 ms,导致焦点回调与 TelephonyCallback 不同步。稳妥做法是同时监听 AudioManager.MODE_IN_CALL,一旦进入通话模式立即 abandon,防止“电话挂断后音乐外放”社死现场。
  3. 折叠屏/多窗口:在分屏或浮窗模式下,系统只对“可见窗口”授予焦点。若用户把视频播一半拖到副屏,主屏打开抖音,你的焦点会被抢走;此时应在 onWindowFocusChanged 里重新请求,否则用户回切后无声。
  4. 隐私合规:工信部 164 号文要求“录音前需弹窗明示”。虽然焦点本身不录音,但部分安全键盘、语音助手在拿到 TRANSIENT 焦点后会立刻开始录音。若你的应用被投诉“诱导授权”,需要在上报日志里区分“因焦点而录音”与“用户主动点击录音”,否则应用市场会被下架。

把以上细节讲透,面试官基本会认定你“真的在一线项目里修过音频焦点的坑”,而不是背八股文。