Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 4.3實現類似iOS在音樂播放過程中如果有來電則音樂聲音漸小鈴聲漸大的效果(二)

Android 4.3實現類似iOS在音樂播放過程中如果有來電則音樂聲音漸小鈴聲漸大的效果(二)

編輯:關於Android編程

 

 

距離上篇博客《Android 4.3實現類似iOS在音樂播放過程中如果有來電則音樂聲音漸小鈴聲漸大的效果》 已經快有4個月了,期間有空寫一點,直到今天才完整地寫完。

 

目前Android的實現是:有來電時,音樂聲音直接停止,鈴聲直接直接使用設置的鈴聲音量進行鈴聲播放。

Android 4.3實現類似iOS在音樂播放過程中如果有來電則音樂聲音漸小鈴聲漸大的效果。

 

如果要實現這個效果,首先要搞清楚兩大問題;

1、來電時的代碼主要實現流程。

2、主流音樂播放器在播放過程中,如果有來電,到底在收到了什麼事件後將音樂暫停了?

 

第一大問題,參見:《Android 4.3實現類似iOS在音樂播放過程中如果有來電則音樂聲音漸小鈴聲漸大的效果》。

現在我們分析第二個問題:主流音樂播放器在播放過程中,如果有來電,到底在收到了什麼事件後將音樂暫停了?

 

