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

目前Android的实现是:有来电时,音乐声音直接停止,铃声直接直接使用设置的铃声音量进行铃声播放。

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

如果要实现这个效果,首先要搞清楚两大问题;

1、来电时的代码主要实现流程。

2、主流音乐播放器在播放过程中,如果有来电,到底在收到了什么事件后将音乐暂停了?

一:来电时的代码主要实现流程

我不是第一研究来电代码的人,网上已经有高手对这个流程剖析过,不是不完全符合我的要求,我参考过的比较有价值的是如下两个文档:

Android来电时停止音乐播放的流程

Android源码分析:Telephony部分–phone进程

有参考价值,但都分析很比较粗略,只能自己再一步一步跟源码进一步了解。

因为我做的事情主要是有来电时,修改铃音的效果,所以不用从头跟进,从响铃通知到达Phone.apk中分析起即可,更细可以参考下上面的两个链接。

分析之前,还是有必要对Phone整体的初始化流程有个基本认识,不然后面跟到沟里去。

Phone.apk 的AndroidManifest.xml中的application的说明:

  1. <application android:name="PhoneApp"
  2. android:persistent="true"
  3. android:label="@string/phoneAppLabel"
  4. android:icon="@mipmap/ic_launcher_phone">

那再看看PhoneApp的实现:

  1. /**
  2. * Top-level Application class for the Phone app.
  3. */
  4. public class PhoneApp extends Application {
  5. PhoneGlobals mPhoneGlobals;
  6. public PhoneApp() {
  7. }
  8. @Override
  9. public void onCreate() {
  10. if (UserHandle.myUserId() == 0) {
  11. // We are running as the primary user, so should bring up the
  12. // global phone state.
  13. mPhoneGlobals = new PhoneGlobals(this);
  14. mPhoneGlobals.onCreate();
  15. }
  16. }
  17. @Override
  18. public void onConfigurationChanged(Configuration newConfig) {
  19. if (mPhoneGlobals != null) {
  20. mPhoneGlobals.onConfigurationChanged(newConfig);
  21. }
  22. super.onConfigurationChanged(newConfig);
  23. }

从源码来看,这个类非常的简单,主要就是对 mPhoneGlobals 属性进行了创建和初始化。再来分析 PhoneGlobals 是如何初始化的:

  1. public void PhoneGlobals.onCreate() {
  2. ...
  3. if (phone == null) {
  4. // Initialize the telephony framework
  5. PhoneFactory.makeDefaultPhones(this);
  6. // Get the default phone
  7. phone = PhoneFactory.getDefaultPhone();
  8. // Start TelephonyDebugService After the default phone is created.
  9. Intent intent = new Intent(this, TelephonyDebugService.class);
  10. startService(intent);
  11. mCM = CallManager.getInstance();
  12. mCM.registerPhone(phone);
  13. // Create the NotificationMgr singleton, which is used to display
  14. // status bar icons and control other status bar behavior.
  15. notificationMgr = NotificationMgr.init(this);
  16. phoneMgr = PhoneInterfaceManager.init(this, phone);
  17. mHandler.sendEmptyMessage(EVENT_START_SIP_SERVICE);
  18. int phoneType = phone.getPhoneType();
  19. if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
  20. // Create an instance of CdmaPhoneCallState and initialize it to IDLE
  21. cdmaPhoneCallState = new CdmaPhoneCallState();
  22. cdmaPhoneCallState.CdmaPhoneCallStateInit();
  23. }
  24. ...
  25. ringer = Ringer.init(this);
  26. ...
  27. notifier = CallNotifier.init(this, phone, ringer, new CallLogAsync());
  28. ...
  29. }
  30. ...
  31. }

PhonePhoneGlobals.onCreate()  中干了很多事情,其中我列出的内容,都是我个人觉得比较重要的部分,建议重点看一下,后面会用得到。

PhoneFactory.makeDefaultPhones(this) 和 phone = PhoneFactory.getDefaultPhone() 这两个函数调用,建议也跟进去重点看一下,这里面做了比较重要的事情,

底层来电事件就是通过类似注册表注册机制做好一系列地注册之后,后面有不同事件过来后,将相应的消息分发特定的对象去处理。

