最近一个项目过认证, 在声压测试时failed。整改方案为:在用户将耳机音量提高至安全音量以上时,阻止此操作并弹出警告框,待用户确认后才提升音量。一开始并不知道android4.2中默认自带了这套机制,于是自己瞎琢磨添加了一套机制:在Audioervice中监听插入耳机时的音量变化,发送广播到SystemUI,再在SystemUI中注册Receiver去判断是否需要弹出警告框。历经千辛万苦终于把流程可以跑通,功能也可以实现,但有一个小BUG,就是用户当前提升的音量超过了安全音量,弹出警告框的同时音量也已经完成调节动作,不符合整改方案的要求。后来,无意中看到AudioService中有SafeVolume之类的变量,才恍然大悟,原来android默认就支持这个功能,于是将之前瞎琢磨的那套怒而删之,仔细研究如何开启android默认的机制, 以下是小小的心得,特此记录。
----------------------------------------------------------------------------------------------------
代码位置:frameworks/base/media/java/android/media/AudioService.java
- public AudioService(Context context) {
- .......... //省略
- mSafeMediaVolumeState = new Integer(Settings.Global.getInt(mContentResolver,Settings.Global.AUDIO_SAFE_VOLUME_STATE,SAFE_MEDIA_VOLUME_NOT_CONFIGURED));
- mSafeMediaVolumeIndex = mContext.getResources().getInteger(com.android.internal.R.integer.config_safe_media_volume_index) * 10;
- .......... //省略
- }
在AudioService的构造函数中,有关于安全音量的初始化设置,从配置文件中读取是否配置安全媒体音量以及安全音量的大小。
是否enable的配置文件在frameworks/base/core/res/res/values-Mcc码 文件夹中:
- <!-- Whether safe headphone volume is enabled or not (country specific). -->
- <bool name="config_safe_media_volume_enabled">true</bool>
goole了一下,中国的mcc码值为460, mnc为00或者01(对应移动或者联通), 我的这套android代码中没有这个文件夹, 我自己创建了一个并赋值。
配置安全媒体音量大小的文件在:frameworks/base/core/res/res/values/Config.xml中:
- <!-- Safe headphone volume index. When music stream volume is below this index the SPL on headphone output is compliant to EN 60950 requirements for portable music players. -->
- <integer name="config_safe_media_volume_index">5</integer>
这个index的大小可以根据实际情况进行调整。
再次编译烧录,插入耳机播放音乐,调整音量至第五个梯度时,弹出警告框。
接下来研究这套机制的具体实现流程:
还是刚才的frameworks/base/media/java/android/media/AudioService.java构造方法:
- public AudioService(Context context) {
- ................. //省略
- IntentFilter intentFilter = .....;
- intentFilter.addAction(Intent.ACTION_BOOT_COMPLETED);
- .................. //省略
- context.registerReceiver(mReceiver, intentFilter);
- }
AudioService的构造方法中监听BOOT_COMPLETED事件,当机器BOOT完成后,做如下操作:
- private class AudioServiceBroadcastReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- ..................
- } else if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
- ................
- sendMsg(mAudioHandler, MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED, SENDMSG_REPLACE, 0, 0,SAFE_VOLUME_CONFIGURE_TIMEOUT_MS);
- ................
- }
- ..................
- }
- @Override
- public void handleMessage(Message msg) {
- ..........................
- case MSG_CHECK_MUSIC_ACTIVE:
- onCheckMusicActive();
- break;
- case MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED:
- case MSG_CONFIGURE_SAFE_MEDIA_VOLUME:
- onConfigureSafeVolume((msg.what == MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED));
- break;
- case MSG_PERSIST_SAFE_VOLUME_STATE:
- onPersistSafeVolumeState(msg.arg1);
- break;
- ........................
- }
开机完成后,AudioService接受到BOOT_COMPLETED, 告诉Receiver处理,Receviver通过Handler处理,配置SafeVolume。
现在看看onConfigureSafeVolume中做了哪些操作:
- private void onConfigureSafeVolume(boolean force) {
- synchronized (mSafeMediaVolumeState) {
- int mcc = mContext.getResources().getConfiguration().mcc;
- if ((mMcc != mcc) || ((mMcc == 0) && force) {
- mSafeMediaVolumeIndex = mContext.getResources().getInteger(
- com.android.internal.R.integer.config_safe_media_volume_index) * 10;
- boolean safeMediaVolumeEnabled = mContext.getResources().getBoolean(
- com.android.internal.R.bool.config_safe_media_volume_enabled);
- // The persisted state is either "disabled" or "active": this is the state applied
- // next time we boot and cannot be "inactive"
- int persistedState;
- if (safeMediaVolumeEnabled) {
- persistedState = SAFE_MEDIA_VOLUME_ACTIVE;
- // The state can already be "inactive" here if the user has forced it before
- // the 30 seconds timeout for forced configuration. In this case we don't reset
- // it to "active".
- if (mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_INACTIVE) {
- mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE;
- enforceSafeMediaVolume();
- }
- } else {
- persistedState = SAFE_MEDIA_VOLUME_DISABLED;
- mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_DISABLED;
- }
- mMcc = mcc;
- sendMsg(mAudioHandler,
- MSG_PERSIST_SAFE_VOLUME_STATE,
- SENDMSG_QUEUE,
- persistedState,
- 0,
- null,
- 0);
- }
- }
- }
流程很清晰,判断当前能否读取到mcc设置,我的理解是当前是否有SIM卡插入,不知道理解的对不对。根据mcc的值重新读取safeMediaVolumeIndex 和 safeMediaVolumeEnabled 的设置。如果启用了safeMediaVolume, 将mSafeMediaVolumeState 设置为激活状态, 并调用enforceSafeMediaVolume().
再看enforceSafeMediaVolume()中做了些什么:
- private void enforceSafeMediaVolume() {
- /* add by quinn start 2014-05-10 */
- //VolumeStreamState streamState = mStreamStates[AudioSystem.STREAM_MUSIC];
- VolumeStreamState streamState;
- if (AudioSystem.isStreamActive(AudioSystem.STREAM_ATV, 0)) { /* now ATV is active */
- streamState = mStreamStates[AudioSystem.STREAM_ATV];
- } else if (AudioSystem.isStreamActive(AudioSystem.STREAM_FM, 0)) { /* now FM is active */
- streamState = mStreamStates[AudioSystem.STREAM_FM];
- } else { /* default music is active */
- streamState = mStreamStates[AudioSystem.STREAM_MUSIC];
- }
- /* add by quinn end 2014-05-10 */
- boolean lastAudible = (streamState.muteCount() != 0);
- int devices = mSafeMediaVolumeDevices;
- int i = 0;
- while (devices != 0) {
- int device = 1 << i++;
- if ((device & devices) == 0) {
- continue;
- }
- int index = streamState.getIndex(device, lastAudible);
- if (index > mSafeMediaVolumeIndex) {
- if (lastAudible) {
- streamState.setLastAudibleIndex(mSafeMediaVolumeIndex, device);
- sendMsg(mAudioHandler,
- MSG_PERSIST_VOLUME,
- SENDMSG_QUEUE,
- PERSIST_LAST_AUDIBLE,
- device,
- streamState,
- PERSIST_DELAY);
- } else {
- streamState.setIndex(mSafeMediaVolumeIndex, device, true);
- sendMsg(mAudioHandler,
- MSG_SET_DEVICE_VOLUME,
- SENDMSG_QUEUE,
- device,
- 0,
- streamState,
- 0);
- }
- }
- devices &= ~device;
- }
- }
android 默认只会对AudioSystem.STREAM_MUSIC进行安全媒体音量的检查,而我们的项目中有FM 和 ATV 功能,所以添加了个判断。此方法中,根据当前激活的音频状态,判断音量大小是否超过了安全音量,如果是则强制恢复到安全音量。对应到实际使用场景中即为: 用户在插入耳机后将音量调整至安全音量以上,再次开机后会强制恢复音量。
现在关于开机部分的安全音量机制已经走了一遍,对于在插入耳机时,用户通过音量按键或者panel去控制音量调整时的流程又是怎么样的呢, 继续往下走:
阅读代码发现,调节音量时会调用AudioService的setStreamVolume() adjustStreamVolume()方法,而在这两个方法中都有一个!checkSafeMediaVolume()的判断,直接贴该方法的实现:
- private boolean checkSafeMediaVolume(int streamType, int index, int device) {
- /* add by quinn start 2014-05-10 */
- boolean musicOrFmOrATVActive = (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC ||
- mStreamVolumeAlias[streamType] == AudioSystem.STREAM_FM ||
- mStreamVolumeAlias[streamType] == AudioSystem.STREAM_ATV);
- /* add by quinn end 2014-05-10 */
- synchronized (mSafeMediaVolumeState) {
- if ((mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE) &&
- /* add by quinn start 2014-05-10 */
- //(mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC) &&
- musicOrFmOrATVActive &&
- /* add by quinn end 2014-05-10 */
- ((device & mSafeMediaVolumeDevices) != 0) &&
- (index > mSafeMediaVolumeIndex)) {
- mVolumePanel.postDisplaySafeVolumeWarning();
- return false;
- }
- return true;
- }
- }
看代码可以发现,当要设置的音量超过安全音量时,会调用VolumePanel的postDisplaySafeVolumeWarning()方法,最终实现为onDisplaySafeVolumeWarrning(),即为需要用户确认的警告框,方法如下:
frameworks/base/core/java/android/view/VolumePanel.java
- protected void onDisplaySafeVolumeWarning() {
- synchronized (sConfirmSafeVolumeLock) {
- if (sConfirmSafeVolumeDialog != null) {
- return;
- }
- sConfirmSafeVolumeDialog = new AlertDialog.Builder(mContext)
- .setMessage(com.android.internal.R.string.safe_media_volume_warning)
- .setPositiveButton(com.android.internal.R.string.yes,
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- mAudioService.disableSafeMediaVolume();
- }
- })
- .setNegativeButton(com.android.internal.R.string.no, null)
- .setIconAttribute(android.R.attr.alertDialogIcon)
- .create();
- final WarningDialogReceiver warning = new WarningDialogReceiver(mContext,
- sConfirmSafeVolumeDialog);
- sConfirmSafeVolumeDialog.setOnDismissListener(warning);
- sConfirmSafeVolumeDialog.getWindow().setType(
- WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
- sConfirmSafeVolumeDialog.show();
- }
- }
当用户点击确认按钮时,调用AudioService的disableSafeMediaVolume()方法:
- public void disableSafeMediaVolume() {
- synchronized (mSafeMediaVolumeState) {
- setSafeMediaVolumeEnabled(false);
- }
- }
- private void setSafeMediaVolumeEnabled(boolean on) {
- synchronized (mSafeMediaVolumeState) {
- if ((mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_NOT_CONFIGURED) &&
- (mSafeMediaVolumeState != SAFE_MEDIA_VOLUME_DISABLED)) {
- if (on && (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_INACTIVE)) {
- mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE;
- enforceSafeMediaVolume();
- } else if (!on && (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE)) {
- mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE;
- mMusicActiveMs = 0;
- sendMsg(mAudioHandler,
- MSG_CHECK_MUSIC_ACTIVE,
- SENDMSG_REPLACE,
- 0,
- 0,
- null,
- MUSIC_ACTIVE_POLL_PERIOD_MS);
- }
- }
- }
- }
最终调用setSafeMediaVolumeEnabled()方法,将mSafeMediaVolumeState 状态设置为SAFE_MEDIA_VOLUME_INACTIVE, 这样再次进入checkSafeMediaVolume()时,则会返回ture,这样音量就能成功设置。
anroid的这套机制还有另外一个功能,就是在安全音量以上累积播放20H会再次弹出警告,实现如下:
- private void onSetWiredDeviceConnectionState(int device, int state, String name)
- {
- synchronized (mConnectedDevices) {
- ............
- if ((device & mSafeMediaVolumeDevices) != 0) {
- sendMsg(mAudioHandler,
- MSG_CHECK_MUSIC_ACTIVE,
- SENDMSG_REPLACE,
- 0,
- 0,
- null,
- MUSIC_ACTIVE_POLL_PERIOD_MS);
- }
- }
- ...........
- }
- private void onCheckMusicActive() {
- synchronized (mSafeMediaVolumeState) {
- if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_INACTIVE) {
- /* add by quinn start 2014-05-10 */
- int device, index;
- boolean streamATVIsActive = false, streamFMIsActive = false, streamMusicIsActive = false, musicOrFMOrATVActive = false;
- if (AudioSystem.isStreamActive(AudioSystem.STREAM_ATV, 0)) {
- device = getDeviceForStream(AudioSystem.STREAM_ATV);
- index = mStreamStates[AudioSystem.STREAM_ATV].getIndex(device, false);
- streamATVIsActive = true;
- } else if (AudioSystem.isStreamActive(AudioSystem.STREAM_FM, 0)) {
- device = getDeviceForStream(AudioSystem.STREAM_FM);
- index = mStreamStates[AudioSystem.STREAM_FM].getIndex(device, false);
- streamFMIsActive = true;
- } else {
- device = getDeviceForStream(AudioSystem.STREAM_MUSIC);
- index = mStreamStates[AudioSystem.STREAM_MUSIC].getIndex(device, false);
- streamMusicIsActive = true;
- }
- musicOrFMOrATVActive = (streamATVIsActive || streamFMIsActive || streamMusicIsActive);
- /* add by quinn end 2014-05-10 */
- if ((device & mSafeMediaVolumeDevices) != 0) {
- sendMsg(mAudioHandler,
- MSG_CHECK_MUSIC_ACTIVE,
- SENDMSG_REPLACE,
- 0,
- 0,
- null,
- MUSIC_ACTIVE_POLL_PERIOD_MS);
- /* add by quinn start 2014-05-10 */
- // int index = mStreamStates[AudioSystem.STREAM_MUSIC].getIndex(device, false /*lastAudible*/);
- // if (AudioSystem.isStreamActive(AudioSystem.STREAM_MUSIC, 0) &&
- if (musicOrFMOrATVActive &&
- /* add by quinn end 2014-05-10 */
- (index > mSafeMediaVolumeIndex)) {
- // Approximate cumulative active music time
- mMusicActiveMs += MUSIC_ACTIVE_POLL_PERIOD_MS;
- if (mMusicActiveMs > UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX) {
- setSafeMediaVolumeEnabled(true);
- mMusicActiveMs = 0;
- mVolumePanel.postDisplaySafeVolumeWarning();
- }
- }
- }
- }
- }
- }
从代码中可以看到,onCheckMusicActive()会通过Handler 每隔MUSIC_ACTIVE_POLL_PERIOD_MS段时间后再次调用onCheckMusicActive(),从而实现播放时间的累计,如果大于预定的时间,则再次弹出警告。
----------------------------------------------------------------------
终于将整个流程都走了一遍,从一开始自己瞎琢磨去做,到仔细研究android的代码,给了我一个深刻的教训,就是遇到问题要先思考,不能拿到手就去敲代码,如果一开始就仔细阅读AudioService的代码,详细了解音量调节是如何实现的,肯定能发现android默认对SafeVolume的支持,就不会傻乎乎的再自己去写一套很有问题的东西。以此为鉴,要踏实稳重,不能太浮躁。