先來看看,Ringer.java 中播放鈴聲具體干了啥。第一次mRingtone對象是null,所以會通過r = RingtoneManager.getRingtone(mContext, mCustomRingtoneUri);對其進行初始化。

 

    private void makeLooper() {
        if (mRingThread == null) {
            mRingThread = new Worker(ringer);
            mRingHandler = new Handler(mRingThread.getLooper()) {
                @Override
                public void handleMessage(Message msg) {
                    Ringtone r = null;
                    switch (msg.what) {
                        case PLAY_RING_ONCE:
                            if (DBG) log(mRingHandler: PLAY_RING_ONCE...);
                            if (mRingtone == null && !hasMessages(STOP_RING)) {
                                // create the ringtone with the uri
                                if (DBG) log(creating ringtone:  + mCustomRingtoneUri);
                                r = RingtoneManager.getRingtone(mContext, mCustomRingtoneUri);
                                synchronized (Ringer.this) {
                                    if (!hasMessages(STOP_RING)) {
                                        mRingtone = r;
                                    }
                                }
                            }
                            r = mRingtone;
                            if (r != null && !hasMessages(STOP_RING) && !r.isPlaying()) {
                                PhoneUtils.setAudioMode();
                                r.play();
                                synchronized (Ringer.this) {
                                    if (mFirstRingStartTime < 0) {
                                        mFirstRingStartTime = SystemClock.elapsedRealtime();
                                    }
                                }
                            }
                            break;

下面的邏輯,調用了new Ringtone()來構造一個新的Ringtone對象,其中第二個參數為true,表示調用通過調用遠程對象來播放鈴聲。

    private static Ringtone getRingtone(final Context context, Uri ringtoneUri, int streamType) {
        ...

        try {
            Ringtone r = new Ringtone(context, true);
            if (streamType >= 0) {
                r.setStreamType(streamType);
            }

            ...

            r.setUri(ringtoneUri);
            return r;
        } catch (Exception ex) {
            Log.e(TAG, Failed to open ringtone  + ringtoneUri + :  + ex);
        }

        return null;
    }

既然這裡allowRemote為true,則 mRemotePlayer = mAudioManager.getRingtonePlayer(),再來看看mAudioManager中的getRingtonePlayer()干了什麼事。

 

    public Ringtone(Context context, boolean allowRemote) {
        mContext = context;
        = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
        mAllowRemote = allowRemote;
        mRemotePlayer = allowRemote ? mAudioManager.getRingtonePlayer() : null;
        mRemoteToken = allowRemote ? new Binder() : null;
    }
再進一步跟蹤,可以發現,mAudioManager其實是到了這個類的實例的引用。AudioService中getRingtonePlayer()的實現如下:

 

 

    @Override
    public void setRingtonePlayer(IRingtonePlayer player) {
        mContext.enforceCallingOrSelfPermission(REMOTE_AUDIO_PLAYBACK, null);
        mRingtonePlayer = player;
    }

    @Override
    public IRingtonePlayer getRingtonePlayer() {
        return mRingtonePlayer;
    }

 

又過了一下代碼,mRingtonePlayer也是外面設置進來的,那我們分析下到底是哪裡設置進來的。

搜索一下setRingtonePlayer這個關鍵字,發現只有在

frameworksasepackagesSystemUIsrccomandroidsystemuimediaRingtonePlayer.java 中有調用,

 

public class RingtonePlayer extends SystemUI {
    private static final String TAG = RingtonePlayer;
    private static final boolean LOGD = false;

    // TODO: support Uri switching under same IBinder

    private IAudioService mAudioService;

    private final NotificationPlayer mAsyncPlayer = new NotificationPlayer(TAG);
    private final HashMap mClients = Maps.newHashMap();

    @Override
    public void start() {
        mAsyncPlayer.setUsesWakeLock(mContext);

        mAudioService = IAudioService.Stub.asInterface(
                ServiceManager.getService(Context.AUDIO_SERVICE));
        try {
            mAudioService.setRingtonePlayer(mCallback);
        } catch (RemoteException e) {
            Slog.e(TAG, Problem registering RingtonePlayer:  + e);
        }
    }

    /**
     * Represents an active remote {@link Ringtone} client.
     */
    private class Client implements IBinder.DeathRecipient {
        private final IBinder mToken;
        private final Ringtone mRingtone;

        public Client(IBinder token, Uri uri, UserHandle user, int streamType) {
            mToken = token;

            mRingtone = new Ringtone(getContextForUser(user), false);
            mRingtone.setStreamType(streamType);
            mRingtone.setUri(uri);
        }

        @Override
        public void binderDied() {
            if (LOGD) Slog.d(TAG, binderDied() token= + mToken);
            synchronized (mClients) {
                mClients.remove(mToken);
            }
            mRingtone.stop();
        }
    }

    private IRingtonePlayer mCallback = new IRingtonePlayer.Stub() {
        @Override
        public void play(IBinder token, Uri uri, int streamType) throws RemoteException {
            if (LOGD) {
                Slog.d(TAG, play(token= + token + , uri= + uri + , uid=
                        + Binder.getCallingUid() + ));
            }
            Client client;
            synchronized (mClients) {
                client = mClients.get(token);
                if (client == null) {
                    final UserHandle user = Binder.getCallingUserHandle();
                    client = new Client(token, uri, user, streamType);
                    token.linkToDeath(client, 0);
                    mClients.put(token, client);
                }
            }
            client.mRingtone.play();
        }

        @Override
        public void stop(IBinder token) {
            if (LOGD) Slog.d(TAG, stop(token= + token + ));
            Client client;
            synchronized (mClients) {
                client = mClients.remove(token);
            }
            if (client != null) {
                client.mToken.unlinkToDeath(client, 0);
                client.mRingtone.stop();
            }
        }

        @Override
        public boolean isPlaying(IBinder token) {
            if (LOGD) Slog.d(TAG, isPlaying(token= + token + ));
            Client client;
            synchronized (mClients) {
                client = mClients.get(token);
            }
            if (client != null) {
                return client.mRingtone.isPlaying();
            } else {
                return false;
            }
        }

        @Override
        public void playAsync(Uri uri, UserHandle user, boolean looping, int streamType) {
            if (LOGD) Slog.d(TAG, playAsync(uri= + uri + , user= + user + ));
            if (Binder.getCallingUid() != Process.SYSTEM_UID) {
                throw new SecurityException(Async playback only available from system UID.);
            }

            mAsyncPlayer.play(getContextForUser(user), uri, looping, streamType);
        }

        @Override
        public void stopAsync() {
            if (LOGD) Slog.d(TAG, stopAsync());
            if (Binder.getCallingUid() != Process.SYSTEM_UID) {
                throw new SecurityException(Async playback only available from system UID.);
            }
            mAsyncPlayer.stop();
        }
    };

    private Context getContextForUser(UserHandle user) {
        try {
            return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user);
        } catch (NameNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        pw.println(Clients:);
        synchronized (mClients) {
            for (Client client : mClients.values()) {
                pw.print(  mToken=);
                pw.print(client.mToken);
                pw.print( mUri=);
                pw.println(client.mRingtone.getUri());
            }
        }
    }
}

在上面的start()方法中,有將 new IRingtonePlayer.Stub() 這個匿名類對象作為參數調用setRingtonePlayer()。

 

最終使用play()方法中,有構造了一個Client對象,這個Client類的實現在上面的代碼也有體現。重點看一下Client的構造函數,

mRingtone = new Ringtone(getContextForUser(user),false);

注意,第二個參數為false,即最終播放鈴聲使用的Ringtone對象,即是SystemUI在初始化時構造出來對象。

 

        public Client(IBinder token, Uri uri, UserHandle user, int streamType) {
            mToken = token;

            mRingtone = new Ringtone(getContextForUser(user), false);
            mRingtone.setStreamType(streamType);
            mRingtone.setUri(uri);
        }
兜了好大一圖,播放鈴聲使用的即是Ringtone對象,Ringtone中播放鈴聲的實現邏輯如下:
    public void play() {
        if (mLocalPlayer != null) {
            // do not play ringtones if stream volume is 0
            // (typically because ringer mode is silent).
            if (mAudioManager.getStreamVolume(mStreamType) != 0) {
                mLocalPlayer.start();
            }
        }
        ...
    }
再看看 Ringtone.mLocalPlayer 這個對象是在何時初始化出來的,

 

 

    public void setUri(Uri uri) {
        destroyLocalPlayer();

        mUri = uri;
        if (mUri == null) {
            return;
        }

        // TODO: detect READ_EXTERNAL and specific content provider case, instead of relying on throwing

        // try opening uri locally before delegating to remote player
        mLocalPlayer = new MediaPlayer();
        try {
            mLocalPlayer.setDataSource(mContext, mUri);
            mLocalPlayer.setAudioStreamType(mStreamType);
            mLocalPlayer.prepare();

        }
        ...
    }
好了,mLocalPlayer = new MediaPlayer();,這個就是一個普通的MediaPlayer對象。最後播放鈴音就是調用了MediaPlayer.start() 方法。

鈴聲是如何播放的,基本上都已經說清楚了,下面我們再來分析下到底是什麼事件觸發了第三方音樂播放器在來電時自動暫停了。

 

在《Android 4.3實現類似iOS在音樂播放過程中如果有來電則音樂聲音漸小鈴聲漸大的效果》中提及的過如下代碼,在 r.play() 之前,有調用 PhoneUtils.setAudioMode(),那再看看這個調用具體做了什麼事情。

 

 

    private void makeLooper() {
        if (mRingThread == null) {
            mRingThread = new Worker(ringer);
            mRingHandler = new Handler(mRingThread.getLooper()) {
                @Override
                public void handleMessage(Message msg) {
                    Ringtone r = null;
                    switch (msg.what) {
                        case PLAY_RING_ONCE:
                            if (DBG) log(mRingHandler: PLAY_RING_ONCE...);
                            if (mRingtone == null && !hasMessages(STOP_RING)) {
                                // create the ringtone with the uri
                                if (DBG) log(creating ringtone:  + mCustomRingtoneUri);
                                r = RingtoneManager.getRingtone(mContext, mCustomRingtoneUri);
                                synchronized (Ringer.this) {
                                    if (!hasMessages(STOP_RING)) {
                                        mRingtone = r;
                                    }
                                }
                            }
                            r = mRingtone;
                            if (r != null && !hasMessages(STOP_RING) && !r.isPlaying()) {
                                PhoneUtils.setAudioMode();
                                r.play();
                                synchronized (Ringer.this) {
                                    if (mFirstRingStartTime < 0) {
                                        mFirstRingStartTime = SystemClock.elapsedRealtime();
                                    }
                                }
                            }
                            break;
                        ...
                    }
                }
            };
        }
    }

 

 

一路調用過程如下:
PhoneUtils.setAudioMode(); --》 setAudioMode(CallManager cm); --》 CallManager.setAudioMode(),這裡面最後做了具體的邏輯:

 

    public void CallManager.setAudioMode() {
        Context context = getContext();
        if (context == null) return;
        AudioManager audioManager = (AudioManager)
                context.getSystemService(Context.AUDIO_SERVICE);
        PhoneConstants.State state = getState();
        int lastAudioMode = audioManager.getMode();

        // change the audio mode and request/abandon audio focus according to phone state,
        // but only on audio mode transitions
        switch (state) {
            case RINGING:
                int curAudioMode = audioManager.getMode();
                if (curAudioMode != AudioManager.MODE_RINGTONE) {
                    // only request audio focus if the ringtone is going to be heard
                    if (audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0) {
                        if (VDBG) Rlog.d(LOG_TAG, requestAudioFocus on STREAM_RING);
                        audioManager.requestAudioFocusForCall(AudioManager.STREAM_RING,
                                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
                    }
                    if(!mSpeedUpAudioForMtCall) {
                        audioManager.setMode(AudioManager.MODE_RINGTONE);
                    }
                }

                if (mSpeedUpAudioForMtCall && (curAudioMode != AudioManager.MODE_IN_CALL)) {
                    audioManager.setMode(AudioManager.MODE_IN_CALL);
                }
                break;
            ...

這裡有調用 audioManager.requestAudioFocusForCall(AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); 這個和預期的想法是一致的,這個在《Android如何判斷當前手機是否正在播放音樂,並獲取到正在播放的音樂的信息》已經詳細說明了。

 

對這個函數調用進行了屏蔽之後,重新編譯出Phone.apk測試後發現,我自己寫的Music測試應用在收到來電時,音樂還是在播放,但是第三方的音樂播放器,還是會暫停,還有哪裡沒有考慮到?

 

於是找了一份第三方的主流音樂播放器反編譯了一下,發現其Menifest.xml中,有如下權限:

 


又找了一下資料發現,主要的音樂播放器都通過如下方法去獲取呼叫狀態。我們通過state的值就知道現在的電話狀態了。
TelephonyManager.listen(new PhoneStateListener() {...}, PhoneStateListener.LISTEN_CALL_STATE);
在TelephonyManager中定義了三種狀態,分別是振鈴(RINGING),摘機(OFFHOOK)和空閒(IDLE)。

 

 

再來看看 TelephonyManager.listen() 的實現:

 

    public void listen(PhoneStateListener listener, int events) {
        String pkgForDebug = mContext != null ? mContext.getPackageName() : ;
        try {
            Boolean notifyNow = true;
            sRegistry.listen(pkgForDebug, listener.callback, events, notifyNow);
        } catch (RemoteException ex) {
            // system process dead
        } catch (NullPointerException ex) {
            // system process dead
        }
    }

 

sRegistry在TelephonyManager的構造函數中有初始化,sRegistry = ITelephonyRegistry.Stub.asInterface(ServiceManager.getService(telephony.registry)); 就是telephony.registry服務的一個遠程引用,最終請求會通過Binder走到TelephonyRegistry.listen()中:

 

    public void listen(String pkgForDebug, IPhoneStateListener callback, int events,
            boolean notifyNow) {
        int callerUid = UserHandle.getCallingUserId();
        int myUid = UserHandle.myUserId();
        ...
        if (events != 0) {
            /* Checks permission and throws Security exception */
            checkListenerPermission(events);

            synchronized (mRecords) {
                // register
                Record r = null;
                find_and_add: {
                    IBinder b = callback.asBinder();
                    final int N = mRecords.size();
                    for (int i = 0; i < N; i++) {
                        r = mRecords.get(i);
                        if (b == r.binder) {
                            break find_and_add;
                        }
                    }
                    r = new Record();
                    r.binder = b;
                    r.callback = callback;
                    r.pkgForDebug = pkgForDebug;
                    r.callerUid = callerUid;
                    mRecords.add(r);
                    if (DBG) Slog.i(TAG, listen: add new record= + r);
                }  ...
mRecords 其中就是一個ArrayList。TelephonyRegistry中保留了所有關注呼叫事件的應用注冊的Listener,在有呼叫事件發生的時候會通知給第三方應用;

 

 

TelephonyManager中還有如下方法,將有呼叫事件發生後,此方法會被調用通知給第三方應用:

 

    public void notifyCallState(int state, String incomingNumber) {
        if (!checkNotifyPermission(notifyCallState())) {
            return;
        }
        synchronized (mRecords) {
            mCallState = state;
            mCallIncomingNumber = incomingNumber;
            for (Record r : mRecords) {
                if ((r.events & PhoneStateListener.LISTEN_CALL_STATE) != 0) {
                    try {
                        r.callback.onCallStateChanged(state, incomingNumber);
                    } catch (RemoteException ex) {
                        mRemoveList.add(r.binder);
                    }
                }
            }
            handleRemoveListLocked();
        }
        broadcastCallStateChanged(state, incomingNumber);
    }

GsmCallTracker.handlePollCalls() --> updatePhoneState(); --> GSMPhone.notifyPhoneStateChanged(); --> DefaultPhoneNotifier.notifyPhoneState(Phone sender); --> mRegistry.notifyCallState(); --> 通知第三方應用 。。。

 

 

OK,搞清楚這個調用流程後,只要延遲對 GsmCallTracker.updatePhoneState() 和 AudioManager.requestAudioFocusForCall() 這兩個函數的調用,即等Music音量下降為0時,再去觸發這兩個函數的調用,即可實現我們需要的功能了。

具體代碼實現,這裡不一一列出了,改動點比較分散有點多。

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved