前语

接到一个开发需求,需求定制化开发一个安全音量功用;此前有了解过为了契合欧盟等有关国家和地区的规定,原生Android是有自带一个安全音量功用的,想要定制则先要了解这个功用原先长什么样子,下面我们就从一个体系工程师的角度动身去探寻一下,原生Android的安全音量功用是怎么实现的。

安全音量装备

安全音量的相关装备都在framework的config.xml里边,能够直接修正或许overlay装备修正其默许值。

<!-- Whether safe headphone volume is enabled or not (country specific). -->
<bool name="config_safe_media_volume_enabled">true</bool>
<!-- 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">10</integer>

config_safe_media_volume_enabled是安全音量功用的总开关,config_safe_media_volume_index则是标明触发安全音量弹框的音量巨细值。

安全音量相关流程

安全音量的首要流程都在AudioService里边,其大致流程如下图所示:

graph TD
A(onSystemReady)-->|MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED|B[onConfigureSafeVolume]
    B-.->E[checkSafeMediaVolume]
    AudioManager-->C[adjustStreamVolume]
    AudioManager-->D[setStreamVolume]
    C-->E
    D-->E
    E-->F(showSafetyWarningH)

onSystemReady 初始化

体系启动进程省略不表,在体系启动完成后会调用onSystemReady;在onSystemReady中,service会发送一个MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED的msg,强制装备安全音量。

public void onSystemReady() {
    ...
    sendMsg(mAudioHandler,
    MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED,
    SENDMSG_REPLACE,
    0,
    0,
    TAG,
    SystemProperties.getBoolean("audio.safemedia.bypass", false) ?
        0 : SAFE_VOLUME_CONFIGURE_TIMEOUT_MS);
    ...
}

发送的MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED会调用onConfigureSafeVolume()来进行安全音量的装备

onConfigureSafeVolume() 安全音量装备

    private void onConfigureSafeVolume(boolean force, String caller) {
        synchronized (mSafeMediaVolumeStateLock) {
            //Mobile contry code,国家代码,首要用来区别不同国家,部分国家策略可能会不一致
            int mcc = mContext.getResources().getConfiguration().mcc;
            if ((mMcc != mcc) || ((mMcc == 0) && force)) {
                //从config_safe_media_volume_index中获取回来的安全音量触发阈值
                mSafeMediaVolumeIndex = mContext.getResources().getInteger(
                        com.android.internal.R.integer.config_safe_media_volume_index) * 10;
                mSafeUsbMediaVolumeIndex = getSafeUsbMediaVolumeIndex();
                //根据audio.safemedia.force特点值或许value装备的值来决议是否使能安全音量
                boolean safeMediaVolumeEnabled =
                        SystemProperties.getBoolean("audio.safemedia.force", false)
                        || mContext.getResources().getBoolean(
                                com.android.internal.R.bool.config_safe_media_volume_enabled);
                //确认是否需求bypass掉安全音量功用
                boolean safeMediaVolumeBypass =
                        SystemProperties.getBoolean("audio.safemedia.bypass", false);
                // 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 && !safeMediaVolumeBypass) {
                    persistedState = SAFE_MEDIA_VOLUME_ACTIVE; //这个值只能是disable或许active,不能是inactive,首要用于下次启动。
                    // 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) {
                        if (mMusicActiveMs == 0) { //mMusicActiveMs首要用于计数,当安全音量弹框弹出时,假如按了确认,这个值便开始递增,当其达到UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX时,则从头使能安全音量
                            mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE;
                            enforceSafeMediaVolume(caller);
                        } else {
                            //跑到这里则表示现已弹过安全音量警示了,而且按了确认,所以把值设置为inactive
                            // We have existing playback time recorded, already confirmed.
                            mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_INACTIVE;
                        }
                    }
                } 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);
            }
        }
    }

由上可知,onConfigureSafeVolume()首要用于装备和使能安全音量功用,而且经过发送MSG_PERSIST_SAFE_VOLUME_STATE来耐久化安全音量装备的值,这个耐久化的值只能是active或许disabled。

case MSG_PERSIST_SAFE_VOLUME_STATE:
    onPersistSafeVolumeState(msg.arg1);
    break;
....
....
private void onPersistSafeVolumeState(int state) {
    Settings.Global.putInt(mContentResolver,
            Settings.Global.AUDIO_SAFE_VOLUME_STATE,
            state);
}

安全音量触发

从实际操作可知,安全音量触发条件是:音量增大到指定值。
从调理音量的代码动身,在调用mAudioManager.adjustStreamVolume和mAudioManager.setStreamVolume时,终究会调用到AudioService中的同名办法,在执行该办法的内部:

