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

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

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

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


 

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

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

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


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

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

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

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

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


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

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

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

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

那再看看PhoneApp的实现:

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

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

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

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

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

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

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

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

[plain] view plaincopy
  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)实现:

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

再看看handlePollCalls()的实现:

[java] view plaincopy
  1. protected synchronized void  
  2. handlePollCalls(AsyncResult ar) {  
  3.     ...  
  4.   
  5.     if (newRinging != null) {  
  6.         phone.notifyNewRingingConnection(newRinging);  
  7.     }  
  8.   
  9.     ...  
  10.   
  11.     updatePhoneState();  
  12.   
  13.     ...  
  14. }  

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

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

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

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

[java] view plaincopy
  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.   
  7.     mAudioManager = (AudioManager) mApplication.getSystemService(Context.AUDIO_SERVICE);  
  8.   
  9.     registerForNotifications();  
  10.     ...  
[java] view plaincopy
  1. private void registerForNotifications() {  
  2.     mCM.registerForNewRingingConnection(this, PHONE_NEW_RINGING_CONNECTION, null);  
  3.   
  4.     ...  

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

[java] view plaincopy
  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(),看看它的实现:

[java] view plaincopy
  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() 看看:

 

[java] view plaincopy
  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.   
  10.     // Check for a few cases where we totally ignore incoming calls.  
  11.     if (ignoreAllIncomingCalls(phone)) {  
  12.         // Immediately reject the call, without even indicating to the user  
  13.         // that an incoming call occurred.  (This will generally send the  
  14.         // caller straight to voicemail, just as if we *had* shown the  
  15.         // incoming-call UI and the user had declined the call.)  
  16.   
  17.         PhoneUtils.hangupRingingCall(ringing);  
  18.         return;  
  19.     }  
  20.   
  21.     ...  
  22.   
  23.     // - don‘t ring for call waiting connections  
  24.     // - do this before showing the incoming call panel  
  25.     if (PhoneUtils.isRealIncomingCall(state)) {  
  26.         startIncomingCallQuery(c);  
  27.     }  
  28.   
  29.   
  30. }  

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

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

 

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

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

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

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

再看看 onCustomRingQueryComplete() 的实现:
[java] view plaincopy
  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.   
  21.     // Ring, either with the queried ringtone or default one.  
  22.     if (VDBG) log("RINGING... (onCustomRingQueryComplete)");  
  23.     mRinger.ring();  
  24.   
  25.     // ...and display the incoming call to the user:  
  26.     if (DBG) log("- showing incoming call (custom ring query complete)...");  
  27.     showIncomingCall();  
  28. }  
从注释上就可以看出,这个是 onNewRingingConnection 的事件处理序列的最后一步,主要干两件事:
    1、触发铃声的播放;
    2、显示来电界面;

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

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

 

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

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

 

[java] view plaincopy
  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() 附近加上如下逻辑:

[java] view plaincopy
  1. mHandler.sendEmptyMessageDelayed(INCREASE_RING_VOLUME, 200);  
  2. mHandler.sendEmptyMessageDelayed(DECREASE_MUSIC_VOLUME, 200);  


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

[java] view plaincopy
  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后,拨打被叫有来电时,正在播放的音乐一下就停止了,铃音是渐强的,哪里出了问题?

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

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