android?原生安全音量配置邏輯設(shè)計(jì)詳解
前言
接到一個(gè)開發(fā)需求,需要定制化開發(fā)一個(gè)安全音量功能;此前有了解過為了符合歐盟等有關(guān)國家和地區(qū)的規(guī)定,原生Android是有自帶一個(gè)安全音量功能的,想要定制則先要了解這個(gè)功能原先長什么樣子,下面我們就從一個(gè)系統(tǒng)工程師的角度出發(fā)去探尋一下,原生Android的安全音量功能是如何實(shí)現(xiàn)的。
安全音量配置
安全音量的相關(guān)配置都在framework的config.xml里面,可以直接修改或者overlay配置修改其默認(rèn)值。
<!-- 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是安全音量功能的總開關(guān),config_safe_media_volume_index則是表明觸發(fā)安全音量彈框的音量大小值。
安全音量相關(guān)流程
安全音量的主要流程都在AudioService里面,其大致流程如下圖所示:
onSystemReady 初始化
系統(tǒng)啟動(dòng)過程略去不表,在系統(tǒng)啟動(dòng)完成后會(huì)調(diào)用onSystemReady;在onSystemReady中,service會(huì)發(fā)送一個(gè)MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED的msg,強(qiáng)制配置安全音量。
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); ... }
發(fā)送的MSG_CONFIGURE_SAFE_MEDIA_VOLUME_FORCED會(huì)調(diào)用onConfigureSafeVolume()來進(jìn)行安全音量的配置
onConfigureSafeVolume() 安全音量配置
private void onConfigureSafeVolume(boolean force, String caller) { synchronized (mSafeMediaVolumeStateLock) { //Mobile contry code,國家代碼,主要用來區(qū)分不同國家,部分國家策略可能會(huì)不一致 int mcc = mContext.getResources().getConfiguration().mcc; if ((mMcc != mcc) || ((mMcc == 0) && force)) { //從config_safe_media_volume_index中獲取回來的安全音量觸發(fā)閾值 mSafeMediaVolumeIndex = mContext.getResources().getInteger( com.android.internal.R.integer.config_safe_media_volume_index) * 10; mSafeUsbMediaVolumeIndex = getSafeUsbMediaVolumeIndex(); //根據(jù)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); //確認(rèn)是否需要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; //這個(gè)值只能是disable或者active,不能是inactive,主要用于下次啟動(dòng)。 // 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主要用于計(jì)數(shù),當(dāng)安全音量彈框彈出時(shí),如果按了確定,這個(gè)值便開始遞增,當(dāng)其達(dá)到UNSAFE_VOLUME_MUSIC_ACTIVE_MS_MAX時(shí),則重新使能安全音量 mSafeMediaVolumeState = SAFE_MEDIA_VOLUME_ACTIVE; enforceSafeMediaVolume(caller); } else { //跑到這里則表示已經(jīng)彈過安全音量警示了,并且按了確定,所以把值設(shè)置為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; //持久化當(dāng)前安全音量的狀態(tài) sendMsg(mAudioHandler, MSG_PERSIST_SAFE_VOLUME_STATE, SENDMSG_QUEUE, persistedState, 0, null, 0); } } }
由上可知,onConfigureSafeVolume()主要用于配置和使能安全音量功能,并且通過發(fā)送MSG_PERSIST_SAFE_VOLUME_STATE來持久化安全音量配置的值,這個(gè)持久化的值只能是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); }
安全音量觸發(fā)
從實(shí)際操作可知,安全音量觸發(fā)條件是:音量增大到指定值。 從調(diào)節(jié)音量的代碼出發(fā),在調(diào)用mAudioManager.adjustStreamVolume和mAudioManager.setStreamVolume時(shí),最終會(huì)調(diào)用到AudioService中的同名方法,在執(zhí)行該方法的內(nèi)部:
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); } .... ....
由以上代碼可以看出,其安全音量彈框警告的觸發(fā)地方就在checkSafeMediaVolume方法附近處,并且都是通過mVolumeController這個(gè)遠(yuǎn)程服務(wù)去調(diào)用UI顯示安全音量彈框警告,但兩種調(diào)節(jié)音量的方法,觸發(fā)效果略有不同:
- adjustStreamVolume:當(dāng)音量步進(jìn)方向是上升并且checkSafeMediaVolume返回false時(shí),直接彈出警告框;由于警告框占據(jù)了焦點(diǎn),此時(shí)無法進(jìn)行UI操作,并且再按音量+鍵時(shí),會(huì)繼續(xù)觸發(fā)這個(gè)彈框,導(dǎo)致無法實(shí)質(zhì)性地調(diào)整音量;
- setStreamVolume:當(dāng)傳入的音量形參大于安全音量閾值,會(huì)觸發(fā)checkSafeMediaVolume返回false,彈出安全音量警告框;并且會(huì)通過mPendingVolumeCommand保存設(shè)置的音量值,待關(guān)掉安全音量后再賦回來。
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,可以看出其判斷主要根據(jù)以下條件:
- mSafeMediaVolumeState是否為active,這個(gè)是安全音量功能的開關(guān)變量;
- 音頻流是否為STREAM_MUSIC,只針對(duì)該音頻流做安全音量;
- 設(shè)備類型,默認(rèn)mSafeMediaVolumeDevices值如下:
/*package*/ final int mSafeMediaVolumeDevices = AudioSystem.DEVICE_OUT_WIRED_HEADSET | AudioSystem.DEVICE_OUT_WIRED_HEADPHONE | AudioSystem.DEVICE_OUT_USB_HEADSET;
由上可知,只針對(duì)耳機(jī)播放或者USB耳機(jī)才做安全音量功能,如有需要系統(tǒng)工程師可自行配置其他設(shè)備;
- 音量大小,只有音量index超過safeMediaVolumeIndex獲取的值,才需要彈出安全音量警示框,而safeMediaVolumeIndex的值則是本文開頭在config.xml中配置的config_safe_media_volume_index所得出的;
UI部分
上面有提到,當(dāng)滿足安全音量警示框的觸發(fā)條件時(shí),會(huì)通過mVolumeController這個(gè)遠(yuǎn)程服務(wù)去調(diào)用UI顯示安全音量彈框警告,其調(diào)用鏈條有點(diǎn)長,中途略過不表,其最終會(huì)走到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,代碼就不貼了,可自行查看,其本質(zhì)是一個(gè)對(duì)話框,在彈出時(shí)會(huì)搶占UI焦點(diǎn),如果不點(diǎn)擊確定或取消,則無法操作其他UI;
點(diǎn)擊確定后,會(huì)調(diào)用mAudioManager.disableSafeMediaVolume()來暫時(shí)關(guān)閉安全音量警告功能,但上面有提到,當(dāng)點(diǎn)擊確定之后其實(shí)是啟動(dòng)了一個(gè)變量mMusicActiveMs的計(jì)數(shù),當(dāng)這個(gè)計(jì)數(shù)到達(dá)一定值(默認(rèn)是20個(gè)小時(shí)),安全音量會(huì)重新啟動(dòng);
但如果點(diǎn)擊了取消,再繼續(xù)調(diào)大音量時(shí),安全音量彈框還是會(huì)繼續(xù)彈出;
disableSafeMediaVolume()
上面有提到,在安全音量彈框彈出后,點(diǎn)擊確定可以暫時(shí)關(guān)閉安全音量警告功能,其實(shí)最終會(huì)調(diào)用到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; } } }
一方面是調(diào)用setSafeMediaVolumeEnabled來暫時(shí)關(guān)閉安全音量功能,另一方面會(huì)把此前臨時(shí)掛起的設(shè)置音量mPendingVolumeCommand重新設(shè)置回去。
小結(jié)
簡(jiǎn)單來講,Android原生的安全音量功能默認(rèn)強(qiáng)制打開,在插入耳機(jī)后,音量調(diào)節(jié)到指定閾值時(shí),會(huì)觸發(fā)音量警告彈框,該彈框會(huì)搶走焦點(diǎn),不點(diǎn)擊確定或取消無法進(jìn)行其他操作;在點(diǎn)擊確定后,默認(rèn)操作者本人允許設(shè)備音量繼續(xù)往上調(diào),但此時(shí)系統(tǒng)會(huì)開始一個(gè)默認(rèn)為20分鐘的倒計(jì)時(shí),在這20分鐘內(nèi)音量隨意調(diào)節(jié)都不會(huì)觸發(fā)安全音量彈框,但20分鐘結(jié)束后,音量大于閾值時(shí)會(huì)繼續(xù)觸發(fā)安全音量彈框,提醒使用者注意。
以上就是android 原生安全音量配置邏輯設(shè)計(jì)詳解的詳細(xì)內(nèi)容,更多關(guān)于android 原生安全音量邏輯的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android創(chuàng)建和使用數(shù)據(jù)庫SQLIte
這篇文章主要為大家詳細(xì)介紹了Android創(chuàng)建和使用數(shù)據(jù)庫SQLIte的相關(guān)資料,感興趣的小伙伴們可以參考一下2016-05-05Android自定義View實(shí)現(xiàn)LayoutParams的方法詳解
這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)LayoutParams,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-02-02Android ScrollView嵌套ExpandableListView顯示不正常的問題的解決辦法
這篇文章主要介紹了Android ScrollView嵌套ExpandableListView顯示不正常的問題的解決辦法的相關(guān)資料,需要的朋友可以參考下2017-02-02Android Studio ADB網(wǎng)絡(luò)調(diào)試匯總
這篇文章主要為大家詳細(xì)介紹了Android Studio ADB網(wǎng)絡(luò)調(diào)試的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-05-05Android實(shí)現(xiàn)調(diào)用攝像頭和相冊(cè)的方法
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)調(diào)用攝像頭和相冊(cè)的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-04-04Android Studio kotlin生成編輯類注釋代碼
這篇文章主要介紹了Android Studio kotlin生成編輯類注釋代碼,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-03-03