當我們在settings中試聽鈴聲,這時候突然來了一個電話,那么會出現試聽鈴聲和來電鈴聲同時播放的情況。當然,此情況同樣適用於鬧鍾鈴聲,媒體音樂播放等。那么怎么解決這個問題呢?這就需要當音頻焦點。---》
因為系統中可能會有多個應用程序會播放音頻,所以需要考慮他們之間該如何交互,為了避免多個應用程序同時播放音樂,Android 系統使用音頻焦點來進行統一管理,即只有獲得了音頻焦點的應用程序才可以播放音樂。 您的應用程序在開始播放音頻文件前,首先應該請求獲得音頻焦點,並且應該同時注冊監聽音頻焦點的丟失通知,即如果音頻焦點被系統或其他的應用程序搶占時,您的應用程序可以做出合適的響應。
首先,我要獲取一個音頻焦點並管理它。
private boolean requestFocus() { // Request audio focus for playback int result = mAudioManager.requestAudioFocus(afChangeListener, AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED; } OnAudioFocusChangeListener afChangeListener = new OnAudioFocusChangeListener() { public void onAudioFocusChange(int focusChange) { if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { // Pause playback if (mLocalPlayer !=null && mLocalPlayer.isPlaying()){ mLocalPlayer.pause(); } } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { // Resume playback startLocalPlayer(); } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { mAudioManager.abandonAudioFocus(afChangeListener); // Stop playback if (mLocalPlayer !=null && mLocalPlayer.isPlaying()){ mLocalPlayer.stop(); } } } };
可以很清晰的看見,上面的第一個方法是獲取音頻焦點,通過requestAudioFocus()來實現。而第二個方法就是對音頻焦點進行監聽並管理。在這里,要先知道以上幾個值的含義:
- AUDIOFOCUS_GAIN_TRANSIENT:只是短暫獲得,一會就釋放焦點,比如你只是想發個notification時用下一秒不到的鈴聲。
- AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:只是背景獲得,之前的音頻焦點使用者無需釋放焦點給我,我將與其共同使用。
- AUDIOFOCUS_GAIN:我要求完全獲得焦點,其他人需要釋放焦點。比如我要播放音樂了,這時就要搶占整個音頻焦點。
- AUDIOFOCUS_LOSS:你會長時間的失去焦點,所以不要指望在短時間內能獲得。請結束自己的相關音頻工作並做好收尾工作。
- AUDIOFOCUS_LOSS_TRANSIENT:你會短暫的失去音頻焦點,你可以暫停音樂,但不要釋放資源,因為你一會就可以奪回焦點並繼續使用。
知道了以上幾個字段的含義,在對應的狀態,我們就能做相應的處理。比如AUDIOFOCUS_LOSS_TRANSIENT短暫失去焦點,我們就暫停我們的音樂。AUDIOFOCUS_LOSS長期失去焦點,就直接停掉音樂。AUDIOFOCUS_GAIN我獲取了焦點,那么我就要開始播放音樂了(由於我完全獲取了焦點,其他音樂就無法播放了,自然當前就只有一個音樂進行播放)。
獲取音頻焦點,就要釋放音頻焦點:(在哪里釋放,就看當時的代碼吧)
private void destroyLocalPlayer() { if (mLocalPlayer != null) { mLocalPlayer.reset(); mLocalPlayer.release(); mLocalPlayer = null; synchronized (sActiveRingtones) { sActiveRingtones.remove(this); } } mAudioManager.abandonAudioFocus(afChangeListener); }
在解決這個問題的時候,我選擇在每次播放試聽鈴聲時,獲取音頻焦點(何時獲取,也要看當時代碼情況):
private void startLocalPlayer() { if (mLocalPlayer == null) { return; } synchronized (sActiveRingtones) { sActiveRingtones.add(this); } mLocalPlayer.setOnCompletionListener(mCompletionListener); if(requestFocus()){ mLocalPlayer.start(); } }
成功獲取到焦點,才可以播放當前的試聽鈴聲哦!
================================================================================================
更新!更新!這樣的改法果然引入了一個嚴重的bug,就是來電鈴聲不能播放!
為什么呢?首先看一下來電鈴聲播放的代碼:
private void handlePlay(SomeArgs args) { RingtoneFactory factory = (RingtoneFactory) args.arg1; Call incomingCall = (Call) args.arg2; args.recycle(); // don't bother with any of this if there is an EVENT_STOP waiting. if (mHandler.hasMessages(EVENT_STOP)) { return; } // If the Ringtone Uri is EMPTY, then the "None" Ringtone has been selected. Do not play // anything. if(Uri.EMPTY.equals(incomingCall.getRingtone())) { mRingtone = null; return; } ThreadUtil.checkNotOnMainThread(); if (mRingtone == null) { mRingtone = factory.getRingtone(incomingCall); if (mRingtone == null) { Uri ringtoneUri = incomingCall.getRingtone(); String ringtoneUriString = (ringtoneUri == null) ? "null" : ringtoneUri.toSafeString(); Log.event(null, Log.Events.ERROR_LOG, "Failed to get ringtone from factory. " + "Skipping ringing. Uri was: " + ringtoneUriString); return; } } handleRepeat(); } private void handleRepeat() { if (mRingtone == null) { return; } if (mRingtone.isPlaying()) { Log.d(this, "Ringtone already playing."); } else { mRingtone.play(); } // Repost event to restart ringer in {@link RESTART_RINGER_MILLIS}. synchronized(this) { if (!mHandler.hasMessages(EVENT_REPEAT)) { mHandler.sendEmptyMessageDelayed(EVENT_REPEAT, RESTART_RINGER_MILLIS); } } }
代碼路徑:packages\services\Telecomm\src\com\android\server\telecom\AsyncRingtonePlayer.java
來電鈴聲播放是mRingtone.play()這一句,也就是說,最終也是用到了Ringtone.java這個類,並且會調用到startLocalPlay()這個方法,而在這個方法里,剛才我們首先獲取了音頻焦點,並設定獲取成功才能播放。我通過追加log發現,來電鈴聲的時候,音頻焦點獲取失敗了,這讓我很費解,為什么試聽鈴聲就能夠獲取成功,來電鈴聲就不行呢?於是繼續向下分析:
mAudioManager.requestAudioFocus(afChangeListener,
AudioManager.STREAM_RING,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
首先進到AudioManager里看一看這個requestAudioFocus方法,最后一直追蹤到AudioService里,看一看這里的代碼:
public int requestAudioFocus(AudioAttributes aa, int durationHint, IBinder cb, IAudioFocusDispatcher fd, String clientId, String callingPackageName, int flags, IAudioPolicyCallback pcb) { // permission checks if ((flags & AudioManager.AUDIOFOCUS_FLAG_LOCK) == AudioManager.AUDIOFOCUS_FLAG_LOCK) { if (AudioSystem.IN_VOICE_COMM_FOCUS_ID.equals(clientId)) { if (PackageManager.PERMISSION_GRANTED != mContext.checkCallingOrSelfPermission( android.Manifest.permission.MODIFY_PHONE_STATE)) { Log.e(TAG, "Invalid permission to (un)lock audio focus", new Exception()); return AudioManager.AUDIOFOCUS_REQUEST_FAILED; } } else { // only a registered audio policy can be used to lock focus synchronized (mAudioPolicies) { if (!mAudioPolicies.containsKey(pcb.asBinder())) { Log.e(TAG, "Invalid unregistered AudioPolicy to (un)lock audio focus"); return AudioManager.AUDIOFOCUS_REQUEST_FAILED; } } } } return mMediaFocusControl.requestAudioFocus(aa, durationHint, cb, fd, clientId, callingPackageName, flags); }
代碼路徑:frameworks\base\services\core\java\com\android\server\audio\AudioService.java
上述方法中的AudioSystem.IN_VOICE_COMM_FOCUS_ID的注釋是:
/** * Constant to identify a focus stack entry that is used to hold the focus while the phone * is ringing or during a call. Used by com.android.internal.telephony.CallManager when * entering and exiting calls. */ public final static String IN_VOICE_COMM_FOCUS_ID = "AudioFocus_For_Phone_Ring_And_Calls";
這就顯而易見了,原來來電鈴聲注冊音頻焦點就會失敗。所以requestFocus的判斷加在這里並不合適。所以我將代碼改成:
private void startLocalPlayer() { if (mLocalPlayer == null) { return; } synchronized (sActiveRingtones) { sActiveRingtones.add(this); } mLocalPlayer.setOnCompletionListener(mCompletionListener); requestFocus(); mLocalPlayer.start(); }
判斷是否成功什么的,去見鬼吧!
