Android车载开发启示录(三)

1,670 阅读10分钟

笔者在从事Android车载行业的开发过程中,发现Android车载开发和平时的Android开发还是有很大不同之处,对于一个小白来说或者说如果是刚入行的新人都会很陌生,目前市场也没有很多系统性的知识提供给大家。

所以笔者准备通过一个专栏系列,把自己在车载开发过程中的学习记录和开发经验记录下来并分享出来,希望能给大家带来一些帮助。

在第一篇内容,笔者介绍了Android车载操作系统现状、整个操作系统架构和架构下核心概念:

Android车载开发启示录(一)

第二篇内容,笔者介绍了Android Automotive操作系统中的一个关键组件CarFramework

Android车载开发启示录(二)

接下来,笔者会先介绍下Android音频焦点和AAOS音频焦点相关的内容

主要内容如下:

  • Android音频焦点
  • AAOS的音频焦点

Android音频焦点

在Android系统实际应用中,经常会出现两个应用同时播放声音的场景,此时就需要一种策略来决定如何播放声音。

当两个或两个以上的Android应用同时向同一输出流播放音频时,系统会将所有音频流混合在一起。虽然从技术上感觉很牛逼,但实际使用场景会给用户体验是不好的。那么为了避免所有音乐应用同时播放,Android 引入了“音频焦点”的概念,就是说某一时刻只能有一个应用获得音频焦点。

那么当某个应用需要输出音频时,它需要请求获得音频焦点,获得焦点后,就可以播放声音了。 不过,在获得音频焦点后,可能无法一直持有这个焦点到播放完成,这是因为其他应用在这个过程中也是可以请求焦点,从而占有当前的音频焦点。

如果发生这种抢占焦点的情况,应该将当前应用暂停播放或降低音量,从而让用户听到新的音频源。

所以音频焦点这个测量是一种合作模式。应用是需要遵守音频焦点准则,但系统是不会强制执行这些准则的。 比如说,如果应用想要在失去音频焦点后继续大声播放,系统也不会阻止它。但从用户使用角度来说,这是一种极其难受的体验,那么用户很可能会卸载具有这种影响体验的应用。

行为恰当的音频应用应根据以下一般准则来管理音频焦点:

  • 在即将开始播放之前调用 requestAudioFocus(),并验证调用是否返回 AUDIOFOCUS_REQUEST_GRANTED,比如可以在Media会话的 onPlay() 回调中调用 requestAudioFocus()
  • 在其他应用获得音频焦点时,停止或暂停播放,或降低音量。
  • 播放停止后,放弃音频焦点。

请求和放弃焦点

Android 8.0之前,调用requestAudioFocus() 请求焦点时,需要设置一个焦点变化的监听器 AudioManager.OnAudioFocusChangeListener。该监听器可以在媒体会话所在的 Activity 或服务中创建。当其他应用获取或放弃音频焦点时就可以在注册的监听器中收到onAudioFocusChange() 回调。

AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);  
AudioManager.OnAudioFocusChangeListener afChangeListener;  
  
// Request audio focus for playback  
int result = audioManager.requestAudioFocus(afChangeListener,  
        // Use the music stream.  
        AudioManager.STREAM_MUSIC,  
        // Request permanent focus.  
        AudioManager.AUDIOFOCUS_GAIN);

但是从 Android 8.0(API 级别 26)开始,当您调用 requestAudioFocus()时,需要加上AudioFocusRequest 参数。

可以通过使用 AudioFocusRequest.Builder 构建 AudioFocusRequest 来请求和放弃音频焦点, 由于焦点请求始终必须指定请求的类型,此类型会包含在构建器的构造函数中。使用构建器的方法来设置请求的其他字段。代码示例如下:

AudioManager audioManager = (AudioManager) Context.getSystemService(Context.AUDIO_SERVICE);  
AudioAttributes playbackAttributes = new AudioAttributes.Builder()  
        .setUsage(AudioAttributes.USAGE_GAME)  
        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)  
        .build();  
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)  
        .setAudioAttributes(playbackAttributes)  
        .setAcceptsDelayedFocusGain(true)  
        .setOnAudioFocusChangeListener(afChangeListener, handler)  
        .build();  
final Object focusLock = new Object();  
  
boolean playbackDelayed = false;  
boolean playbackNowAuthorized = false;  
  
