《深入理解Android:卷III A》一一3.2音量管理

简介:

本节书摘来华章计算机出版社《深入理解Android:卷III A》一书中的第3章,第3.2节,作者:张大伟 更多章节内容可以访问云栖社区“华章计算机”公众号查看。1

3.2音量管理

Android手机有两种改变系统音量的方式。最直接的做法就是通过手机的音量键进行音量调整,还有一种做法是从设置界面中调整某一种类型音频的音量。另外,应用程序可以随时将某种类型的音频静音。它们都是通过AudioService进行的。
本节将从上述三个方面对AudioService的音量管理进行探讨。
3.2.1音量键的处理流程

  1. 触发音量键
    在音量键被按下后,Android输入系统将该事件一路派发给Activity,如果无人截获并处理这个事件,承载当前Activity的显示PhoneWindow类的onKeyDown()或onKeyUp()函数将会处理,从而开始通过音量键调整音量的处理流程。输入事件的派发机制及PhoneWindow类的作用将在后续章节中详细介绍,现在只需要知道,PhoneWindow描述了一片显示区域,用于显示与管理我们所看到的Activity和对话框等内容。同时,它还是输入事件的派发对象,而且只有显示在最上面的PhoneWindow才会收到事件。

按照Android的输入事件派发策略,Window对象在事件的派发队列中位于Activity的后面,所以应用程序可以重写自己的Activity.onKeyDown()函数以截获音量键的消息,将其用作其他的功能。比如说,在一个相机应用中,按下音量键所执行的动作是拍照而不是调节音量。
PhoneWindow的onKeyDown()函数实现如下:
[PhoneWindow.java-->PhoneWindow.onKeyDown()]
......

switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:
case KeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_MUTE: {
    // 直接调用到AudioManager的handleKeyUp里面去,是不是很简单而且直接呢
    getAudioManager().handleKeyDown(event, mVolumeControlStreamType);
    return true;
}

......
注意handleKeyDown()函数的第二个参数,它的意义是指定音量键将要改变哪一种流类型的音量。在Android中,音量的控制与流类型是密不可分的,每种流类型都独立地拥有自己的音量设置,它们在绝大部分情况下互不干扰,例如音乐音量、通话音量就是相互独立的。所以说,离开流类型谈音量是没有意义的。在Android中,音量这个概念描述的一定是某一种流类型的音量。
这里传入了mVolumeControlStreamType,那么这个变量的值是从哪里来的呢?Activity类中有一个函数名为setVolumeControlStream(int streamType)。应用可以通过调用这个函数来指定显示这个Activity时音量键所控制的流类型。这个函数的内容很简单,就一行,如下:
[Activity.java-->Activity.setVolumeControlStream()]
getWindow().setVolumeControlStream(streamType);
getWindow()的返回值就是用于显示当前Activity的PhoneWindow。从名字就可以看出,这个调用改变了mVolumeControlStreamType,于是也改变了按下音量键后传入AudioManager.handleKeyUp()函数的参数,从而达到setVolumeControlStream的目的。同时,还应该能看出,这个设置被绑定到Activity的Window上,在不同Activity之间切换时,接收按键事件的Window也会随之切换,所以应用不需要去考虑在其生命周期中音量键所控制的流类型的切换问题。
AudioManager的handleKeyDown()的实现很简单,在一个switch中,它调用了AudioService的adjustSuggestedStreamVolume(),所以直接看一下AudioService的这个函数。

  1. adjustSuggestedStreamVolume()分析
    我们先来看函数原型:

public void adjustSuggestedStreamVolume(int direction, int suggestedStreamType,

                                    int flags)