我修改了Phone的源码,将日志全部放开,然后将重新编译得到的 Phone.apk 更新到手机中,真实地拨打了一个电话,

日志量比较大,只列出开头的一小部分,具体日志如下:

  1. 10-10 21:20:18.862: D/CallNotifier(814): RING before NEW_RING, skipping
  2. 10-10 21:20:18.862: D/InCallScreen(814): Handler: handling message { what=123 when=0 obj=android.os.AsyncResult@418f38f8 } while not in foreground
  3. 10-10 21:20:18.862: D/InCallScreen(814): onIncomingRing()...
  4. 10-10 21:20:20.834: D/CallNotifier(814): PHONE_ENHANCED_VP_OFF...
  5. 10-10 21:20:20.844: D/CallNotifier(814): RINGING... (new)
  6. 10-10 21:20:20.844: D/CallNotifier(814): onNewRingingConnection(): state = RINGING, conn = {  incoming: true state: INCOMING post dial state: NOT_STARTED }
  7. 10-10 21:20:20.844: D/CallNotifier(814): Incoming number is: 02556781234
  8. 10-10 21:20:20.844: V/BlacklistProvider(814): Query uri=content://blacklist/bynumber/02556781234, match=2
  9. 10-10 21:20:20.864: D/CallNotifier(814): stopSignalInfoTone: Stopping SignalInfo tone player
  10. 10-10 21:20:20.864: D/CallNotifier(814): - connection is ringing!  state = INCOMING
  11. 10-10 21:20:20.864: D/CallNotifier(814): Holding wake lock on new incoming connection.
  12. 10-10 21:20:20.864: D/PhoneApp(814): requestWakeState(PARTIAL)...
  13. 10-10 21:20:20.864: D/PhoneUtils(814): PhoneUtils.startGetCallerInfo: new query for phone number...
  14. ...

从上面的日志可以看出,当有来电时,其实是 PHONE_NEW_RINGING_CONNECTION 这个事件交给了Phoe应用来处理了。

底层的流程大致如下,更详细的参见《Android来电时停止音乐播放的流程》:

        1).    RIL在接收到请求的时候会向GsmCallTracker广播消息,而GsmCallTracker在接收到该消息的时候会继续
                向上层的CallManager广播
        2).    CallManager在这个只充当了一个转播者的角色,它会继续将消息传播给CallNotifier
        3).    而CallNotifier接收到消息后会判断来电是否需要查询,不查询则会直接设置声音模式(包含停止音乐播放并
                开始响铃)并显示来电界面等待用户的下一步操作; 若需要查询则会在查询接收后执行此部分过程 

从代码层面上,这个是如何体现的呢?

1、RIL怎么将消息传递给 GsmCallTracker 的,这个没有研究,跳过。

2、GsmCallTracker如何将消息向上层传播的?来看看代码:GsmCallTracker这个类本身是继承自Handler这个类的,看看handleMessage (Message msg)实现:

  1. handleMessage (Message msg) {
  2. AsyncResult ar;
  3. switch (msg.what) {
  4. case EVENT_POLL_CALLS_RESULT:
  5. ar = (AsyncResult)msg.obj;
  6. if (msg == lastRelevantPoll) {
  7. if (DBG_POLL) log(
  8. "handle EVENT_POLL_CALL_RESULT: set needsPoll=F");
  9. needsPoll = false;
  10. lastRelevantPoll = null;
  11. handlePollCalls((AsyncResult)msg.obj);
  12. }
  13. break;
  14. ...
  15. }
  16. }

再看看handlePollCalls()的实现:

  1. protected synchronized void
  2. handlePollCalls(AsyncResult ar) {
  3. ...
  4. if (newRinging != null) {
  5. phone.notifyNewRingingConnection(newRinging);
  6. }
  7. ...
  8. updatePhoneState();
  9. ...
  10. }

重点关注有来电相关的代码, GSMPhone.notifyNewRingingConnection(newRinging); -->  PhoneBase.notifyNewRingingConnectionP()

