Android 4.3实现类似iOS在音乐播放过程中如果有来电则音乐声音渐小铃声渐大的效果(二)

原创链接:http://blog.csdn.net/zhao_3546/article/details/12893167,转载请注明,谢谢。


距离上篇博客《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这个关键字,发现只有在

frameworks\base\packages\SystemUI\src\com\android\systemui\media\RingtonePlayer.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<IBinder, Client> 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中,有如下权限:

<uses-permission android:name="android.permission.READ_PHONE_STATE" />

又找了一下资料发现,主要的音乐播放器都通过如下方法去获取呼叫状态。我们通过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() : "<unknown>";
        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时,再去触发这两个函数的调用,即可实现我们需要的功能了。

具体代码实现,这里不一一列出了,改动点比较分散有点多。


Android 4.3实现类似iOS在音乐播放过程中如果有来电则音乐声音渐小铃声渐大的效果(二)

上一篇:微信公众平台开发案例


下一篇:【Android Developers Training】 65. 应用投影和相机视图