adjustSuggestedStreamVolume()有三个参数,第一个参数direction指示了音量的调整方向,1为增大,-1为减小;第二个参数suggestedStreamType表示要求调整音量;第三个参数flags的意思就不那么容易理解了。其实AudioManager在handleKeyDown()中设置了两个flag,分别是FLAG_SHOW_UI和FLAG_VIBRATE。从名字上我们就能看出一些端倪。前者告诉AudioService我们需要弹出一个音量控制面板。而在handleKeyUp()里设置了FLAG_PLAY_SOUND,这是为什么当在松开音量键后“有时候”会有一个提示音。注意,handleKeyUp()中设置了FLAG_PLAY_SOUND,但只是有时候这个flag才会生效,在下面的代码中可以看到这是为什么。还需要注意的是,第二个参数名为suggestedStreamType,从其命名来推断,这个参数传入的流类型对AudioService来说只是一个建议,是否采纳这个建议,AudioService有自己的考虑。
看一下它的实现:
[AudioService.java-->AudioService.adjustSuggestedStreamVolume()]
public void adjustSuggestedStreamVolume(int direction, int suggestedStreamType,

                                    int flags) {
int streamType;
// ① 从这一小段代码中可以看出,在 AudioService中还有地方可以强行改变音量键控制的流类型
if (mVolumeControlStream != -1) {
    streamType = mVolumeControlStream;
} else {
    // ② 通过getActiveStreamType()函数获取要控制的流类型
    // 这里根据建议的流类型与AudioService的实际情况,返回一个值
    streamType = getActiveStreamType(suggestedStreamType);
}
// ③ 这个冗长条件判断的目的,就是只有在特定的流类型下,并且没有处于锁屏状态时才会播放声音
if ((streamType != STREAM_REMOTE_MUSIC) &&
        (flags& AudioManager.FLAG_PLAY_SOUND) != 0 &&
        ((mStreamVolumeAlias[streamType] != AudioSystem.STREAM_RING)
         || (mKeyguardManager != null && mKeyguardManager.
            isKeyguardLocked()))) {
    flags&= ~AudioManager.FLAG_PLAY_SOUND;
}

if (streamType == STREAM_REMOTE_MUSIC) {
    ...... //我们不讨论远程播放的情况
} else {
    // ④ 调用adjustStreamVolume
    adjustStreamVolume(streamType, direction, flags);
}

}
初看这段代码时,可能有读者对下面这句代码感到疑惑:
VolumeStreamState streamState = mStreamStates[mStreamVolumeAlias[streamType]];
其实,这是为了满足所谓的“将铃声音量用作通知音量”这种需求。这就需要实现在两个有这个需求的流A与B之间建立起一个A→B映射。当我们对A流进行音量操作时,实际上是在操作B流。笔者个人认为这个功能对用户体验的提升并不大,却给AudioService的实现增加了不小的复杂度。直观上来想,我们可以使用一个HashMap解决这个问题,键是源流类型,值是目标流类型。而Android使用了一个更简单却不是那么好理解的方法来完成这件事。AudioService用一个名为mStreamVolumeAlias的整型数组来描述这个映射关系。
要实现“以铃声音量用作音乐音量”,只需要修改相应位置的值为STREAM_RING即可,就像下面这样:
mStreamVolumeAlias[AudioSystem.STREAM_MUSIC] = AudioSystem.STREAM_RING;
之后,因为需要对A流进行音量操作时,实际上是在操作B流,所以就不难理解为什么在很多和流相关的函数里都会先做这样的一个转换:
streamType = mStreamVolumeAlias[streamType];
其具体的工作方式就留给读者思考。在本章的分析过程中,大可忽略这种转换,这并不影响我们对音量控制原理的理解。
简单来说,这个函数做了三件事:
确定要调整音量的流类型。
在某些情况下屏蔽FLAG_PLAY_SOUND。
调用adjustStreamVolume()。
关于这个函数有几点仍需要说明一下。在函数刚开始的时候有一个判断,条件是一个名为mVolumeControlStream的整型变量是否等于-1,从这块代码来看,mVolumeControlStream比参数传入的suggestedStreamType厉害多了,只要它不是-1,要调整音量的流类型就是它。那这么厉害的控制手段的作用是什么?其实,mVolumeControlStream是VolumePanel通过forceVolumeControlStream()函数设置的。什么是VolumePanel呢?就是我们按下音量键后的那个音量调节通知框。VolumePanel在显示时会调用forceVolumeControlStream强制后续的音量键操作固定为促使它显示的那个流类型,并在它关闭时取消这个强制设置,即设置mVolumeControlStream为-1。这个在后面分析VolumePanel时会看到。
接下来我们继续看一下adjustStreamVolume()的实现。

  1. adjustStreamVolume()分析
    [AudioService.java-->AudioService.adjustStreamVolume()]

public void adjustStreamVolume(int streamType, int direction, int flags) {

// 首先还是获取streamType映射到的流类型。这个映射的机制确实给我们的分析带来不小的干扰
// 在非必要的情况下忽略它们吧
int streamTypeAlias = mStreamVolumeAlias[streamType];
// 注意VolumeStreamState类
VolumeStreamState streamState = mStreamStates[streamTypeAlias];

final int device = getDeviceForStream(streamTypeAlias);
// 获取当前音量,注意第二个参数的值,它的目的是如果这个流被静音,则取出它被静音前的音量
final int aliasIndex = streamState.getIndex(device,
                                (streamState.muteCount() != 0) 
boolean adjustVolume = true;

// rescaleIndex用于将音量值的变化量从源流类型变换到目标流类型下
// 由于不同的流类型的音量调节范围不同,所以这个转换是必需的
int step = rescaleIndex(10, streamType, streamTypeAlias);

//上面准备好了所需的所有信息,接下来要做一些真正有用的动作了
// 比如说checkForRingerModeChange()。调用这个函数可能变更情景模式
// 它的返回值adjustVolume是一个布尔变量,用来表示是否有必要继续设置音量值
// 这是因为在一些情况下,音量键用来改变情景模式,而不是设置音量值
if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
      (streamTypeAlias == getMasterStreamType())) {
   ......
   adjustVolume = checkForRingerModeChange(aliasIndex, direction, step);
   ......
}

int index;
// 取出调整前的音量值。这个值稍后被用在sendVolumeUpdate()的调用中
final int oldIndex = mStreamStates[streamType].getIndex(device,
       (mStreamStates[streamType].muteCount() != 0) /* lastAudible */);
// 接下来我们可以看到,只有流没有被静音时,才会设置音量到底层去,否则只调整其静音前的音量
// 为了简单起见,暂不考虑静音时的情况
if (streamState.muteCount() != 0) {
    ......
} else {
    // 为什么还要判断streamState.adjustIndex的返回值呢?
    // 因为如果音量值在调整(adjust)之后并没有发生变化,比如说达到了最大值,就不需要
       继续后面的操作了
    if (adjustVolume && streamState.adjustIndex(direction * step, device)) {
        // 发送消息给AudioHandler
        // 这个消息在setStreamVolumeInt()函数的分析中已经看到过
        // 这个消息将把音量设置到底层去,并将其存储到SettingsProvider中
        sendMsg(mAudioHandler,
                MSG_SET_DEVICE_VOLUME,
                SENDMSG_QUEUE,
                device,
                0,
                streamState,
                0);
        }
   index = mStreamStates[streamType].getIndex(device, false  /* lastAudible */);
}

// 最后,调用sendVolumeUpdate函数,通知外界音量值发生了变化
sendVolumeUpdate(streamType, oldIndex, index, flags);

}
在这个函数的实现中,有一个非常重要的类型:VolumeStreamState。前面提到过,Android的音量是依赖于某种流类型的。如果Android定义了N个流类型,AudioService就需要维护N个音量值与之对应。另外每个流类型的音量等级范围不一样,所以还需要为每个流类型维护它们的音量调节范围。VolumeStreamState类的功能就是为了保存与一个流类型所有音量相关的信息。AudioService为每一种流类型都分配了一个VolumeStreamState对象,并且以流类型的值为索引,保存在一个名为mStreamStates的数组中。在这个函数中调用了VolumeStreamState对象的adjustIndex()函数,于是就改变了这个对象中存储的音量值。不过,仅仅是改变了它的存储值,并且没有把这个变化设置到底层。
总结一下这个函数都做了什么。
准备工作。计算按下音量键的音量步进值。细心的读者一定注意到了,这个步进值是10而不是1。原来,在VolumeStreamState中保存的音量值是其实际值的10倍。为什么这么做呢?这是为了在不同流类型之间进行音量转换时能够保证一定精度的一种实现,其转换过程读者可以参考rescaleIndex()函数的实现。我们可以将这种做法理解为在转换过程中保留了小数点后一位的精度。其实,直接使用float类型来保存岂不更简单?
检查是否需要改变情景模式。checkForRingerModeChange()和情景模式有关。读者可以自行研究其实现。
调用adjustIndex()更改VolumeStreamState对象中保存的音量值。
通过sendMsg()发送消息MSG_SET_DEVICE_VOLUME到mAudioHandler。
调用sendVolumeUpdate()函数,通知外界音量发生了变化。
我们将重点分析后面三项内容:adjustIndex()、MSG_SET_DEVICE_VOLUME消息的处理和sendVolumeUpdate()。

  1. VolumeStreamState的adjustIndex()分析
    我们先看一下这个函数的定义:

