目前Android的实现是:有来电时,音乐声音直接停止,铃声直接直接使用设置的铃声音量进行铃声播放。
Android 4.3实现类似iOS在音乐播放过程中如果有来电则音乐声音渐小铃声渐大的效果。
如果要实现这个效果,首先要搞清楚两大问题;
1、来电时的代码主要实现流程。
2、主流音乐播放器在播放过程中,如果有来电,到底在收到了什么事件后将音乐暂停了?
我不是第一研究来电代码的人,网上已经有高手对这个流程剖析过,不是不完全符合我的要求,我参考过的比较有价值的是如下两个文档:
因为我做的事情主要是有来电时,修改铃音的效果,所以不用从头跟进,从响铃通知到达Phone.apk中分析起即可,更细可以参考下上面的两个链接。
分析之前,还是有必要对Phone整体的初始化流程有个基本认识,不然后面跟到沟里去。
Phone.apk 的AndroidManifest.xml中的application的说明:
<application android:name="PhoneApp" android:persistent="true" android:label="@string/phoneAppLabel" android:icon="@mipmap/ic_launcher_phone">那再看看PhoneApp的实现:
/** * Top-level Application class for the Phone app. */ public class PhoneApp extends Application { PhoneGlobals mPhoneGlobals; public PhoneApp() { } @Override public void onCreate() { if (UserHandle.myUserId() == 0) { // We are running as the primary user, so should bring up the // global phone state. mPhoneGlobals = new PhoneGlobals(this); mPhoneGlobals.onCreate(); } } @Override public void onConfigurationChanged(Configuration newConfig) { if (mPhoneGlobals != null) { mPhoneGlobals.onConfigurationChanged(newConfig); } super.onConfigurationChanged(newConfig); }从源码来看,这个类非常的简单,主要就是对 mPhoneGlobals 属性进行了创建和初始化。再来分析 PhoneGlobals 是如何初始化的:
public void PhoneGlobals.onCreate() { ... if (phone == null) { // Initialize the telephony framework PhoneFactory.makeDefaultPhones(this); // Get the default phone phone = PhoneFactory.getDefaultPhone(); // Start TelephonyDebugService After the default phone is created. Intent intent = new Intent(this, TelephonyDebugService.class); startService(intent); mCM = CallManager.getInstance(); mCM.registerPhone(phone); // Create the NotificationMgr singleton, which is used to display // status bar icons and control other status bar behavior. notificationMgr = NotificationMgr.init(this); phoneMgr = PhoneInterfaceManager.init(this, phone); mHandler.sendEmptyMessage(EVENT_START_SIP_SERVICE); int phoneType = phone.getPhoneType(); if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) { // Create an instance of CdmaPhoneCallState and initialize it to IDLE cdmaPhoneCallState = new CdmaPhoneCallState(); cdmaPhoneCallState.CdmaPhoneCallStateInit(); } ... ringer = Ringer.init(this); ... notifier = CallNotifier.init(this, phone, ringer, new CallLogAsync()); ... } ... }PhonePhoneGlobals.onCreate() 中干了很多事情,其中我列出的内容,都是我个人觉得比较重要的部分,建议重点看一下,后面会用得到。
PhoneFactory.makeDefaultPhones(this) 和 phone = PhoneFactory.getDefaultPhone() 这两个函数调用,建议也跟进去重点看一下,这里面做了比较重要的事情,
底层来电事件就是通过类似注册表注册机制做好一系列地注册之后,后面有不同事件过来后,将相应的消息分发特定的对象去处理。
我修改了Phone的源码,将日志全部放开,然后将重新编译得到的 Phone.apk 更新到手机中,真实地拨打了一个电话,
日志量比较大,只列出开头的一小部分,具体日志如下:
10-10 21:20:18.862: D/CallNotifier(814): RING before NEW_RING, skipping 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 10-10 21:20:18.862: D/InCallScreen(814): onIncomingRing()... 10-10 21:20:20.834: D/CallNotifier(814): PHONE_ENHANCED_VP_OFF... 10-10 21:20:20.844: D/CallNotifier(814): RINGING... (new) 10-10 21:20:20.844: D/CallNotifier(814): onNewRingingConnection(): state = RINGING, conn = { incoming: true state: INCOMING post dial state: NOT_STARTED } 10-10 21:20:20.844: D/CallNotifier(814): Incoming number is: 02556781234 10-10 21:20:20.844: V/BlacklistProvider(814): Query uri=content://blacklist/bynumber/02556781234, match=2 10-10 21:20:20.864: D/CallNotifier(814): stopSignalInfoTone: Stopping SignalInfo tone player 10-10 21:20:20.864: D/CallNotifier(814): - connection is ringing! state = INCOMING 10-10 21:20:20.864: D/CallNotifier(814): Holding wake lock on new incoming connection. 10-10 21:20:20.864: D/PhoneApp(814): requestWakeState(PARTIAL)... 10-10 21:20:20.864: D/PhoneUtils(814): PhoneUtils.startGetCallerInfo: new query for phone number... ...从上面的日志可以看出,当有来电时,其实是 PHONE_NEW_RINGING_CONNECTION 这个事件交给了Phoe应用来处理了。
底层的流程大致如下,更详细的参见《Android来电时停止音乐播放的流程》:
1、RIL怎么将消息传递给 GsmCallTracker 的,这个没有研究,跳过。
2、GsmCallTracker如何将消息向上层传播的?来看看代码:GsmCallTracker这个类本身是继承自Handler这个类的,看看handleMessage (Message msg)实现:
handleMessage (Message msg) { AsyncResult ar; switch (msg.what) { case EVENT_POLL_CALLS_RESULT: ar = (AsyncResult)msg.obj; if (msg == lastRelevantPoll) { if (DBG_POLL) log( "handle EVENT_POLL_CALL_RESULT: set needsPoll=F"); needsPoll = false; lastRelevantPoll = null; handlePollCalls((AsyncResult)msg.obj); } break; ... } }
再看看handlePollCalls()的实现:
protected synchronized void handlePollCalls(AsyncResult ar) { ... if (newRinging != null) { phone.notifyNewRingingConnection(newRinging); } ... updatePhoneState(); ... }
重点关注有来电相关的代码, GSMPhone.notifyNewRingingConnection(newRinging); --> PhoneBase.notifyNewRingingConnectionP()
--> PhoneBase.mNewRingingConnectionRegistrants.notifyRegistrants(ar) --> ...
一路跟下去,到 Registrant.internalNotifyRegistrant(),这个是这个 h 到底对应的是哪个Handler呢?
/*package*/ void internalNotifyRegistrant (Object result, Throwable exception) { Handler h = getHandler(); if (h == null) { clear(); } else { Message msg = Message.obtain(); msg.what = what; msg.obj = new AsyncResult(userObj, result, exception); h.sendMessage(msg); } }
我们在前面看的初始化相关的代码的作用就体现出来了,PhoneBase.mNewRingingConnectionRegistrants这个列表中的内容是何时放进去的呢?
/** Private constructor; @see init() */ private CallNotifier(PhoneGlobals app, Phone phone, Ringer ringer, CallLogAsync callLog) { mApplication = app; mCM = app.mCM; mCallLog = callLog; mAudioManager = (AudioManager) mApplication.getSystemService(Context.AUDIO_SERVICE); registerForNotifications(); ...
private void registerForNotifications() { mCM.registerForNewRingingConnection(this, PHONE_NEW_RINGING_CONNECTION, null); ...
mCM就是CallManager对象,CallNotifier在初步化时将自己与PHONE_NEW_RINGING_CONNECTION事件的关系注册到了CallManager的mNewRingingConnectionRegistrants对象中。
/** * Notifies when a new ringing or waiting connection has appeared.<p> * * Messages received from this: * Message.obj will be an AsyncResult * AsyncResult.userObj = obj * AsyncResult.result = a Connection. <p> * Please check Connection.isRinging() to make sure the Connection * has not dropped since this message was posted. * If Connection.isRinging() is true, then * Connection.getCall() == Phone.getRingingCall() */ public void registerForNewRingingConnection(Handler h, int what, Object obj){ mNewRingingConnectionRegistrants.addUnique(h, what, obj); }
CallNotifier也是继承了Handler的,在上面的 internalNotifyRegistrant() 中,最终也是将消息发送给 CallNotifier 对象去处理的,CallNotifier 的 handleMessage() 函数就会被间接地调用了。
下面进入CallNotifier 的 handleMessage(),看看它的实现:
@Override public void handleMessage(Message msg) { switch (msg.what) { case PHONE_NEW_RINGING_CONNECTION: log("RINGING... (new)"); mSilentRingerRequested = false; ((AsyncResult) msg.obj); break; ...看看这里输出的日志,在上面我列出的日志中是有输出的: "RINGING... (new)"。再跟到 onNewRingingConnection() 看看:
/** * Handles a "new ringing connection" event from the telephony layer. */ private void onNewRingingConnection(AsyncResult r) { Connection c = (Connection) r.result; log("onNewRingingConnection(): state = " + mCM.getState() + ", conn = { " + c + " }"); Call ringing = c.getCall(); Phone phone = ringing.getPhone(); // Check for a few cases where we totally ignore incoming calls. if (ignoreAllIncomingCalls(phone)) { // Immediately reject the call, without even indicating to the user // that an incoming call occurred. (This will generally send the // caller straight to voicemail, just as if we *had* shown the // incoming-call UI and the user had declined the call.) PhoneUtils.hangupRingingCall(ringing); return; } ... // - don't ring for call waiting connections // - do this before showing the incoming call panel if (PhoneUtils.isRealIncomingCall(state)) { startIncomingCallQuery(c); } ... }主要的逻辑就是判断基于一定的规则判断是否自动拦截此呼叫,如果不拦截,则会向下走,调用到 startIncomingCallQuery() 函数。
这个函数,干的事情也比较简单,就是基于号码来查询联系人详情啥的,如果获取到联系人信息,则根据这个结果判断是使用默认铃声,还是用户给其设置的特定铃声。
/** * Helper method to manage the start of incoming call queries */ private void startIncomingCallQuery(Connection c) { ... if (shouldStartQuery) { // Reset the ringtone to the default first. mRinger.setCustomRingtoneUri(Settings.System.DEFAULT_RINGTONE_URI); // query the callerinfo to try to get the ringer. PhoneUtils.CallerInfoToken cit = PhoneUtils.startGetCallerInfo( mApplication, c, this, this); // if this has already been queried then just ring, otherwise // we wait for the alloted time before ringing. if (cit.isFinal) { if (VDBG) log("- CallerInfo already up to date, using available data"); onQueryComplete(0, this, cit.currentInfo); } else { if (VDBG) log("- Starting query, posting timeout message."); // Phone number (via getAddress()) is stored in the message to remember which // number is actually used for the look up. sendMessageDelayed( Message.obtain(this, RINGER_CUSTOM_RINGTONE_QUERY_TIMEOUT, c.getAddress()), RINGTONE_QUERY_WAIT_TIME); } // The call to showIncomingCall() will happen after the // queries are complete (or time out). } ... }这里面有一点细节要说明一下, PhoneUtils.startGetCallerInfo() 这个调用之后,如果成功,则会再回调到 CallNotifier.onQueryComplete();
为了防止PhoneUtils.startGetCallerInfo()出现异常长时间不回调,在else这个分支中,还插入了一个RINGER_CUSTOM_RINGTONE_QUERY_TIMEOUT这样一个消息,在500ms后,如果CallNotifier.onQueryComplete()没有被回调,则此消息会被触发。不管有没有超时,onCustomRingQueryComplete() 都会被调用到。
具体是使用到了Handler的机制,Handler的原理说明可以参见我的这个blog:《深入理解Android消息处理系统——Looper、Handler、Thread》。
/** * Performs the final steps of the onNewRingingConnection sequence: * starts the ringer, and brings up the "incoming call" UI. * * Normally, this is called when the CallerInfo query completes (see * onQueryComplete()). In this case, onQueryComplete() has already * configured the Ringer object to use the custom ringtone (if there * is one) for this caller. So we just tell the Ringer to start, and * proceed to the InCallScreen. * * But this method can *also* be called if the * RINGTONE_QUERY_WAIT_TIME timeout expires, which means that the * CallerInfo query is taking too long. In that case, we log a * warning but otherwise we behave the same as in the normal case. * (We still tell the Ringer to start, but it's going to use the * default ringtone.) */ private void onCustomRingQueryComplete() { ... // Ring, either with the queried ringtone or default one. if (VDBG) log("RINGING... (onCustomRingQueryComplete)"); mRinger.ring(); // ...and display the incoming call to the user: if (DBG) log("- showing incoming call (custom ring query complete)..."); showIncomingCall(); }从注释上就可以看出,这个是 onNewRingingConnection 的事件处理序列的最后一步,主要干两件事:
void ring() { if (DBG) log("ring()..."); synchronized (this) { ... AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); if (audioManager.getStreamVolume(AudioManager.STREAM_RING) == 0) { if (DBG) log("skipping ring because volume is zero"); return; } makeLooper(); if (mFirstRingEventTime < 0) { mFirstRingEventTime = SystemClock.elapsedRealtime(); mRingHandler.sendEmptyMessage(PLAY_RING_ONCE); } ... } }makeLooper()中有对 mRingHandler有初始化:
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; ... } } }; } }会初始化出一个Ringtone对象,通过这个对象来播放铃声,这个Ringtone播放铃声其实还有点绕的, 最终是通过Binder机制使用 "audio"服务中的Ringtone对象中的 mLocalPlayer属性,即 MediaPlayer的实例来播放铃声的。怎么实现的,这里就不说了,代码太多了,而且还涉及到Binder机制,如果有疑问,可以单独找我。
总算找到开始播放铃声的代码了,在这附近加一些逻辑来控制铃声音量、和音乐音量的代码就可以了。
通过 r.play() 附近加上如下逻辑:
mHandler.sendEmptyMessageDelayed(INCREASE_RING_VOLUME, 200); mHandler.sendEmptyMessageDelayed(DECREASE_MUSIC_VOLUME, 200);
makeLooper()中再加上如下代码:
if (mHandler == null) { mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case INCREASE_RING_VOLUME: int ringerVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING); if (mRingerVolumeSetting > 0 && ringerVolume < mRingerVolumeSetting) { ringerVolume++; mAudioManager.setStreamVolume(AudioManager.STREAM_RING, ringerVolume, 0); sendEmptyMessageDelayed(INCREASE_RING_VOLUME, 200); } break; case DECREASE_MUSIC_VOLUME: int musicVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); if (musicVolume > 0) { musicVolume--; mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, musicVolume, 0); sendEmptyMessageDelayed(DECREASE_MUSIC_VOLUME, 200); } break; } } }; }
当然,你还要考虑一些细节,比如Music是否正在播放,铃声或音乐的音量大小是否是0,或最大等。
AudioManager中的一些说明,可以参见《Android如何判断当前手机是否正在播放音乐,并获取到正在播放的音乐的信息》。
当我修改完代码,并怀着十分期待的心情将Phone.apk替换原有的apk后,拨打被叫有来电时,正在播放的音乐一下就停止了,铃音是渐强的,哪里出了问题?
分析清楚这个问题花的时间比之前还要长,有空再写下面的内容吧。