int res = audioManager.requestAudioFocus(focusRequest);  
synchronized(focusLock)  {  
    if (res == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {  
        playbackNowAuthorized = false;  
    } else if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {  
        playbackNowAuthorized = true;  
        playbackNow();  
    } else if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {  
        playbackDelayed = true;  
        playbackNowAuthorized = false;  
    }  
}  
  
@Override  
public void onAudioFocusChange(int focusChange) {  
    switch (focusChange) {  
        case AudioManager.AUDIOFOCUS_GAIN:  
            if (playbackDelayed || resumeOnFocusGain) {  
                synchronized (focusLock) {  
                    playbackDelayed = false;  
                    resumeOnFocusGain = false;  
                }  
                playbackNow();  
            }  
            break;  
        case AudioManager.AUDIOFOCUS_LOSS:  
            synchronized (focusLock) {  
                resumeOnFocusGain = false;  
                playbackDelayed = false;  
            }  
            pausePlayback();  
            break;        
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:  
            synchronized (focusLock) {  
                resumeOnFocusGain = true;  
                playbackDelayed = false;  
            }  
            pausePlayback();  
            break;        
        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:  
            // ... pausing or ducking depends on your app  
            break;  
    }  
}

在 Android 8.0(API 级别 26)中,当其他应用使用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 请求焦点时,系统可以在不调用应用的 onAudioFocusChange() 回调的情况下降低和恢复音量。

自动降低音量的行为对于音乐和视频播放应用来说是可接受的,但在播放语音内容时(例如在听书应用中)就没什么用处了。在这种情况下,应用可以选择直接暂停播放。

如果应用在被要求降低音量时暂停播放,还是要创建包含 onAudioFocusChange() 回调方法的 OnAudioFocusChangeListener,在该回调方法里去实现所需的暂停播放/恢复播放的操作。 调用 setOnAudioFocusChangeListener()来注册监听器,然后调用 setWillPauseWhenDucked(true) 告诉系统使用您的回调,而不是执行自动降低音量。

放弃焦点

调用 abandonAudioFocus(listener)可以放弃焦点

audioManager.abandonAudioFocus(afChangeListener);

在Android8.0之后,调用 abandonAudioFocusRequest() 释放音频焦点时,需要添加上AudioFocusRequest 作为参数。需要注意的是,在请求和放弃焦点时,要使用相同的 AudioFocusRequest 实例。

延迟获取焦点

在有些情况下,因为焦点被其他应用“锁定”了,系统不能批准对音频焦点的请求,例如在通话过程中。在这种情况下,requestAudioFocus() 会返回 AUDIOFOCUS_REQUEST_FAILED。在这种情况下,由于未获得焦点,应用是不会播放音频的。

通过 setAcceptsDelayedFocusGain(true)可让应用异步处理焦点请求。设置这个标记后,在焦点锁定时发出的请求会返回 AUDIOFOCUS_REQUEST_DELAYED。当锁定音频焦点的情况不再存在时(例如当通话结束时),系统会批准待处理的焦点请求,并调用 onAudioFocusChange() 来通知您的应用。

为了处理“延迟获取焦点”,也是需要通过调用 setOnAudioFocusChangeListener()来实现所需行为并注册监听器。在OnAudioFocusChangeListener监听器的 onAudioFocusChange() 回调方法的 `进行处理。

响应音频焦点变化

当应用获得音频焦点后,它必须能够在其他应用为自己请求音频焦点时释放该焦点。出现这种情况时,应用就会收到对 AudioFocusChangeListener 中的 onAudioFocusChange() 方法的调用,在 onAudioFocusChange()回调中的 focusChange 参数表示所发生的更改类型

AudioManager.OnAudioFocusChangeListener afChangeListener =  
        new AudioManager.OnAudioFocusChangeListener() {  
            public void onAudioFocusChange(int focusChange) {  
                if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {  
                    // Permanent loss of audio focus  
                    // Pause playback immediately                } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {  
                    // Pause playback  
                } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {  
                    // Lower the volume, keep playing  
                } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {  
                    // Your app has been granted audio focus again  
                    // Raise volume to normal, restart playback if necessary                }  
            }  
        };

暂时性失去焦点

如果焦点更改是暂时性的(AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 或 AUDIOFOCUS_LOSS_TRANSIENT),应用一般要降低音量或暂停播放,否则保持相同的状态。

在暂时性失去音频焦点时,您应该继续监控音频焦点的变化,并准备好在重新获得焦点后恢复正常播放。当抢占焦点的应用放弃焦点时,您会收到一个回调 (AUDIOFOCUS_GAIN)。此时,您可以将音量恢复到正常水平或重新开始播放。

永久性失去焦点

如果是永久性失去音频焦点 (AUDIOFOCUS_LOSS),则其他应用会播放音频。您的应用应立即暂停播放,因为它不会收到 AUDIOFOCUS_GAIN 回调。所以用户必须执行明确的操作,重新开始播放的。

AAOS的音频焦点

交互类型

为了支持 AAOS,音频焦点请求是根据请求的CarAudioContext和当前焦点持有者之间的预定义交互来处理的。交互有三种类型:

1.独占交互

这是 Android 最常用的交互模型。

在独占交互中,一次只允许一个应用程序保持焦点。因此,传入的焦点请求将被授予焦点,而现有的焦点持有者将失去焦点。由于两个应用程序都播放媒体,因此只允许一个应用程序保持焦点。因此,新启动的应用程序的焦点请求将返回AUDIOFOCUS_REQUEST_GRANTED ,而当前播放音乐的应用程序会收到焦点更改事件,其状态为与所发出的请求类型相对应的丢失状态。

2.拒绝交互

对于拒绝交互,传入的请求始终被拒绝。例如,在通话过程中尝试播放音乐时。在这种情况下,如果拨号器保持呼叫的音频焦点,并且第二个应用程序请求焦点来播放音乐,则音乐应用程序会收到AUDIOFOCUS_REQUEST_FAILED作为对请求的响应。由于焦点请求被拒绝,因此不会将焦点丢失分派给当前焦点持有者。

3.并发交互

AAOS 的独特之处在于并发交互。这使得请求车内音频焦点的应用程序能够与其他应用程序同时保持焦点。当然如果要发生并发交互,是需要满足以下条件的:

  • 传入的焦点请求必须请求AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
  • 当前焦点持有者未setPauseWhenDucked(true)
  • 当前焦点持有者选择不接收鸭子事件

如果满足这些条件,焦点请求会返回AUDIOFOCUS_REQUEST_GRANTED ,而当前焦点持有者的焦点没有变化。但是,如果当前焦点持有者选择接收鸭子事件或在鸭子被鸭子时暂停,则当前焦点持有者将失去焦点,就像独占交互时发生的那样。

交互矩阵

CarAudioService定义一个交互矩阵。每行代表当前焦点持有者的CarAudioContext ,每列代表传入请求的 CarAudioContext

car-audio-focus.svg

由于是同时进行交互的,因此可能有多个焦点持有者。在这种情况下,在确定要应用什么交互之前,会将传入的焦点请求与每个当前焦点持有者进行比较。在这种情况下,会优秀使用保守的交互:先是拒绝,然后排他,最后并发。

例如,当电话应用程序在持有焦点过程中,音乐媒体请求焦点时,则矩阵指示拒绝音乐媒体的焦点请求,当前只能处理电话的交互。

当音乐媒体应用程序在导航应用程序请求焦点时保持焦点时,假设满足并发交互的其他标准,则矩阵指示两个交互可以同时播放。

多区域焦点管理

对于具有多个音频区域的车辆,每个区域的音频焦点都是独立管理的。对一个区域的请求不会考虑其他区域中持有焦点的内容,也不会导致其他区域中的焦点持有者失去焦点。主舱的焦点可以与后座娱乐系统分开管理,从而不会因焦点改变到另一个区域而中断一个区域的音频播放。

对于所有应用程序, CarAudioService会自动管理焦点。焦点请求的音频区域由其关联的UserIdUID确定,应用程序要同时在多个区域中播放音频,则必须通过在捆绑包中包含AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID来请求每个区域的焦点

Bundle bundle = new Bundle();  
bundle.putInt(CarAudioManager.AUDIOFOCUS_EXTRA_REQUEST_ZONE_ID,  
zoneId);  
  
AudioAttributes attributesWithZone = new AudioAttributes.Builder()  
        .setUsage(AudioAttributes.USAGE_MEDIA)  
        .addBundle(bundle)  
        .build();