[AudioService.java-->VolumeStreamState.adjustIndex()]
public b3oolean adjustIndex(int deltaIndex, int device) {

// 将现有的音量值加上变化量,然后调用setIndex进行设置
// 返回值与setIndex一样
return setIndex(getIndex(device, false  /* lastAudible */) + deltaIndex,
                device,
                true  /* lastAudible */);

}
这个函数很简单,下面再看一下setIndex()的实现:
[AudioService.java-->VolumeStreamState.setIndex()]
public synchronized boolean setIndex(int index, int device, boolean lastAudible) {
int oldIndex = getIndex(device, false / lastAudible /);
index = getValidIndex(index);
// 在VolumeStreamState中保存设置的音量值,注意使用了一个HashMap
mIndex.put(device, getValidIndex(index));

if (oldIndex != index) {

  // 保存到lastAudible
  if (lastAudible) {
      mLastAudibleIndex.put(device, index);
  }
  // 同时设置所有映射到当前流类型的其他流的音量
  boolean currentDevice = (device == getDeviceForStream(mStreamType));
  int numStreamTypes = AudioSystem.getNumStreamTypes();
  for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
      ......
  }
  return true;

} else {

  return false;

}
}
在这个函数中有三项工作要做:
首先保存设置的音量值。这是VolumeStreamState的本职工作,这和Android 4.1之前的版本不一样,音量值与设备相关联了。因此对同一种流类型来说,在不同的音频设备下将会拥有不同的音量值。
然后根据参数的要求保存音量值到mLastAudibleIndex中。从名字就可以看出,它保存了静音前的音量。当取消静音时,AudioService就会恢复到这里保存的音量。
再就是对流映射的处理。既然A→B,那么在设置B的音量的同时要改变A的音量。这就是后面那个循环的作用。
可以看出,VolumeStreamState.adjustIndex()除了更新自己所保存的音量值外,没有做其他的事情。接下来再看一下MSG_SET_DEVICE_VOLUME的消息处理做了什么。

  1. MSG_SET_DEVICE_VOLUME消息的处理
    adjustStreamVolume()函数使用sendMsg()函数发送MSG_SET_DEVICE_VOLUME消息给mAudioHandler,这个Handler运行在AudioService的主线程上。直接看一下在mAudio-Handler中负责处理MSG_SET_DEVICE_VOLUME消息的setDeviceVolume()函数:

[AudioService.java-->AudioHandler.setIndex()]
private void setDeviceVolume(VolumeStreamState streamState, int device) {

/* 调用VolumeStreamState的applyDeviceVolume。这个函数的内容很简单,就是在调用Audio-
   System.setStreamVolumeIndex()。到这里,音量就被设置到底层的AudioFlinger中 */
streamState.applyDeviceVolume(device);

// 和上面一样,需要处理流音量映射的情况。这段代码和上面setIndex的相关代码很像,不是吗
int numStreamTypes = AudioSystem.getNumStreamTypes();
for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
    ......
}
/* 发送消息给mAudioHandler,其处理函数将会调用persitVolume()函数,这将会把音量的设置信息
  存储到SettingsProvider中。AudioService在初始化时,将会从SettingsProvider中将音量
  设置读取出来并进行设置 */
sendMsg(mAudioHandler,
       MSG_PERSIST_VOLUME,
       SENDMSG_QUEUE,
       PERSIST_CURRENT|PERSIST_LAST_AUDIBLE,
       device,
       streamState,
       PERSIST_DELAY);

}
sendMsg()是一个异步操作,这就意味着,完成adjustIndex()更新音量信息后adjustStreamVolume()函数就返回了,但是音量并没有立刻被设置到底层。不过由于Handler处理多个消息的过程是串行的,这就隐含着一种风险:如果当Handler正在处理某一个消息时发生了阻塞,那么按下音量键,虽然调用adjustStreamVolume()可以立刻返回,并且从界面上看或用getStreamVolume()获取音量值都是没有问题的,但是手机发出声音时的音量大小并没有改变。

  1. sendVolumeUpdate()分析
    接下来,分析一下sendVolumeUpdate()函数,它用于通知外界音量发生了变化。

