zoukankan      html  css  js  c++  java
  • 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.   
    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 是如何初始化的:

    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 更新到手机中,真实地拨打了一个电话,

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

    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.   
    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()的实现:

    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呢?

    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这个列表中的内容是何时放进去的呢?

    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.     ...  
    1. private void registerForNotifications() {  
    2.     mCM.registerForNewRingingConnection(this, PHONE_NEW_RINGING_CONNECTION, null);  
    3.   
    4.     ...  

    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.   
    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() 函数。

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

    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() 的实现:
    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消息去播放铃声

    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有初始化:

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

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

  • 相关阅读:
    RT throttling分析【转】
    linux异步IO的两种方式【转】
    linux select 与 阻塞( blocking ) 及非阻塞 (non blocking)实现io多路复用的示例【转】
    10. linux输入子系统/input 设备【转】
    Unix/Linux进程间通信(一):概述
    Linux进程间通信(九):数据报套接字 socket()、bind()、sendto()、recvfrom()、close()
    Linux进程间通信(八):流套接字 socket()、bind()、listen()、accept()、connect()、read()、write()、close()
    PHP函数 rtrim() 的一个怪异现象
    Linux进程间通信(七):消息队列 msgget()、msgsend()、msgrcv()、msgctl()
    Linux进程间通信(六):共享内存 shmget()、shmat()、shmdt()、shmctl()
  • 原文地址:https://www.cnblogs.com/zhaoxinshanwei/p/3870780.html
Copyright © 2011-2022 走看看