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

來源:互聯網
上載者:User

 

 

距離上篇部落格《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時,再去觸發這兩個函數的調用,即可實現我們需要的功能了。

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

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.