[AudioService.java-->AudioService.sendVolumeUpdate()]
private void sendVolumeUpdate(int streamType, int oldIndex, int index, int flags) {

/* 读者可能会感觉这句代码有点奇怪,mVoiceCapable是从SettingsProvider中取出来的一个常量。
  从某种意义上来说,它可以用来判断设备是否拥有通话功能。对没有通话能力的设备来说,RING流类
  型自然也就没有意义了。这句话应该算是一种从语义操作上进行的保护 */
if (!mVoiceCapable && (streamType == AudioSystem.STREAM_RING)) {
    streamType = AudioSystem.STREAM_NOTIFICATION;
}

// mVolumePanel是一个VolumePanel类的实例,就是它显示了音量提示框
mVolumePanel.postVolumeChanged(streamType, flags);

/*发送广播。可以看到它们都有(x+5)/10的一个操作。为什么除以10可以理解,但是+5的意义是什么
  呢?原来是为了实现四舍五入 */
oldIndex = (oldIndex + 5) / 10;
index = (index + 5) / 10;
Intent intent = new Intent(AudioManager.VOLUME_CHANGED_ACTION);
intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, streamType);
intent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, index);
intent.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, oldIndex);
mContext.sendBroadcast(intent);

}
这个函数将音量的变化通过广播的形式通知给其他感兴趣的模块。同时,它还特别通知了mVolumePanel。mVolumePanel是VolumePanel类的一个实例。我们所看到的音量调节通知框就是它。
至此,从按下音量键开始的整个处理流程就完结了。在继续分析音量调节通知框的工作原理之前,先对之前的分析过程进行总结,参考图3-2的序列图。

图 3-2通过音量键调整音量的处理流程
结合上面分析的结果,由图 3-2可知:
音量键处理流程的发起者是PhoneWindow。
AudioManager仅仅起到代理的作用。
AudioService接受AudioManager的调用请求,操作VolumeStreamState的实例进行音量的设置。
VolumeStreamState负责保存音量设置,并且提供了将音量设置到底层的方法。
AudioService负责将设置结果以广播的形式通知外界。
到这里,相信大家对音量调节的流程已经有了一个比较清晰的认识。接下来我们将介绍音量调节通知框的工作原理。
7.音量调节通知框的工作原理
在分析sendVolumeUpdate()函数时曾经注意到,它调用了mVolumePanel的post-VolumeChanged()函数。mVolumePanel是一个VolumePanel的实例,作为一个Handler的子类,它承接了音量变化的UI/声音的通知工作。在继续上面的讨论之前,先了解一下VolumePanel工作的基本原理。
VolumePanel位于android.view包下,却没有在API中提供,因为它只能被AudioService使用,所以和AudioService放在一个包下可能更合理一些。从这个类的注释上可以看到,谷歌的开发人员对它被放在android.view下也有极大不满(What A Mass! 他们这么写道……)。
VolumePanel下定义了两个重要的子类型,分别是StreamResources和StreamControl。StreamResources实际上是一个枚举,它的每一个可用元素保存了一个流类型的通知框所需要的各种资源,如图标、提示文字等。StreamResources的定义就像下面这样:
[VolumePanel.java-->VolumePanel.StreamResources]
private enum StreamResources {

BluetoothSCOStream(AudioManager.STREAM_BLUETOOTH_SCO,
       R.string.volume_icon_description_bluetooth,
       R.drawable.ic_audio_bt,
       R.drawable.ic_audio_bt,
       false),
// 我们省略了后面的几个枚举项的构造参数,这些与BluetoothSCOStream的内容是一致的
RingerStream(......),
VoiceStream(......),
AlarmStream(......),
MediaStream(......),
NotificationStream(......),
MasterStream(......),
RemoteStream(......);
int streamType;         // 流类型
int descRes;             // 描述信息
int iconRes;             // 图标
int iconMuteRes;        // 静音图标
boolean show;            // 是否显示
// 构造函数
StreamResources(int streamType, int descRes, int iconRes, int iconMuteRes
                , boolean show) {
    ......
}

};
这几个枚举项组成了一个名为STREAM的数组,如下:
[VolumePanel.java-->VolumePanel.STREAMS]
private static final StreamResources[] STREAMS = {

StreamResources.BluetoothSCOStream,
StreamResources.RingerStream,
StreamResources.VoiceStream,
StreamResources.MediaStream,
StreamResources.NotificationStream,
StreamResources.AlarmStream,
StreamResources.MasterStream,
StreamResources.RemoteStream

};
VolumePanel将从这个STREAMS数组中获取它所支持的流类型的相关资源。这么做是不是有点啰嗦呢?事实上,在这里使用枚举并没有什么特殊的意义,使用一个普通的Java类来定义StreamResources就已经足够了。
StreamControl类则保存了一个流类型的通知框所需要显示的控件,其定义如下:
[VolumePanel.java-->VolumePanel.StreamControl]
private class StreamControl {

int streamType;
ViewGroup group;
ImageView icon;
SeekBar seekbarView;
int iconRes;
int iconMuteRes;

}
很简单对不对?StreamControl实例中保存了音量调节通知框中所需的所有控件。关于这个类在VolumePanel的使用,我们可能很直观地认为只有一个StreamControl实例,在对话框显示时,使其保存的控件按需加载指定流类型的StreamResources实例中定义的资源。其实不然,出于对运行效率的考虑,StreamControl实例也是每个流类型人手一份,和StreamResources实例形成一一对应的关系。所有的StreamControl实例被保存在一个以流类型的值为键的Hashtable中,名为mStreamControls。我们可以在StreamControl的初始化函数createSliders()中一窥端倪。
[VolumePanel-->VolumePanel.createSliders()]
private void createSliders() {
......
// 遍历STREAM中所有的StreamResources实例
for (int i = 0; i < STREAMS.length; i++) {

   StreamResources streamRes = STREAMS[i];
   int streamType = streamRes.streamType;
    ......
    // 为streamType创建一个StreamControl
    StreamControl sc = new StreamControl();
    // 这里将初始化sc的成员变量
    ......
    // 将初始化好的sc放入mStreamControls中
    mStreamControls.put(streamType, sc);
}

}
值得一提的是,这个初始化的工作并没有在构造函数中进行,而是在postVolume-Changed()函数中处理的。
既然已经有了通知框所需要的资源和通知框的控件,接下来就要有一个对话框承载它们。没错,VolumePanel保存了一个名为mDialog的Dialog实例,这就是通知框的本身了。每当有新的音量变化到来时,mDialog的内容就会被替换为指定流类型对应的StreamControl中所保存的控件,并且根据音量变化情况设置其音量条的位置,最后调用mDialog.show()显示出来。同时,发送一个延时消息MSG_TIMEOUT,这条延时消息生效时,将会关闭提示框。
StreamResource、StreamControl与mDialog的关系就像图3-3所示的那样,StreamControl可以说是mDialog的配件,随需拆卸。

图 3-3StreamResource、StreamControl与mDialog的关系
接下来具体看一下VolumePanel在收到音量变化通知后都做了什么。我们在上一小节中说到了mVolumePanel.postVolumeChanged()函数。它的内容很简单,直接发送了一条消息MSG_VOLUME_CHANGED,然后在handleMessage中调用onVolumeChanged()函数进行真正的处理。
VolumePanel在MSG_VOLUME_CHANGED的消息处理函数中调用onVolume-Changed()函数,而不是直接在postVolumeChanged()函数中直接调用。这么做是有实际意义的。由于Android要求只能在创建控件的线程中对控件进行操作。postVolumeChanged()作为一个回调性质的函数,不能要求调用者位于哪个线程中。所以必须通过向Handler发送消息的方式,将后续的操作转移到指定的线程中。在设计具有UI Controller功能的类时,VolumePanel的实现方式有很好的参考意义。
下面看一下onVolumeChanged()函数的实现:
[VolumePanel.java-->VolumePanel.onVolumeChanged()]
protected void onVolumeChanged(int streamType, int flags) {

//  需要flags中包含AudioManager.FLAG_SHOW_UI 才会显示音量调节通知框
if ((flags & AudioManager.FLAG_SHOW_UI) != 0) {
    synchronized (this) {
        if (mActiveStreamType != streamType) {
            reorderSliders(streamType); // 在Dialog里装载需要的StreamControl
        }

        // 这个函数负责最终的显示
        onShowVolumeChanged(streamType, flags);
    }
}
// 是否播出Tone音,注意有个小延迟
if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 && ! mRingIsSilent) {
    removeMessages(MSG_PLAY_SOUND);
    sendMessageDelayed(obtainMessage(MSG_PLAY_SOUND, streamType, flags),
                       PLAY_SOUND_DELAY);
}

// 取消声音与振动的播放
if ((flags & AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE) != 0) {
    removeMessages(MSG_PLAY_SOUND);
    removeMessages(MSG_VIBRATE);
    onStopSounds();
}

// 开始安排回收资源
removeMessages(MSG_FREE_RESOURCES);
sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY);

// 重置音量框超时关闭的时间
resetTimeout();

}
注意最后一个resetTimeout()的调用,其实它重新延时发送了MSG_TIMEOUT消息。当MSG_TIMEOUT消息生效时,mDialog将被关闭。
之后就是onShowVolumeChanged了。这个函数负责为通知框的内容填充音量、图表等信息,然后再显示通知框(如果还没有显示)。以铃声音量为例,省略其他的代码。
[VolumePanel.java-->VolumePanel.onShowVolumeChanged()]
protected void onShowVolumeChanged(int streamType, int flags) {

// 获取音量值
int index = getStreamVolume(streamType);
// 获取音量最大值,这两个将用来设置进度条
int max = getStreamMaxVolume(streamType);

switch (streamType) {
// 在这个switch语句中,我们要根据每种流类型的特点进行各种调整。
// 例如Music有时就需要更新它的图标,因为使用蓝牙耳机时的图标和平时的不一样,
// 所以每一次都需要更新一下
case AudioManager.STREAM_MUSIC: {
    if ((mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC) &
                    (AudioManager.DEVICE_OUT_BLUETOOTH_A2DP |
                     AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES |
                      AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER)) != 0) {
                        setMusicIcon(R.drawable.ic_audio_bt,
                               R.drawable.ic_audio_bt_mute); // 设置为蓝牙图标
    } else {
        setMusicIcon(R.drawable.ic_audio_vol, 
        R.drawable.ic_audio_vol_mute);// 设置为普通图标
    }
    break;
......
}

 // 取出Music流类型对应的StreamControl,并设置其SeekBar的音量显示
StreamControl sc = mStreamControls.get(streamType);
if (sc != null) {
    if (sc.seekbarView.getMax() != max) {
        sc.seekbarView.setMax(max);
    }
    sc.seekbarView.setProgress(index);
    ......
}

if (!mDialog.isShowing()) {  //  如果对话框还没有显示
  /*forceVolumeControlStream()的调用在这里,一旦此通知框被显示,之后按下音量键都只能
    调节当前流类型的音量。直到通知框关闭时,重新调用forceVolumeControlStream(),并设
    置streamType为-1*/
    mAudioManager.forceVolumeControlStream(streamType);
  // 为Dialog设置显示控件
  / *注意, mView目前已经在reorderSlider()函数中安装好Music流所对应的StreamControl了 */
  mDialog.setContentView(mView);
  ......
  // 显示对话框
  mDialog.show();

}
}
至此,音量调节通知框就被显示出来了,下面总结一下它的工作过程:
postVolumeChanged() 是VolumePanel显示的入口。
检查flags中是否有FLAG_SHOW_UI。
VolumePanel会在第一次被要求弹出时初始化其控件资源。
mDialog 加载指定流类型对应的StreamControl,也就是控件。
显示对话框并开始超时计时。
超时计时到达,关闭对话框。
到此为止,AudioService对音量键的处理流程介绍完毕。而 Android还有另外一种改变音量的方式,即音量设置函数etStreamVolume(),下面对其进行介绍。
3.2.2通用的音量设置函数setStreamVolume()
除了可以通过音量键调节音量以外,用户还可以在系统设置中进行调节。Audio-Manager.setStreamVolume()是系统设置界面中调整音量所使用的接口。

  1. setStreamVolume()分析
    setStreamVolume()是SDK中提供给应用的API,它的作用是为特定的流类型设置范围内允许的任意音量。我们看一下它的实现:

