音頻焦點問題

當咱們在settings中試聽鈴聲,這時候忽然來了一個電話,那麼會出現試聽鈴聲和來電鈴聲同時播放的狀況。固然,此狀況一樣適用於鬧鐘鈴聲,媒體音樂播放等。那麼怎麼解決這個問題呢?這就須要當音頻焦點。---》java

由於系統中可能會有多個應用程序會播放音頻,因此須要考慮他們之間該如何交互,爲了不多個應用程序同時播放音樂,Android 系統使用音頻焦點來進行統一管理,即只有得到了音頻焦點的應用程序才能夠播放音樂。 您的應用程序在開始播放音頻文件前,首先應該請求得到音頻焦點,而且應該同時註冊監聽音頻焦點的丟失通知,即若是音頻焦點被系統或其餘的應用程序搶佔時,您的應用程序能夠作出合適的響應。android

首先,我要獲取一個音頻焦點並管理它。ide

      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()來實現。而第二個方法就是對音頻焦點進行監聽並管理。在這裏,要先知道以上幾個值的含義:post

  •      AUDIOFOCUS_GAIN_TRANSIENT:只是短暫得到,一會就釋放焦點,好比你只是想發個notification時用下一秒不到的鈴聲。
  •      AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:只是背景得到,以前的音頻焦點使用者無需釋放焦點給我,我將與其共同使用。
  •      AUDIOFOCUS_GAIN:我要求徹底得到焦點,其餘人須要釋放焦點。好比我要播放音樂了,這時就要搶佔整個音頻焦點。
  •      AUDIOFOCUS_LOSS:你會長時間的失去焦點,因此不要期望在短期內能得到。請結束本身的相關音頻工做並作好收尾工做。
  •     AUDIOFOCUS_LOSS_TRANSIENT:你會短暫的失去音頻焦點,你能夠暫停音樂,但不要釋放資源,由於你一會就能夠奪回焦點並繼續使用。

知道了以上幾個字段的含義,在對應的狀態,咱們就能作相應的處理。好比AUDIOFOCUS_LOSS_TRANSIENT短暫失去焦點,咱們就暫停咱們的音樂。AUDIOFOCUS_LOSS長期失去焦點,就直接停掉音樂。AUDIOFOCUS_GAIN我獲取了焦點,那麼我就要開始播放音樂了(因爲我徹底獲取了焦點,其餘音樂就沒法播放了,天然當前就只有一個音樂進行播放)。this

獲取音頻焦點,就要釋放音頻焦點:(在哪裏釋放,就看當時的代碼吧)spa

    private void destroyLocalPlayer() {
        if (mLocalPlayer != null) {
            mLocalPlayer.reset();
            mLocalPlayer.release();
            mLocalPlayer = null;
            synchronized (sActiveRingtones) {
                sActiveRingtones.remove(this);
            }
        }
        mAudioManager.abandonAudioFocus(afChangeListener);
    }

在解決這個問題的時候,我選擇在每次播放試聽鈴聲時,獲取音頻焦點(什麼時候獲取,也要看當時代碼狀況):rest

    private void startLocalPlayer() {
        if (mLocalPlayer == null) {
            return;
        }
        synchronized (sActiveRingtones) {
            sActiveRingtones.add(this);
        }
        mLocalPlayer.setOnCompletionListener(mCompletionListener);
        if(requestFocus()){
            mLocalPlayer.start();
        }
    }

成功獲取到焦點,才能夠播放當前的試聽鈴聲哦!code

================================================================================================server

更新!更新!這樣的改法果真引入了一個嚴重的bug,就是來電鈴聲不能播放!blog

爲何呢?首先看一下來電鈴聲播放的代碼:

    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();
    }

判斷是否成功什麼的,去見鬼吧!

相關文章
相關標籤/搜索