protected void adjustStreamVolume(int streamType, int direction, int flags,
        String callingPackage, String caller, int uid) {
    ...
    ...
    ...
    } else if ((direction == AudioManager.ADJUST_RAISE) &&
            !checkSafeMediaVolume(streamTypeAlias, aliasIndex + step, device)) {
        Log.e(TAG, "adjustStreamVolume() safe volume index = " + oldIndex);
        mVolumeController.postDisplaySafeVolumeWarning(flags);
    ....
    ...
private void setStreamVolume(int streamType, int index, int flags, String callingPackage,
            String caller, int uid) {
    ....
    ....
        if (!checkSafeMediaVolume(streamTypeAlias, index, device)) {
                mVolumeController.postDisplaySafeVolumeWarning(flags);
                mPendingVolumeCommand = new StreamVolumeCommand(
                                                    streamType, index, flags, device);
            } else {
                onSetStreamVolume(streamType, index, flags, device, caller);
                index = mStreamStates[streamType].getIndex(device);
            }
    ....
    ....

由以上代码能够看出,其安全音量弹框正告的触发地方就在checkSafeMediaVolume办法附近处,而且都是经过mVolumeController这个远程服务去调用UI显示安全音量弹框正告,但两种调理音量的办法,触发效果略有不同:

  • adjustStreamVolume:当音量步进方向是上升而且checkSafeMediaVolume返回false时,直接弹出正告框;由于正告框占有了焦点,此刻无法进行UI操作,而且再按音量+键时,会持续触发这个弹框,导致无法本质性地调整音量;
  • setStreamVolume:当传入的音量形参大于安全音量阈值,会触发checkSafeMediaVolume返回false,弹出安全音量正告框;而且会经过mPendingVolumeCommand保存设置的音量值,待关掉安全音量后再赋回来。
private boolean checkSafeMediaVolume(int streamType, int index, int device) {
        synchronized (mSafeMediaVolumeStateLock) {
            if ((mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE) &&
                    (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_MUSIC) &&
                    ((device & mSafeMediaVolumeDevices) != 0) &&
                    (index > safeMediaVolumeIndex(device))) {
                return false;
            }
            return true;
        }
    }

以上是安全音量判别条件checkSafeMediaVolume,能够看出其判别首要根据以下条件:

  • mSafeMediaVolumeState是否为active,这个是安全音量功用的开关变量;
  • 音频流是否为STREAM_MUSIC,只针对该音频流做安全音量;
  • 设备类型,默许mSafeMediaVolumeDevices值如下:
    /*package*/ final int mSafeMediaVolumeDevices = AudioSystem.DEVICE_OUT_WIRED_HEADSET
            | AudioSystem.DEVICE_OUT_WIRED_HEADPHONE
            | AudioSystem.DEVICE_OUT_USB_HEADSET;

由上可知,只针对耳机播映或许USB耳机才做安全音量功用,如有需求体系工程师可自行装备其他设备;

  • 音量巨细,只有音量index超越safeMediaVolumeIndex获取的值,才需求弹出安全音量警示框,而safeMediaVolumeIndex的值则是本文开头在config.xml中装备的config_safe_media_volume_index所得出的;

UI部分

上面有说到,当满足安全音量警示框的触发条件时,会经过mVolumeController这个远程服务去调用UI显示安全音量弹框正告,其调用链条有点长,半途略过不表,其终究会走到VolumeDialogImpl.java的showSafetyWarningH,如下:

public class VolumeDialog {
    ...
    private void showSafetyWarningH(int flags) {
        if ((flags & (AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_SHOW_UI_WARNINGS)) != 0
                || mShowing) {
            synchronized (mSafetyWarningLock) {
                if (mSafetyWarning != null) {
                    return;
                }
                mSafetyWarning = new SafetyWarningDialog(mContext, mController.getAudioManager()) {
                    @Override
                    protected void cleanUp() {
                        synchronized (mSafetyWarningLock) {
                            mSafetyWarning = null;
                        }
                        recheckH(null);
                    }
                };
                mSafetyWarning.show();
            }
            recheckH(null);
        }
        rescheduleTimeoutH();
    }
    ...
}

UI装备部分首要在SafetyWarningDialog.java,代码就不贴了,可自行检查,其本质是一个对话框,在弹出时会抢占UI焦点,假如不点击确认或撤销,则无法操作其他UI;点击确认后,会调用mAudioManager.disableSafeMediaVolume()来暂时封闭安全音量正告功用,但上面有说到,当点击确认之后其实是启动了一个变量mMusicActiveMs的计数,当这个计数到达必定值(默许是20个小时),安全音量会从头启动;但假如点击了撤销,再持续调大音量时,安全音量弹框还是会持续弹出;

disableSafeMediaVolume()

上面有说到,在安全音量弹框弹出后,点击确认能够暂时封闭安全音量正告功用,其实终究会调用到AudioService中的disableSafeMediaVolume(),代码如下:

public void disableSafeMediaVolume(String callingPackage) {
        enforceVolumeController("disable the safe media volume");
        synchronized (mSafeMediaVolumeStateLock) {
            setSafeMediaVolumeEnabled(false, callingPackage);
            if (mPendingVolumeCommand != null) {
                onSetStreamVolume(mPendingVolumeCommand.mStreamType,
                                  mPendingVolumeCommand.mIndex,
                                  mPendingVolumeCommand.mFlags,
                                  mPendingVolumeCommand.mDevice,
                                  callingPackage);
                mPendingVolumeCommand = null;
            }
        }
    }

一方面是调用setSafeMediaVolumeEnabled来暂时封闭安全音量功用,另一方面会把此前临时挂起的设置音量mPendingVolumeCommand从头设置回去。

小结

简略来讲,Android原生的安全音量功用默许强制翻开,在刺进耳机后,音量调理到指定阈值时,会触发音量正告弹框,该弹框会抢走焦点,不点击确认或撤销无法进行其他操作;在点击确认后,默许操作者自己答应设备音量持续往上调,但此刻体系会开始一个默许为20分钟的倒计时,在这20分钟内音量随意调理都不会触发安全音量弹框,但20分钟完毕后,音量大于阈值时会持续触发安全音量弹框,提醒使用者留意。