[AudioService.java-->AudioService.setStreamVolume()]
public void setStreamVolume(int streamType, int index, int flags) {

// 这里先判断一下流类型这个参数的有效性
ensureValidStreamType(streamType);
// 获取保存了指定流类型音量信息的VolumeStreamState对象
// 注意,这里面使用mStreamVolumeAlias对这个数组进行流类型转换
VolumeStreamState streamState = mStreamStates[mStreamVolumeAlias[streamType]];
// 获取当前流将使用哪一个音频设备进行播放。它最终会被调用到AudioPolicyService中
final int device = getDeviceForStream(streamType);
// 获取流当前的音量
final int oldIndex = streamState.getIndex(device,
                        (streamState.muteCount() != 0) /* lastAudible */);

// 将原流类型下的音量值映射到目标流类型下的音量值
// 因为不同流类型的音量值刻度不一样,所以需要进行转换
index = rescaleIndex(index * 10, streamType, mStreamVolumeAlias[streamType]);

//暂时先忽略下面这段if中的代码。它的作用是根据flags的要求修改手机的情景模式
if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
            (mStreamVolumeAlias[streamType] == getMasterStreamType())) {
    ......
}
// 调用setStreamVolumeInt()
setStreamVolumeInt(mStreamVolumeAlias[streamType], index, device, false, true);
// 获取设置的结果
index = mStreamStates[streamType].getIndex(device,
           (mStreamStates[streamType].muteCount() != 0) /* lastAudible */);
// 广播通知
sendVolumeUpdate(streamType, oldIndex, index, flags);

}
看明白这个函数了吗?抛开被忽略掉的那个if块可以归纳为:这个函数的工作其实很简单,就执行了下面三方面的工作:
为调用setStreamVolumeInt()准备参数。
调用setStreamVolumeInt()。
广播音量发生变化的通知。
下面分析的主线将转向setStreamVolumeInt()的内容。

  1. setStreamVolumeInt()分析
    看一下setStreamVolumeInt()函数的代码,和前面一样,暂时忽略目前与分析目标无关的部分代码。

[AudioService.java-->AudioService.setStreamVolumeInt()]
private void setStreamVolumeInt(int streamType,

                            int index,
                            int device,
                            boolean force,
                            boolean lastAudible) {
// 获取保存音量信息的VolumeStreamState对象
VolumeStreamState streamState = mStreamStates[streamType];

if (streamState.muteCount() != 0) {
    // 这里的内容是为了处理当流被静音后的情况。我们在讨论静音的实现时再考虑这段代码
    ......
} else {
    // 调用streamState.setIndex()
    if (streamState.setIndex(index, device, lastAudible) || force) {
        // 如果setIndex返回true,或者force参数为true,就在这里向mAudioHandler发送消息
        sendMsg(mAudioHandler,
                MSG_SET_DEVICE_VOLUME,
                SENDMSG_QUEUE,
                device,
                0,
                streamState,
                0);
    }
}

}
此函数有两个工作内容,一个是调用streamState.setIndex(),另一个则是根据setIndex()的返回值和force参数决定是否发送MSG_SET_DEVICE_VOLUME消息。这两项内容在3.2.1节中已经介绍过,在此不再赘述。
至此,setStreamVolume()的分析完成。
分析完setStreamVolume()的工作流程后,读者是否觉得有些熟悉呢?如果我们用setStreamVolumeInt()的代码替换setStreamVolume()中对setStreamVolumeInt()的调用,再和adjustStreamVolume()函数进行以下比较,就会发现它们的内容出奇得相似。Android在其他地方也有这样的情况。从这一点上来说,已经发展到4.1版本的Android源代码仍然不够精致。读者可以思考一下,有没有办法把这两个函数融合为一个函数呢?
到此,对于音量设置相关的内容就告一段落。接下来我们将讨论和音量相关的另一个重要的内容—静音。
3.2.3静音控制
静音控制的情况与音量调节有很大的不同。因为每个应用都有可能进行静音操作,所以为了防止状态发生紊乱,就需要为静音操作进行计数,也就是说多次静音后需要多次取消静音。
不过,进行了静音计数后还会引入另外一个问题。如果一个应用在静音操作(计数加1)后因为某种原因不小心崩溃了,那么将不会有人再为它进行取消静音的操作,静音计数无法再回到0,也就是说这个“倒霉”的流将被永远静音下去。
那么怎么处理应用异常退出后的静音计数呢?AudioService的解决办法是记录下每个应用自己的静音计数,当应用崩溃时,在总的静音计数中减去崩溃应用自己的静音计数,也就是说,为这个应用完成它没能完成的取消静音这个操作。为此,VolumeStreamState定义了一个继承自DeathRecepient的内部类,名为VolumeDeathHandler,并且为每个进行静音操作的进程创建一个实例。VolumeDeathHandler的实例保存了对应进程的静音计数,并在进程死亡时进行计数清零的操作。从这个名字来看可能是Google希望这个类将来能够承担更多与音量相关的事情吧,不过眼下它只负责静音。我们将在后续的内容中对这个类进行深入讲解。
经过前面的介绍,我们不难得出AudioService、VolumeStreamState与VolumeDeathHandler的关系,如图3-4所示。