--> PhoneBase.mNewRingingConnectionRegistrants.notifyRegistrants(ar) --> ...
一路跟下去,到 Registrant.internalNotifyRegistrant(),这个是这个 h 到底对应的是哪个Handler呢?

  1. /*package*/ void
  2. internalNotifyRegistrant (Object result, Throwable exception)
  3. {
  4. Handler h = getHandler();
  5. if (h == null) {
  6. clear();
  7. } else {
  8. Message msg = Message.obtain();
  9. msg.what = what;
  10. msg.obj = new AsyncResult(userObj, result, exception);
  11. h.sendMessage(msg);
  12. }
  13. }

我们在前面看的初始化相关的代码的作用就体现出来了,PhoneBase.mNewRingingConnectionRegistrants这个列表中的内容是何时放进去的呢?

  1. /** Private constructor; @see init() */
  2. private CallNotifier(PhoneGlobals app, Phone phone, Ringer ringer, CallLogAsync callLog) {
  3. mApplication = app;
  4. mCM = app.mCM;
  5. mCallLog = callLog;
  6. mAudioManager = (AudioManager) mApplication.getSystemService(Context.AUDIO_SERVICE);
  7. registerForNotifications();
  8. ...
  1. private void registerForNotifications() {
  2. mCM.registerForNewRingingConnection(this, PHONE_NEW_RINGING_CONNECTION, null);
  3. ...

mCM就是CallManager对象,CallNotifier在初步化时将自己与PHONE_NEW_RINGING_CONNECTION事件的关系注册到了CallManager的mNewRingingConnectionRegistrants对象中。

  1. /**
  2. * Notifies when a new ringing or waiting connection has appeared.<p>
  3. *
  4. *  Messages received from this:
  5. *  Message.obj will be an AsyncResult
  6. *  AsyncResult.userObj = obj
  7. *  AsyncResult.result = a Connection. <p>
  8. *  Please check Connection.isRinging() to make sure the Connection
  9. *  has not dropped since this message was posted.
  10. *  If Connection.isRinging() is true, then
  11. *   Connection.getCall() == Phone.getRingingCall()
  12. */
  13. public void registerForNewRingingConnection(Handler h, int what, Object obj){
  14. mNewRingingConnectionRegistrants.addUnique(h, what, obj);
  15. }

CallNotifier也是继承了Handler的,在上面的 internalNotifyRegistrant()
中,最终也是将消息发送给 CallNotifier 对象去处理的,CallNotifier 的 handleMessage()
函数就会被间接地调用了。
下面进入CallNotifier 的 handleMessage(),看看它的实现:

  1. @Override
  2. public void handleMessage(Message msg) {
  3. switch (msg.what) {
  4. case PHONE_NEW_RINGING_CONNECTION:
  5. log("RINGING... (new)");
  6. mSilentRingerRequested = false;
  7. ((AsyncResult) msg.obj);
  8. break;
  9. ...

看看这里输出的日志,在上面我列出的日志中是有输出的:  "RINGING... (new)"。再跟到 onNewRingingConnection() 看看:

  1. /**
  2. * Handles a "new ringing connection" event from the telephony layer.
  3. */
  4. private void onNewRingingConnection(AsyncResult r) {
  5. Connection c = (Connection) r.result;
  6. log("onNewRingingConnection(): state = " + mCM.getState() + ", conn = { " + c + " }");
  7. Call ringing = c.getCall();
  8. Phone phone = ringing.getPhone();
  9. // Check for a few cases where we totally ignore incoming calls.
  10. if (ignoreAllIncomingCalls(phone)) {
  11. // Immediately reject the call, without even indicating to the user
  12. // that an incoming call occurred.  (This will generally send the
  13. // caller straight to voicemail, just as if we *had* shown the
  14. // incoming-call UI and the user had declined the call.)
  15. PhoneUtils.hangupRingingCall(ringing);
  16. return;
  17. }
  18. ...
  19. // - don't ring for call waiting connections
  20. // - do this before showing the incoming call panel
  21. if (PhoneUtils.isRealIncomingCall(state)) {
  22. startIncomingCallQuery(c);
  23. }
  24. }

主要的逻辑就是判断基于一定的规则判断是否自动拦截此呼叫,如果不拦截,则会向下走,调用到 startIncomingCallQuery() 函数。

这个函数,干的事情也比较简单,就是基于号码来查询联系人详情啥的,如果获取到联系人信息,则根据这个结果判断是使用默认铃声,还是用户给其设置的特定铃声。

  1. /**
  2. * Helper method to manage the start of incoming call queries
  3. */
  4. private void startIncomingCallQuery(Connection c) {
  5. ...
  6. if (shouldStartQuery) {
  7. // Reset the ringtone to the default first.
  8. mRinger.setCustomRingtoneUri(Settings.System.DEFAULT_RINGTONE_URI);
  9. // query the callerinfo to try to get the ringer.
  10. PhoneUtils.CallerInfoToken cit = PhoneUtils.startGetCallerInfo(
  11. mApplication, c, this, this);
  12. // if this has already been queried then just ring, otherwise
  13. // we wait for the alloted time before ringing.
  14. if (cit.isFinal) {
  15. if (VDBG) log("- CallerInfo already up to date, using available data");
  16. onQueryComplete(0, this, cit.currentInfo);
  17. } else {
  18. if (VDBG) log("- Starting query, posting timeout message.");
  19. // Phone number (via getAddress()) is stored in the message to remember which
  20. // number is actually used for the look up.
  21. sendMessageDelayed(
  22. Message.obtain(this, RINGER_CUSTOM_RINGTONE_QUERY_TIMEOUT, c.getAddress()),
  23. RINGTONE_QUERY_WAIT_TIME);
  24. }
  25. // The call to showIncomingCall() will happen after the
  26. // queries are complete (or time out).
  27. } ...
  28. }

这里面有一点细节要说明一下,PhoneUtils.startGetCallerInfo() 这个调用之后,如果成功,则会再回调到 CallNotifier.onQueryComplete();

为了防止PhoneUtils.startGetCallerInfo()出现异常长时间不回调,在else这个分支中,还插入了一个RINGER_CUSTOM_RINGTONE_QUERY_TIMEOUT
这样一个消息,在500ms后,如果CallNotifier.onQueryComplete()没有被回调,则此消息会被触发。不管有没有超
时,onCustomRingQueryComplete()
都会被调用到。

具体是使用到了Handler的机制,Handler的原理说明可以参见我的这个blog:《深入理解Android消息处理系统——Looper、Handler、Thread》。

再看看 onCustomRingQueryComplete() 的实现:
  1. /**
  2. * Performs the final steps of the onNewRingingConnection sequence:
  3. * starts the ringer, and brings up the "incoming call" UI.
  4. *
  5. * Normally, this is called when the CallerInfo query completes (see
  6. * onQueryComplete()).  In this case, onQueryComplete() has already
  7. * configured the Ringer object to use the custom ringtone (if there
  8. * is one) for this caller.  So we just tell the Ringer to start, and
  9. * proceed to the InCallScreen.
  10. *
  11. * But this method can *also* be called if the
  12. * RINGTONE_QUERY_WAIT_TIME timeout expires, which means that the
  13. * CallerInfo query is taking too long.  In that case, we log a
  14. * warning but otherwise we behave the same as in the normal case.
  15. * (We still tell the Ringer to start, but it's going to use the
  16. * default ringtone.)
  17. */
  18. private void onCustomRingQueryComplete() {
  19. ...
  20. // Ring, either with the queried ringtone or default one.
  21. if (VDBG) log("RINGING... (onCustomRingQueryComplete)");
  22. mRinger.ring();
  23. // ...and display the incoming call to the user:
  24. if (DBG) log("- showing incoming call (custom ring query complete)...");
  25. showIncomingCall();
  26. }

从注释上就可以看出,这个是 onNewRingingConnection 的事件处理序列的最后一步,主要干两件事:

    1、触发铃声的播放;
    2、显示来电界面;

第一个是我更想关心的,再看看这个干了什么,说不定就是我们要修改的地方:

进入到Ringer.ring()的实现看看,如果铃声音量值不是0,就发PLAY_RING_ONCE消息去播放铃声:

  1. void ring() {
  2. if (DBG) log("ring()...");
  3. synchronized (this) {
  4. ...
  5. AudioManager audioManager =
  6. (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
  7. if (audioManager.getStreamVolume(AudioManager.STREAM_RING) == 0) {
  8. if (DBG) log("skipping ring because volume is zero");
  9. return;
  10. }
  11. makeLooper();
  12. if (mFirstRingEventTime < 0) {
  13. mFirstRingEventTime = SystemClock.elapsedRealtime();
  14. mRingHandler.sendEmptyMessage(PLAY_RING_ONCE);
  15. } ...
  16. }
  17. }

makeLooper()中有对 mRingHandler有初始化:

  1. private void makeLooper() {
  2. if (mRingThread == null) {
  3. mRingThread = new Worker("ringer");
  4. mRingHandler = new Handler(mRingThread.getLooper()) {
  5. @Override
  6. public void handleMessage(Message msg) {
  7. Ringtone r = null;
  8. switch (msg.what) {
  9. case PLAY_RING_ONCE:
  10. if (DBG) log("mRingHandler: PLAY_RING_ONCE...");
  11. if (mRingtone == null && !hasMessages(STOP_RING)) {
  12. // create the ringtone with the uri
  13. if (DBG) log("creating ringtone: " + mCustomRingtoneUri);
  14. r = RingtoneManager.getRingtone(mContext, mCustomRingtoneUri);
  15. synchronized (Ringer.this) {
  16. if (!hasMessages(STOP_RING)) {
  17. mRingtone = r;
  18. }
  19. }
  20. }
  21. r = mRingtone;
  22. if (r != null && !hasMessages(STOP_RING) && !r.isPlaying()) {
  23. PhoneUtils.setAudioMode();
  24. r.play();
  25. synchronized (Ringer.this) {
  26. if (mFirstRingStartTime < 0) {
  27. mFirstRingStartTime = SystemClock.elapsedRealtime();
  28. }
  29. }
  30. }
  31. break;
  32. ...
  33. }
  34. }
  35. };
  36. }
  37. }

会初始化出一个Ringtone对象,通过这个对象来播放铃声,这个Ringtone播放铃声其实还有点绕的,最终是通过Binder机制使用"audio"服务中的Ringtone对象中的mLocalPlayer属性,即MediaPlayer的实例来播放铃声的。怎么实现的,这里就不说了,代码太多了,而且还涉及到Binder机制,如果有疑问,可以单独找我。

总算找到开始播放铃声的代码了,在这附近加一些逻辑来控制铃声音量、和音乐音量的代码就可以了。

通过 r.play() 附近加上如下逻辑:

  1. mHandler.sendEmptyMessageDelayed(INCREASE_RING_VOLUME, 200);
  2. mHandler.sendEmptyMessageDelayed(DECREASE_MUSIC_VOLUME, 200);

makeLooper()中再加上如下代码:

  1. if (mHandler == null) {
  2. mHandler = new Handler() {
  3. @Override
  4. public void handleMessage(Message msg) {
  5. switch (msg.what) {
  6. case INCREASE_RING_VOLUME:
  7. int ringerVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING);
  8. if (mRingerVolumeSetting > 0 && ringerVolume < mRingerVolumeSetting) {
  9. ringerVolume++;
  10. mAudioManager.setStreamVolume(AudioManager.STREAM_RING, ringerVolume, 0);
  11. sendEmptyMessageDelayed(INCREASE_RING_VOLUME, 200);
  12. }
  13. break;
  14. case DECREASE_MUSIC_VOLUME:
  15. int musicVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
  16. if (musicVolume > 0) {
  17. musicVolume--;
  18. mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, musicVolume, 0);
  19. sendEmptyMessageDelayed(DECREASE_MUSIC_VOLUME, 200);
  20. }
  21. break;
  22. }
  23. }
  24. };
  25. }

当然,你还要考虑一些细节,比如Music是否正在播放,铃声或音乐的音量大小是否是0,或最大等。

AudioManager中的一些说明,可以参见《Android如何判断当前手机是否正在播放音乐,并获取到正在播放的音乐的信息》。

当我修改完代码,并怀着十分期待的心情将Phone.apk替换原有的apk后,拨打被叫有来电时,正在播放的音乐一下就停止了,铃音是渐强的,哪里出了问题?

分析清楚这个问题花的时间比之前还要长,有空再写下面的内容吧。

上一篇:UWP中实现大爆炸效果(二)


下一篇:Hook机制里登场的角色