上一篇我们简单地说了一下Android java层的基本框架。接下来我们就来聊一下在android中音量控制的相关内容。
1.音量定义
在Android中,音量的控制与流类型是密不可分的,每种流类型都独立地拥有自己的音量设置,各种流类型的音量是互不干扰的,例如音乐音量、通话音量就是相互独立的。Andorid当前在AudioSystem.java默认有10种流类型(见下表列二)。既然Android当中有10种流类型,每种流类型的音量都是相互独立的,所以在默认音量方面,Android也给每种流类型初始化了一个默认的音量(下表列二、三)和最大音量(下表列四)。虽然Android中拥有10种流类型,但是为了便于使用,android通过判断设备的类型,去映射具体流类型。当前Android在AudioSystem.java中提供了3个设备(DEFAULT,VOICE,TELEVISION)作为可选择项,分别去映射我们具体的音频流类型。其中,DEFAULT和VOICE类型的音频映射是一致的。
所以,从上表中可以看出,在手机设备当中,我们当前可调控的流类型音量其实只有5个,当你想调节STREAM_SYSTEM,STREAM_NOTIFICATION等流类型的音量时,实际上是调节了STREAM_RING的音量。当前可控的流类型可以通过下表更直观地显示:
每次手机开机,在Android6.0之前,SettingsProvide的内容提供者会将流类型默认值写入到数据库settings.db里面,当然,这是个SQLite数据库,如果数据库已存在则不再进行写入。如下图:
但是,从Andorid6.0开始,google将设置部分相关的内容从settings.db转移出来,转为以xml形式异步保存在/data/system/user/0(用户名)/目录下。当前负责存储音量和铃声相关的文件为settings_system.xml。形式如下:
<setting id="4" name="volume_alarm" value="6" package="android" /> <setting id="0" name="volume_music" value="11" package="android" /> <setting id="3" name="volume_voice" value="4" package="android" /> <setting id="32" name="ringtone" value="content://media/internal/audio/media/180" package="com.android.providers.media" /> <setting id="13" name="hearing_aid" value="0" package="android" /> <setting id="1" name="volume_ring" value="5" package="android" />当前Android6.0将这些设置转移出来,是为了便于执行并提高性能。更改一个设置可以从原本的400ms左右变为10ms左右。这大大地提高了读写的效率。另一方面,它为每个用户都会新建一个这样的表从而避免了多用户的设置的冲突。用户体验更好,设置更人性化。
2.音量调整
在Android手机上有两种改变系统音量的方式。最直接的做法就是通过手机的音量键进行音量调整,还有就是从设置界面中调整某一种类型音频的音量。他们都是都是通过AudioService进行的。
2.1 音量键的处理流程
音量键被按下后,Android输入系统将该事件一路派发给Activity,如果无人截获这个事件,承载当前Activity的显示的PhoneWindow类的onKeyDown()或onKeyUp()函数将会将其处理,从而开始了通过音量键调整音量的处理流程。需要注意的是,按照Android的输入事件派发策略,Window对象在事件的派发队列中排在Activity的后面(应该说排在队尾比较合适),所以应用程序可以重写自己的onKeyDown()函数,将音量键用作其他的功能。
PhoneWindow的onKeyDown()函数实现如下(省略部分代码):…… switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_DOWN: case KeyEvent.KEYCODE_VOLUME_MUTE: { …… /* 在这里,先判断mMediaController是否为空(显示的音量调整UI是否还存在),假如存在,就直接调用mMediaController的adjustVolume进行调整音量。不存在就通过MediaSession去创建UI并调整。 */ if (mMediaController != null) { mMediaController.adjustVolume(direction, AudioManager.FLAG_SHOW_UI); } else { MediaSessionLegacyHelper.getHelper(getContext()).sendAdjustVolumeBy( mVolumeControlStreamType, direction, AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE | AudioManager.FLAG_FROM_KEY); } return true; } ……
上面的代码显示,PhoneWindow接收onKeyDown()事件处理时,先判断显示的音量调整UI是否存在,假如存在,就直接在当前的流类型上进行调整音量。假如不存在就通过MediaSession去创建UI并调整音量。
在这里Android从5.0开始使用MediaSession对音量进行控制。通过MeidaSession相关的类,最终在MeidaSessionService中调用AudioService的adjustSuggestedStreamVolume()进行真正的音量设置的初步处理。
AudioService的adjustSuggestedStreamVolume()实现如下(省略部分代码):
…… int streamType; boolean isMute = isMuteAdjust(direction); //在这里也可以更改需要修改的流类型 if (mVolumeControlStream != -1) { streamType = mVolumeControlStream; } else { //通过getActiveStreamType()函数获取要控制的流类型 streamType = getActiveStreamType(suggestedStreamType); } ensureValidStreamType(streamType); final int resolvedStream = mStreamVolumeAlias[streamType]; …… // For notifications/ring, show the ui before making any adjustments // Don't suppress mute/unmute requests if (mVolumeController.suppressAdjustment(resolvedStream, flags, isMute)) { direction = 0; flags &= ~AudioManager.FLAG_PLAY_SOUND; flags &= ~AudioManager.FLAG_VIBRATE; if (DEBUG_VOL) Log.d(TAG, "Volume controller suppressed adjustment"); } adjustStreamVolume(streamType, direction, flags, callingPackage, caller, uid); ……
adjustSuggestedStreamVolume()负责接收MeidaSessionService传入的信息,然后针对要修改流类型获取相应的映射,更改是否显示ui的标志,然后将具体的调整音量操作交给adjustStreamVolume()去完成。
另外,关于这个adjustSuggestedStreamVolume()有点是需要特别说明一下。它刚开始的时候有一个判断,条件是一个名为mVolumeControlStream的整型变量是否等于-1,从这块代码来看,mVolumeControlStream比参数传入的suggestedStreamType厉害多了,只要它不是-1,那么要调整音量的流类型就是它。那这么厉害的控制手段,是做什么用的呢?其实,mVolumeControlStream是VolumePanel通过forceVolumeControlStream()函数设置的。什么是VolumePanel呢?就是我们按下音量键后的那个音量条提示框了。VolumePanel在显示时会调用forceVolumeControlStream强制后续的音量键操作固定为促使它显示的那个流类型。并在它关闭时取消这个强制设置,即置mVolumeControlStream为-1。
AudioService的adjustStreamVolume()实现如下(省略部分代码):
…… //确认一下调整的音量方向和流类型 ensureValidDirection(direction); ensureValidStreamType(streamType); // 首先还是获取streamType映射到的流类型。 int streamTypeAlias = mStreamVolumeAlias[streamType]; VolumeStreamState streamState = mStreamStates[streamTypeAlias]; final int device = getDeviceForStream(streamTypeAlias); //然后获取这个streamType的当前音量 int aliasIndex = streamState.getIndex(device); boolean adjustVolume = true; int step; …… //确定当前流类型的音量等级 if ((streamTypeAlias == AudioSystem.STREAM_MUSIC) && ((device & mFixedVolumeDevices) != 0)) { flags |= AudioManager.FLAG_FIXED_VOLUME; if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE && (device & mSafeMediaVolumeDevices) != 0) { step = mSafeMediaVolumeIndex; } else { step = streamState.getMaxIndex(); } if (aliasIndex != 0) { aliasIndex = step; } } else { step = rescaleIndex(10, streamType, streamTypeAlias); } …… //判断是否该改变情景模式。例如当从震动转换成响铃时,不需要更改音量。adjustVolume作为一个控制量,控制是否需要更改音量。 final int result = checkForRingerModeChange(aliasIndex, direction, step, streamState.mIsMuted); adjustVolume = (result & FLAG_ADJUST_VOLUME) != 0; …… //调用adjustIndex()更改VolumeStreamState对象中保存的音量值 } else if (streamState.adjustIndex(direction * step, device, caller) || streamState.mIsMuted) { //发送消息给AudioHandle,更改音量。 sendMsg(mAudioHandler, MSG_SET_DEVICE_VOLUME, SENDMSG_QUEUE, device, 0, streamState, 0); } …… //最后通过sendVolumeUpdate去通知音量已经发生变化了。 int index = mStreamStates[streamType].getIndex(device); sendVolumeUpdate(streamType, oldIndex, index, flags); }
AudioService的adjustStreamVolume ()针对音量设置做了很多的操作,所以在这里简单地总结一下这个函数都作了什么:
1) 准备工作。计算按下音量键的音量步进值。主要通过rescaleIndex()函数的实现。
2) 检查是否需要改变情景模式。checkForRingerModeChange()和情景模式有关。调用adjustIndex()更改VolumeStreamState对象中保存的音量值。
3) 通过sendMsg()发送消息MSG_SET_DEVICE_VOLUME到mAudioHandler。
4) 调用sendVolumeUpdate()函数,通知外界音量发生了变化。
VolumeStreamState是AudioService的一个内部类,当进行音量或者铃声模式管理时,需要锁定这个对象,避免顺序出错。
下面是VolumeSteramState获取音量和保存音量的操作:
...... public void readSettings() { //先锁定,避免出错 synchronized (VolumeStreamState.class) { …… String name = getSettingNameForDevice(device); int defaultIndex = (device == AudioSystem.DEVICE_OUT_DEFAULT) ? AudioSystem.DEFAULT_STREAM_VOLUME[mStreamType] : -1; //通过读取setting数据库去获取值 int index = Settings.System.getIntForUser( mContentResolver, name, defaultIndex, UserHandle.USER_CURRENT); …… } ...... public boolean setIndex(int index, int device, String caller) { …… //先锁定,避免出错 synchronized (VolumeStreamState.class) { oldIndex = getIndex(device); index = getValidIndex(index); …… // 首先是在mIndexMap中保存设置的音量值 mIndexMap.put(device, index); changed = oldIndex != index; if (changed) { // 同时设置所有映射到当前流类型的其他流的音量 boolean currentDevice = (device == getDeviceForStream(mStreamType)); int numStreamTypes = AudioSystem.getNumStreamTypes(); for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) { if (streamType != mStreamType && mStreamVolumeAlias[streamType] == mStreamType) { …… } // 发送通知 mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, index); mVolumeChanged.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, oldIndex); mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE_ALIAS, mStreamVolumeAlias[mStreamType]); sendBroadcastToAll(mVolumeChanged); } return changed; }
VolumeSteramState可以直接通过调用Settings.System.getIntForUser()去获取数据库中的音量,但是,在更新音量时,它只是更新了内部保存的音量而没有做更多的处理。所以,真正的更新音量的操作应该由mAudioHandler去处理。
从AudioService的adjustStreamVolume ()可以知道,adjustStreamVolume()给AudioHandler发送了带有“MSG_SET_DEVICE_VOLUME”的消息。AudioHandler根据此消息会进行setDeviceVolume()处理。
以下是setDeviceVolume()的主要内容:
…… private void setDeviceVolume(VolumeStreamState streamState, int device) { //先锁定,避免出错 synchronized (VolumeStreamState.class) { //通过VolumeStreamState调用AudioSystem的setStreamVolumeIndex()设置音量到底层的AudioFlinger里面去 streamState.applyDeviceVolume_syncVSS(device); …… //继续给AudioHandler发送信息,调用persistVolume(),通过System.putIntForUser()将目标音量保存在Setting数据库中 sendMsg(mAudioHandler, MSG_PERSIST_VOLUME, SENDMSG_QUEUE, device, 0, streamState, PERSIST_DELAY); }
从上面代码可以看出,AudioService通过setDeviceVolume()真正地更改了音量。setDeviceVolume()先用过VolumeStremState调用AudioSystem的setStreamVolumeIndex()设置音量到底层的AudioFlinger里面去,然后在通过AudioHandler.persistVolume()将音量真正保存起来。这样就完成了大部分的音量调整了。
之后,AudioService通过sendVolumeUpdate()去更新界面。sendVolumeUpdate()会通过AIDL去调用VolumeDialogController.java中的onVolumeChangedW()方法,从而显示界面调整。在这里就不细说了。
总的来说,通过音量键去调整音量的序列图如下:
2.2 通过设置调整音量
在Android中,除了通过音量键直接调节音量之外,还可以在系统设置进行音量的调整。在当前系统设置应用当中,Android主要通过调用SeekBarVolumizer去显示界面并调整音量的。SeekBarVolumizer是一个控件,当我们触动这个控件的时候,控件会根据当初的音量和模式去调用AudioManager的adjustStreamVolume(静音或震动模式)或setStreamVolume(普通模式)去调整相对应的音量。具体的调整音量方式其实是大致一样的,这里就不细说了。
普通模式下通过设置调整音量的序列图如下:
2.3 静音与震动
静音与震动是另外的2种响铃模式。响铃模式的调整其实与上面音量调整的设置的思想和流程大部分是一致的。当前外部程序设置为静音或震动的流程为:先通过调用AudioManager去进行响铃模式的调整。实际的响铃模式调整发生在AudioService。AudioService设置并保存流类型的模式。稍微不同的是当前AudioService并没有调用AudioSystem去保存RingerMode,而是直接通过persistRingerMode()将其保存在数据库当中。然后Android6.0调整当前设置模式的序列图为:
在音频中,设置音量等等流程上是挺简单的。只需要我们细心点去一步一步往下走,就能找到我们想要的东西。