图 3-4与静音相关的类

  1. setStreamMute()分析
    同音量设置一样,静音控制也是相对于某一个流类型而言的。正如本节开头所提到的,静音控制涉及引用计数和客户端进程的死亡监控。所以相对于音量控制来说,静音控制有一定的复杂度。还好,静音控制对外入口只有一个函数,就是AudioManager.setStreamMute()。其第二个参数state为true,表示静音,否则表示解除静音。

[AudioManager.java-->AudioManager.setStreamMute()]
public void setStreamMute(int streamType, boolean state) {

IAudioService service = getService();
try {
    // 调用AudioService的setStreamMute,注意第三个参数mICallBack
    service.setStreamMute(streamType, state, mICallBack);
} catch (RemoteException e) {
    Log.e(TAG, "Dead object in setStreamMute", e);
}

}
AudioManager一如既往地充当着AudioService代理的一个角色,不过这次有一个很小却很重要的动作:AudioManager为AudioService传入了一个名为mICallBack的变量。查看一下mICallBack的定义:
private final IBinder mICallBack = new Binder();
真是简单得不得了。全文搜索一下,我们发现mICallBack只用来作为AudioService的几个函数调用的参数。从AudioManager角度看它没有任何实际意义。其实,这在Android的进程间交互通信中是一种常见且非常重要的技术。mICallBack这个简单Binder对象可以充当Bp端在Bn端的一个唯一标识。而且AudioService拿到这个标识后,就可以通过DeathRecipient机制获取Bp端异常退出的回调。这是AudioService维持静音状态正常变迁的一个基石。
服务端把客户端传入的这个Binder对象作为客户端的一个唯一标识的时候,往往会以这个标识为键创建一个Hashtable,用来保存每个客户端的相关信息。这在Android各个系统服务的实现中是一种很常见的用法。
另外,本例传入的mICallBack是直接从Binder类实例化出来的,是一个很原始的IBinder对象。进一步讲,如果传递了一个通过AIDL定义的IBinder对象,那么这个对象就有了交互能力,服务端可以通过它向客户端进行回调。在后面探讨AudioFocus机制时会遇到这种情况。

  1. VolumeDeathHandler分析
    我们继续跟踪AudioService.setStreamMute()的实现,记得注意第三个参数cb,它代表特定客户端的标识。

[AudioService.java-->AudioService.setStreamMute()]
public void setStreamMute(int streamType, boolean state, IBinder cb) {

// 只有可以静音的流类型才能执行静音操作。这说明,并不是所有的流都可以被静音
if (isStreamAffectedByMute(streamType)) {
    // 直接调用了流类型对应的mStreamStates的mute()函数
    // 这里没有进行那个令人讨厌的流类型的映射。这是出于操作语义上的原因。读者可以自行思考一下
    mStreamStates[streamType].mute(cb, state);
}

}
接下来是VolumeStreamState的mute()函数。VolumeStreamState的确是音量相关操作的核心类型。
[AudioService.java-->VolumeStreamState.mute()]
public synchronized void mute(IBinder cb, boolean state) {

// 这句话是一个重点,VolumeDeathHandler与cb一一对应
// 用来管理客户端的静音操作,并且监控客户端的生命状态
VolumeDeathHandler handler = getDeathHandler(cb, state);
if (handler == null) {
   Log.e(TAG, "Could not get client death handler for stream:"+mStreamType);
   return;
}
// 通过VolumeDeathHandler执行静音操作
handler.mute(state);

}
上述代码引入了静音控制的主角,VolumeDeathHandler,也许叫做MuteHandler更合适一些。它其实只有两个成员变量,分别是mICallBack和mMuteCount。其中mICallBack保存了客户端传进来的标识,mMuteCount则保存了当前客户端执行静音操作的引用计数。另外,它继承自IBinder.DeathRecipient,所以它拥有监听客户端生命状态的能力。而VolumeDeathHandler()的成员函数只有两个,分别是mute()和binderDied()。说到这里,再看看上面VolumeStreamState.mute()的实现,读者能想象到VolumeDeathHandler的具体实现是什么样子的吗?
继续上面的脚步,看一下它的mute()函数。它的参数state的取值指定了进行静音还是取消静音。所以这个函数也就被分成两部分,分别是处理静音与取消静音两个操作。其实,这完全可以放在两个函数中完成。先看看静音操作是怎么实现的吧。
[AudioService.java-->VolumeDeathHandler.mute()part 1]
public void mute(boolean state) {

if (state) {
    // 静音操作
    if (mMuteCount == 0) {
        // 如果mMuteCount 等于0,则表示客户端是第一次执行静音操作
        // 此时linkToDeath开始对客户端的生命状况进行监听
        // 这样做的好处是可以避免非静音状态下额外占用Binder资源
        try {
            // 为什么要判断linkToDeath是否为空?AudioManager不是传递进一个有效的
            // Binder吗?原来AudioManager也可能会调用mute()
            // 此时的mICallback为空
            if (mICallback != null) {
                mICallback.linkToDeath(this, 0);
            }

            // 保存到mDeathHandlers列表中
            mDeathHandlers.add(this);
            // muteCount()是对全局的静音操作的引用计数
            // 如果它的返回值为0,则表示这个流目前还没有被静音
            if (muteCount() == 0) {
                // 在这里设置流的音量为0
                ......
            }
        } catch (RemoteException e) {
            ......
        }
    }
    // 引用计数加1
    mMuteCount++;
} else {
    // 暂时先不给出取消静音的操作
    ......
}

}
看明白了吗?这个函数的条件嵌套比较多,仔细归纳一下,就会发现这段代码的思路是非常清晰的。静音操作根据条件满足与否,完成三个任务:
无论在什么条件下,只要执行这个函数,静音操作的引用计数都会加1。
如果这是客户端第一次执行静音,则开始监控其生命状态,并且把自己加入VolumeStreamState的mDeathHandlers列表中。这是这段代码中很精练的一个操作,只有在客户端执行过静音操作后才会对其生命状态感兴趣,才有保存其VolumeDeathHandler的必要。
更进一步的是,如果这是这个流类型第一次被静音,则设置流音量为0,这才是真正的静音动作。
不得不说,这段代码是非常精练的,不是说代码量少,而是它的行为非常干净,决不会做多余的操作,也不会保存多余的变量。
下面我们要看一下取消静音的操作。取消静音作为静音的逆操作,相信读者已经可以想象到它都做什么事情了吧?这里就不再对其进行说明了。
[AudioService.java-->VolumeDeathHandler.mute() part 2]
public void mute(boolean state) {

if (state) {
    // 忽略掉静音操作
    ......
} else {
    if (mMuteCount == 0) {
        Log.e(TAG, "unexpected unmute for stream: "+mStreamType);
    } else {
        // 引用计数先减1
        mMuteCount--;
        if (mMuteCount == 0) {
            // 如果这是客户端最后一次有效地取消静音
            mDeathHandlers.remove(this);
            if (mICallback != null) {
            mICallback.unlinkToDeath(this, 0);
        }
        if (muteCount() == 0) {
            // 将流的音量值设置回静音前的音量,也就是lastAudibleIndex
            ......
        }
    }
    }
}

}
下面就剩下最后的binderDied()函数了。当客户端发生异常,没能取消其执行过的静音操作时,需要替它完成它应该做却没做的事情。
[AudioService.java-->VolumeDeathHandler.binderDied()]
public void binderDied() {

if (mMuteCount != 0) {
    mMuteCount = 1;
    mute(false);
}

}
这个实现不难理解,读者可以自行分析一下为什么这么做可以消除意外退出的客户端遗留下来的影响。
3.2.4音量控制小结
音量控制是AudioService最重要的功能之一。经过上面的讨论,相信读者对AudioService的音量管理流程已经有了一定的理解。
总结一下我们在这一节里所学到的内容:
AudioService音量管理的核心是VolumeStreamState。它保存了一个流类型所有的音量信息。
VolumeStreamState保存了运行时的音量信息,而音量的生效则是在底层AudioFlinger完成的。所以进行音量设置需要做两件事情:更新VolumeStreamState存储的音量值,设置音量到Audio底层系统。
VolumeDeathHandler是VolumeStreamState的一个内部类。它的实例对应在一个流类型上执行静音操作的一个客户端,是实现静音功能的核心对象。

相关文章
|
3月前
|
数据采集 小程序 数据可视化
智慧校园电子班牌管理系统源码 Java Android原生
家长通过家长小程序端随时了解孩子在校的情况,实时接收学生的出勤情况,学生到校、离校时间。随时了解学生在校的表现、学生成绩排名,及时与教师沟通,关注孩子的健康成长。
30 0
智慧校园电子班牌管理系统源码 Java Android原生
|
5月前
|
Java 关系型数据库 MySQL
0002Java安卓程序设计-基于Uniapp+springboot菜谱美食饮食健康管理App2
0002Java安卓程序设计-基于Uniapp+springboot菜谱美食饮食健康管理App
34 0
|
7月前
|
数据采集 前端开发 Android开发
Android平台RTMP推送或GB28181设备接入端如何实现采集audio音量放大?
我们在做Android平台RTMP推送和GB28181设备对接的时候,遇到这样的问题,有的设备,麦克风采集出来的audio,音量过高或过低,特别是有些设备,采集到的麦克风声音过低,导致播放端听不清前端采集的audio,这时候,就需要针对采集到的audio,做音量放大处理。
|
3天前
|
存储 开发工具 Android开发
Android系统 权限组管理和兼容性
Android系统 权限组管理和兼容性
14 0
|
1月前
|
Shell 开发工具 Android开发
ADB 下载、安装及使用教程:让你更好地管理 Android 设备
ADB 下载、安装及使用教程:让你更好地管理 Android 设备
513 2
|
5月前
|
Android开发
0002Java安卓程序设计-基于Uniapp+springboot菜谱美食饮食健康管理App1
0002Java安卓程序设计-基于Uniapp+springboot菜谱美食饮食健康管理App
61 0
|
6月前
|
存储 安全 Java
Android DataStore:安全存储和轻松管理数据
Android DataStore:安全存储和轻松管理数据
|
2月前
|
小程序 Java 数据挖掘
Java校园智慧管理云平台源码 小程序+android电子班牌系统
智慧校园技术架构 ❀后端:Java ❀框架:springboot ❀前端页面:vue +element-ui ❀小程序:小程序原生开发 ❀电子班牌:Java Android
36 0
|
5月前
|
小程序 Java 数据库
0002Java安卓程序设计-springboot合同管理APP论文
0002Java安卓程序设计-springboot合同管理APP论文
21 0
|
7月前
|
Android开发
Android 弹窗优先级管理,赶紧拿去用吧
Android 弹窗优先级管理,赶紧拿去用吧