Android系统篇-来去电UI分析
InCallScreen是什么
图1中所示就是InCallScreen不同情况下所展示的界面,分别是拨号、接通、来电三种情况下InCallScreen的现实情况。
InCallScreen结构分析
经过对比后可以发现,InCallScreen的拨号以及接通时,界面表现基本一致,而来电界面主要由于多了一个滑动接听的控件,从而导致界面不太一样。这里我们队InCallScreen的结构分析,采用接通之后的界面。如图2:
InCallScreen布局分析
在InCallScreen.java中,我们可以在onCreate方法中找到InCallScreen加载的布局文件,即incall_screen.xml。在incall_screen.java文件中,我们可以看到有以下几个控件:
- call_card:显示当前通话的信息,比如来电号码,通话时间,移动运营商等等;
- incall_touch_ui:包括挂断按钮,显示DTMF拨号盘按钮,扬声器,静音,暂停,加入通话等几个按钮,就是通话界面下方的控制按钮;
- otaCallCardStub:CDMA模式下跟OTA相关的控件;
- manageConferencePanelStub:多方通话管理界面;
- vtInCallScreenStub:视屏通话控件;
- dtmf_twelve_key_dialer_stub:这个控件为DTMF控件,也就是我们点击按钮之后,会弹出一个0~9以及*和#的拨号盘。
因为我们这里主要分析一般模式下的通话界面,因此暂不涉及到视屏通话。
总的来讲,在图2显示的界面中,我们直观能够看到的控件主要是:call_card以及incall_touch_ui这两块。当我们点击DTMF弹出按钮之后,会显示DTMF控件。当然在我们接入多方通话之后,就会看到多方通话的界面了。call_card通话信息展示
<?xml version="1.0" encoding="utf-8"?> <com.android.phone.CallCard xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/call_info_container" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <include android:id="@+id/primary_call_info" layout="@layout/primary_call_info" /> <RelativeLayout android:id="@+id/largeAreaForSharing" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone"/> </com.android.phone.CallCard>
整个布局情况如下图:
incall_touch_ui通话控制界面
InCallScreen初始化流程
通过查看Phone.apk的AndroidManifest.xml文件可以看到:
<activity android:name="InCallScreen" android:theme="@style/Theme.InCallScreen" <!-- InCallScreen的Theme --> android:label="@string/phoneIconLabel" android:excludeFromRecents="true" <!-- 该Activity不会显示在最近使用列表中 --> android:launchMode="singleInstance" <!-- 该Activity为单例模式 --> android:screenOrientation="nosensor" <!-- 该Activity不会横竖屏切换,默认竖屏 --> android:configChanges="keyboardHidden" <!-- 该Activity显示时隐藏keyboard --> android:exported="false"> <!-- 该Activity不能被其它调用 --> </activity>
查看@style/Theme.InCallScreen可以看到:
<style name="Theme.InCallScreen" parent="@android:style/Theme.Holo.NoActionBar"> <item name="android:windowBackground">@android:color/black</item> <item name="*android:windowAnimationStyle">@style/InCallAnimationStyle</item> </style>
可以看到InCallScreen的Theme中没有ActionBar,窗口背景为黑色,有过场动画。
因为InCallScreen为单例模式,第一次启动时调用onCreate而后面则会调用其onNewIntent方法。我们知道在onCreate方法中,一般都是对一些对象进行创建并初始化,以及设置布局文件等等。在InCallScreen的onCreate方法中,完成了PhoneApp对象的获取,以及Window参数的设置等等,在这些过程中我们需要关注以下三个方法:- initInCallScreen:初始化CallCard以及InCallTouchUi等截面;
- registerForPhoneStates:注册关于Phone状态改变的监听事件,这也就是为什么Phone状态改变之后InCallScreen能够收到变化消息的原因,这一点我们在来电流程中也有提及;
-
internalResolveIntent:该方法用于处理InCallScreen收到的Intent信息;
initInCallScreen
private void initInCallScreen() { ... ...省略 // Initialize CallTime 通话时间初始化 mCallTime = new CallTime(this); // Initialize the CallCard. 通话信息初始化 mCallCard = (CallCard) findViewById(R.id.callCard); ... ...省略 //第二路通话界面初始化 mSecCallInfo = (ViewStub) findViewById(R.id.secondary_call_info); mCallCard.setInCallScreenInstance(this); //通话录音按钮初始化 mVoiceRecorderIcon = (ImageView) findViewById(R.id.voiceRecorderIcon); mVoiceRecorderIcon.setBackgroundResource(R.drawable.voice_record); mVoiceRecorderIcon.setVisibility(View.INVISIBLE); // Initialize the onscreen UI elements. 通话控制布局初始化 initInCallTouchUi(); ... ...省略 // The DTMF Dialpad. DTMF拨号盘初始化 ViewStub stub = (ViewStub) findViewById(R.id.dtmf_twelve_key_dialer_stub); mDialer = new DTMFTwelveKeyDialer(this, stub); mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); // Initialize VTInCallScreen 视屏电话初始化 mVTInCallScreen = new VTInCallScreenProxy(this, mDialer); }
可以看到这里全都是对InCallScreen上布局的一些初始化过程,我们继续看到其中对于InCallTouchUi初始化的代码:
private void initInCallTouchUi() { ... ...省略 mInCallTouchUi = (InCallTouchUi) findViewById(R.id.inCallTouchUi); mInCallTouchUi.setInCallScreenInstance(this); // 挂断并通话短信回复 mRespondViaSmsManager = new RespondViaSmsManager(); mRespondViaSmsManager.setInCallScreenInstance(this); }
通过代码可以知道InCallScreen上的布局显示几乎都是在initInCallScreen方法中做的,如果我们修改了InCallScreen的布局那么我们应该在这里对修改后的布局进行初始化。
registerForPhoneStates
private void registerForPhoneStates() { if (!mRegisteredForPhoneStates) { if (FeatureOption.MTK_GEMINI_SUPPORT) { ... ...省略 mCMGemini.registerForIncomingRingGemini(mHandler, PHONE_INCOMING_RING, null, PhoneConstants.GEMINI_SIM_1); mCMGemini.registerForIncomingRingGemini(mHandler, PHONE_INCOMING_RING2, null, PhoneConstants.GEMINI_SIM_2); ... ...省略 } else { ... ...省略 mCM.registerForIncomingRing(mHandler, PHONE_INCOMING_RING, null); ... ...省略 }
这里所用的mHandler就是InCallScreen的mHandler,这里通过register****方法注册监听实际上为观察者模式的运用。
internalResolveIntent
该方法是InCallScreen用于处理Intent的方法,代码如下:
private void internalResolveIntent(Intent intent) { ... ...省略 if (action.equals(intent.ACTION_MAIN)) { //是否显示Dialpad if (intent.hasExtra(SHOW_DIALPAD_EXTRA)) { boolean showDialpad = intent.getBooleanExtra(SHOW_DIALPAD_EXTRA, false); if (VDBG) log("- internalResolveIntent: SHOW_DIALPAD_EXTRA: " + showDialpad); mApp.inCallUiState.showDialpad = showDialpad; final boolean hasActiveCall = mCM.hasActiveFgCall(); final boolean hasHoldingCall = mCM.hasActiveBgCall(); if (showDialpad && !hasActiveCall && hasHoldingCall) { PhoneUtils.switchHoldingAndActive(mCM.getFirstActiveBgCall()); } } ... ...省略 //强制开启扬声器 if (FeatureOption.MTK_TB_APP_CALL_FORCE_SPEAKER_ON) { if (intent.hasExtra(EXTRA_FORCE_SPEAKER_ON)) { boolean forceSpeakerOn = intent.getBooleanExtra(EXTRA_FORCE_SPEAKER_ON, false); if (forceSpeakerOn) { Log.e("MTK_TB_APP_CALL_FORCE_SPEAKER_ON", "forceSpeakerOn is true"); if (!PhoneGlobals.getInstance().isHeadsetPlugged() && !(mApp.isBluetoothHeadsetAudioOn())) { //Only force the speaker ON while not video call and speaker is not ON if (!intent.getBooleanExtra(Constants.EXTRA_IS_VIDEO_CALL, false) && !PhoneUtils.isSpeakerOn(mApp)) { Log.e("MTK_TB_APP_CALL_FORCE_SPEAKER_ON", "PhoneUtils.turnOnSpeaker"); PhoneUtils.turnOnSpeaker(mApp, true, true, true); } } } } } //视屏通话处理 if (FeatureOption.MTK_VT3G324M_SUPPORT) { if (getInVoiceAnswerVideoCall()) { setInVoiceAnswerVideoCall(false); } if (mCM.getState() == PhoneConstants.State.RINGING) { if (DBG) { log("call manager state is ringing"); } // When VT call incoming, use voice call incoming call GUI mVTInCallScreen.setVTVisible(false); mVTInCallScreen.setVTScreenMode(Constants.VTScreenMode.VT_SCREEN_CLOSE); } else if (intent.getBooleanExtra(Constants.EXTRA_IS_VIDEO_CALL, false)) { if (DBG) { log("vt extra is true"); } // When dialing VT call, inflate VTInCallScreen mVTInCallScreen.initVTInCallScreen(); // When dialed a VT call, but dialed failed, needs not init state for dialing if (CallStatusCode.SUCCESS == mApp.inCallUiState.getPendingCallStatusCode()) { mVTInCallScreen.initDialingSuccessVTState(); } mVTInCallScreen.initDialingVTState(); mVTInCallScreen.initCommonVTState(); if (PhoneConstants.State.IDLE != PhoneGlobals.getInstance().mCM.getState() && !VTCallUtils.isVideoCall(mCM.getActiveFgCall())) { // When voice is connected and place a VT call, need close VT GUI mVTInCallScreen.setVTScreenMode(Constants.VTScreenMode.VT_SCREEN_CLOSE); } else { mVTInCallScreen.setVTScreenMode(Constants.VTScreenMode.VT_SCREEN_OPEN); } } else { // set VT open or close according the active foreground call if (mCM.getState() != PhoneConstants.State.IDLE && VTCallUtils.isVideoCall(mCM.getActiveFgCall())) { if (DBG) { log("receive ACTION_MAIN, but active foreground call is video call"); } mVTInCallScreen.initVTInCallScreen(); mVTInCallScreen.initCommonVTState(); mVTInCallScreen.setVTScreenMode(Constants.VTScreenMode.VT_SCREEN_OPEN); } else if (!intent.getBooleanExtra(Constants.EXTRA_IS_NOTIFICATION, false)) { mVTInCallScreen.setVTScreenMode(Constants.VTScreenMode.VT_SCREEN_CLOSE); } } mVTInCallScreen.updateVTScreen(mVTInCallScreen.getVTScreenMode()); } return; } //接听电话时触发 if (action.equals(Intent.ACTION_ANSWER)) { internalAnswerCall(); mApp.setRestoreMuteOnInCallResume(false); return; } //OTA相关处理 if (action.equals(OtaUtils.ACTION_DISPLAY_ACTIVATION_SCREEN)) { if (!TelephonyCapabilities.supportsOtasp(mPhone)) { throw new IllegalStateException( "Received ACTION_DISPLAY_ACTIVATION_SCREEN intent on non-OTASP-capable device: " + intent); } setInCallScreenMode(InCallScreenMode.OTA_NORMAL); if ((mApp.cdmaOtaProvisionData != null) && (!mApp.cdmaOtaProvisionData.isOtaCallIntentProcessed)) { mApp.cdmaOtaProvisionData.isOtaCallIntentProcessed = true; mApp.cdmaOtaScreenState.otaScreenState = CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION; } return; } // 异常和未定义intent处理 if (action.equals(OtaUtils.ACTION_PERFORM_CDMA_PROVISIONING)) { throw new IllegalStateException( "Unexpected ACTION_PERFORM_CDMA_PROVISIONING received by InCallScreen: " + intent); } else if (action.equals(Intent.ACTION_CALL) || action.equals(Intent.ACTION_CALL_EMERGENCY)) { throw new IllegalStateException("Unexpected CALL action received by InCallScreen: " + intent); } else if (action.equals(ACTION_UNDEFINED)) { Log.wtf(LOG_TAG, "internalResolveIntent: got launched with ACTION_UNDEFINED"); return; } else { Log.wtf(LOG_TAG, "internalResolveIntent: unexpected intent action: " + action); return; } }
InCallScreen初始化小结
InCallScreenUI控制流程
- incomingCallWidget:接通/挂断/短信回复时需要使用;
- dialpadButton:也就是显示或隐藏拨号盘(DTMF);
- audioButton:开启/关闭扬声器;
- muteButton:开启/关闭麦克风静音,开启之后对方无法听到你的声音;
- holdButton:开启/关闭呼叫保持;
- addButton:增加多路通话;也就是在通话的过程中可以暂停当前通话,拨打另一路通话并接通;
incomingCallWidget滑动控件
该滑动控件在来电的时候,会显示在InCallScreen界面上,默认情况下用户可以选择接听、挂断、挂断并短信回复三种模式,向右滑动为接听,向左滑动为挂断,向上滑动为挂断并选择快捷短信回复,如图6:
控制流程
1)布局文件
Phone/res/layout/incall_touch_ui.xml
<com.android.internal.widget.multiwaveview.GlowPadView android:id="@+id/incomingCallWidget" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center|bottom" android:layout_marginTop="20dip" android:layout_marginBottom="-110dip" android:background="@android:color/black" android:visibility="gone" android:gravity="top" prvandroid:targetDrawables="@array/incoming_call_widget_3way_targets" prvandroid:targetDescriptions="@array/incoming_call_widget_3way_target_descriptions" prvandroid:directionDescriptions="@array/incoming_call_widget_3way_direction_descriptions" prvandroid:handleDrawable="@drawable/ic_in_call_touch_handle" prvandroid:innerRadius="@*android:dimen/glowpadview_inner_radius" prvandroid:outerRadius="@*android:dimen/glowpadview_target_placement_radius" prvandroid:outerRingDrawable="@*android:drawable/ic_lockscreen_outerring" prvandroid:snapMargin="@*android:dimen/glowpadview_snap_margin" prvandroid:vibrationDuration="20" prvandroid:feedbackCount="1" prvandroid:glowRadius="@*android:dimen/glowpadview_glow_radius" prvandroid:pointDrawable="@*android:drawable/ic_lockscreen_glowdot" />
(2)初始化文件
Phone/src/com/android/phone/inCallTouchUi.java
(3)工作流程
首先初始化,代码如下:
private GlowPadView mIncomingCallWidget; mIncomingCallWidget = (GlowPadView) findViewById(R.id.incomingCallWidget); mIncomingCallWidget.setOnTriggerListener(this);
通过初始化代码我们可以看到其注册了一个TriggerListener并且就在本类中就有其实现,那么我继续找到其TriggerListener的实现,代码如下:
/** * Handles "Answer" and "Reject" actions for an incoming call. * We get this callback from the incoming call widget * when the user triggers an action. */ @Override public void onTrigger(View view, int whichHandle) { ... ...省略 mShowInCallControlsDuringHidingAnimation = false; switch (whichHandle) { //来电选择接听电话 case ANSWER_CALL_ID: if (DBG) log("ANSWER_CALL_ID: answer!"); cancelIncomingPingTime(); mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallAnswer); mShowInCallControlsDuringHidingAnimation = true; mLastIncomingCallActionTime = SystemClock.uptimeMillis(); break; //来电选择挂断并通过短信回复 case SEND_SMS_ID: if (DBG) log("SEND_SMS_ID!"); mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallRespondViaSms); break; //来电选择拒接 case DECLINE_CALL_ID: if (DBG) log("DECLINE_CALL_ID: reject!"); mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallReject); mLastIncomingCallActionTime = SystemClock.uptimeMillis(); break; default: Log.wtf(LOG_TAG, "onDialTrigger: unexpected whichHandle value: " + whichHandle); break; } //隐藏滑动控件 hideIncomingCallWidget(); // Regardless of what action the user did, be sure to clear out // the hint text we were displaying while the user was dragging. mInCallScreen.updateIncomingCallWidgetHint(0, 0); }
最终根据用户的不同选择,跳转到InCallScreen.java中的handleOnscreenButtonClick方法去执行具体代码,如下:
public void handleOnscreenButtonClick(int id) { ... ...省略 switch (id) { //来电正常接听 case R.id.incomingCallAnswer: internalAnswerCall(); break; //来电拒接 case R.id.incomingCallReject: ... ...省略 hangupRingingCall(); break; //来电拒接并短信回复 case R.id.incomingCallRespondViaSms: internalRespondViaSms(); break; ... ...省略 //更新InCallTouchUi updateInCallTouchUi(); }
(4)时序图
时序图以来电接听为例,如图7:图 7 来电接听时序图
dialpadButton显示/隐藏拨号盘
该控件的作用是点击之后显示或隐藏拨号盘,当电话接通之后如果用户点击该控件则会如图8所示:
图 8 DTMF拨号盘
控制流程
(1)布局文件
Phone/res/layout/incall_touch_ui.xml<ToggleButton android:id="@+id/dialpadButton" style="@style/InCallCompoundButton" android:background="@drawable/btn_dialpad" android:contentDescription="@string/onscreenShowDialpadText"/>
Phone/res/layout/incall_screen.xml
<ViewStub android:id="@+id/dtmf_twelve_key_dialer_stub" android:layout="@layout/dtmf_twelve_key_dialer_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginBottom="@dimen/dialpad_vertical_margin_dtmf"/>
(2)初始化文件
Phone/src/com/android/phone/inCallTouchUi.java
Phone/src/com/android/phone/inCallScreen.java
(3)工作流程
控件初始化代码如下:
dialpadButton控件初始化:
private CompoundButton mDialpadButton; mDialpadButton = (CompoundButton) mInCallControls.findViewById(R.id.dialpadButton); mDialpadButton.setOnClickListener(this); mDialpadButton.setOnLongClickListener(this);
这里的LongClick实际上是,在我们长按该控件时会弹出一个toast显示该控件的作用。
拨号盘控件初始化:
private DTMFTwelveKeyDialer mDialer; ViewStub stub = (ViewStub) findViewById(R.id.dtmf_twelve_key_dialer_stub); mDialer = new DTMFTwelveKeyDialer(this, stub);
我们先找到mDialpadButton的onClick实现方法,如下:
@Override public void onClick(View view) { int id = view.getId(); ... ...省略 switch (id) { ... ...省略 case R.id.dialpadButton: ... ...省略 mInCallScreen.handleOnscreenButtonClick(id); break; ... ...省略 } }
这里还是调用了InCallScreen中的handleOnscreenButtonClick方法,代码如下:
public void handleOnscreenButtonClick(int id) { ... ...省略 switch (id) { ... ...省略 case R.id.dialpadButton: onOpenCloseDialpad(); break; ... ...省略
这里最终调用onOpenCloseDialpad方法去实现打开或者关闭拨号盘,这里我们继续查看onOpenCloseDialpad方法,代码如下:
public void onOpenCloseDialpad() { ... ...省略 //判断拨号盘是否已经打开,如果是则隐藏反之则显示 if (mDialer.isOpened()) { closeDialpadInternal(true); } else { openDialpadInternal(true); } mApp.updateProximitySensorMode(mCM.getState()); } //显示拨号盘 private void openDialpadInternal(boolean animate) { mDialer.openDialer(animate); mApp.inCallUiState.showDialpad = true; } //以藏拨号盘 private void closeDialpadInternal(boolean animate) { mDialer.closeDialer(animate); mApp.inCallUiState.showDialpad = false; }
这里的拨号盘实际上是InCallScreen中一块独立的布局,也就是DTMFTwelveKeyDialer,当我们点击dialpadButton后,最终调用到openDialpadInternal或者closeDialpadInternal,通过mDialer去实现拨号盘的显示或隐藏,如mDialer.openDialer代码如下:
public void openDialer(boolean animate) { ... ...省略 if (!isOpened()) { // 这里animate=true if (animate) { AnimationUtils.Fade.show(mDialerView); ... ...省略 }
查看AnimationUtils.Fade.show方法如下:
public static void show(final View view) { ... ...省略 view.setVisibility(View.VISIBLE); ... ...省略 }
最终使得mDialerView显示到界面上。
(4)时序图
图 9 接通后显示拨号盘时序图
audioButton开启/关闭扬声器
该控件的作用是点击之后开启或关闭扬声器,也就是使得对方的通话声音能够通过外放增大。如果我们此时连接了蓝牙耳机,那么显示界面如图10所示:
图 10 接入蓝牙耳机后InCallTouchUi上audioButton改变
我可以选择三种不同的音频输出方式:Speaker即扬声器,Handset earpiece手机听筒,Bluetooth蓝牙耳机。
控制流程
(1)布局文件
Phone/res/layout/incall_touch_ui.xml
<ToggleButton android:id="@+id/audioButton" style="@style/InCallCompoundButton" android:background="@drawable/btn_compound_audio" android:contentDescription="@string/onscreenAudioText"/>
(2)初始化文件
Phone/src/com/android/phone/inCallTouchUi.java
(3)工作流程
控件初始化代码如下:
audioButton控件初始化:
private CompoundButton mAudioButton; mAudioButton = (CompoundButton) mInCallControls.findViewById(R.id.audioButton); mAudioButton.setOnClickListener(this); mAudioButton.setOnLongClickListener(this);
这里的LongClick实际上是,在我们长按该控件时会弹出一个toast显示该控件的作用。找到audioButton的onClick实现方法,如下:
@Override public void onClick(View view) { int id = view.getId(); ... ...省略 switch (id) { ... ...省略 case R.id.audioButton: ... ...省略 handleAudioButtonClick(); break; ... ...省略 } }
这里的handleAudioButtonClick方法对是否接入了蓝牙耳机进行了判断,如果是则会像图9所示那样,弹出三个选项按钮。这里我们假设没有连接蓝牙耳机,代码如下:
private void handleAudioButtonClick() { ... ...省略 //如果连接了蓝牙耳机则执行if里面的代码 if (inCallControlState.bluetoothEnabled) { ... ...省略 } else { ... ...省略 mInCallScreen.toggleSpeaker(); } }
这里我们继续查看InCallScreen中的toggleSpeaker方法:
public void toggleSpeaker() { ... ...省略 PhoneUtils.turnOnSpeaker(this, newSpeakerState, true); ... ...省略 }
这里最终交由PhoneUtils中的turnOnSpeaker去最终负责打开扬声器。
(4)时序图
muteButton开启/关闭麦克风静音
控制流程
(1)布局文件
Phone/res/layout/incall_touch_ui.xml
<ToggleButton android:id="@+id/muteButton" style="@style/InCallCompoundButton" android:background="@drawable/btn_compound_mute" android:contentDescription="@string/onscreenMuteText"/>
(2)初始化文件
Phone/src/com/android/phone/inCallTouchUi.java
(3)工作流程
控件初始化代码如下:
muteButton控件初始化:
private CompoundButton mMuteButton; mMuteButton = (CompoundButton) mInCallControls.findViewById(R.id.muteButton); mMuteButton.setOnClickListener(this); mMuteButton.setOnLongClickListener(this);
这里的LongClick实际上是,在我们长按该控件时会弹出一个toast显示该控件的作用。找到muteButton的onClick实现方法,如下:
@Override public void onClick(View view) { int id = view.getId(); ... ...省略 switch (id) { ... ...省略 case R.id.muteButton: ... ...省略 mInCallScreen.handleOnscreenButtonClick(id); break; ... ...省略 } }
依然调用了InCallScreen中的handleOnscreenButtonClick方法,代码如下:
public void handleOnscreenButtonClick(int id) { ... ...省略 switch (id) { ... ...省略 case R.id.muteButton: onMuteClick(); break; ... ...省略
然后调用onMuteClick方法来实现开启/关闭麦克风静音功能,代码如下:
public void onMuteClick() { ... ...省略 PhoneUtils.setMute(newMuteState); ... ...省略 }
最终的实现仍然在PhoneUtils中的setMute方法,该方法将继续传递直到audioManager去执行该静音操作。
(4)时序图
holdButton开启/关闭呼叫保持
控制流程
(1)布局文件
Phone/res/layout/incall_touch_ui.xml
<ToggleButton android:id="@+id/holdButton" style="@style/InCallCompoundButton" android:background="@drawable/btn_compound_hold" android:contentDescription="@string/onscreenHoldText"/>
(2)初始化文件
Phone/src/com/android/phone/inCallTouchUi.java
(3)工作流程
控件初始化代码如下:
muteButton控件初始化:
private CompoundButton mHoldButton; mHoldButton = (CompoundButton) mInCallControls.findViewById(R.id.holdButton); mHoldButton.setOnClickListener(this); mHoldButton.setOnLongClickListener(this);
这里的LongClick实际上是,在我们长按该控件时会弹出一个toast显示该控件的作用。找到holdButton的onClick实现方法,如下:
@Override public void onClick(View view) { int id = view.getId(); ... ...省略 switch (id) { ... ...省略 case R.id.muteButton: ... ...省略 mInCallScreen.handleOnscreenButtonClick(id); break; ... ...省略 } }
依然调用了InCallScreen中的handleOnscreenButtonClick方法,代码如下:
public void handleOnscreenButtonClick(int id) { ... ...省略 switch (id) { ... ...省略 case R.id.holdButton: onHoldClick(); break; ... ...省略
然后调用onHoldClick方法来实现开启/关闭呼叫保持功能,代码如下:
private void onHoldClick() { ... ...省略 if (hasActiveCall && !hasHoldingCall) { // 开启呼叫保持 PhoneUtils.switchHoldingAndActive( mCM.getFirstActiveBgCall()); // Really means "hold" in this state newHoldState = true; holdButtonEnabled = true; } else if (!hasActiveCall && hasHoldingCall && !haveMultipleHoldingCall) { // 取消呼叫保持 PhoneUtils.switchHoldingAndActive( mCM.getFirstActiveBgCall()); // Really means "unhold" in this state newHoldState = false; holdButtonEnabled = true; } ... ...省略 // 强制关闭拨号盘 closeDialpadInternal(true); // do the "closing" animation }
继续追踪可以找到PhoneUtils中的switchHoldingAndActive方法,代码如下:
static void switchHoldingAndActive(Call heldCall) { ... ...省略 try { CallManager cm = PhoneGlobals.getInstance().mCM; ... ...省略 cm.switchHoldingAndActive(heldCall); ... ...省略 }
最终的实现会通过CallManager一层层的向下传递,并最终实现呼叫保持功能。
(4)时序图
addButton添加一路通话
当用户在当前通话过程中点击该图标之后,界面出现拨号盘,如果添加一路通话成功则会如下图14所示:
图 14 添加一路通话界面以及添加后界面
控制流程
(1)布局文件
Phone/res/layout/incall_touch_ui.xml
<ImageButton android:id="@+id/addButton" style="@style/InCallButton" android:src="@drawable/ic_add_contact_holo_dark" android:contentDescription="@string/onscreenAddCallText"/>
(2)初始化文件
Phone/src/com/android/phone/inCallTouchUi.java
(3)工作流程
控件初始化代码如下:
addButton控件初始化:
private CompoundButton mAddButton; mAddButton = (CompoundButton) mInCallControls.findViewById(R.id.addButton); mAddButton.setOnClickListener(this); mAddButton.setOnLongClickListener(this);
这里的LongClick实际上是,在我们长按该控件时会弹出一个toast显示该控件的作用。找到holdButton的onClick实现方法,如下:
@Override public void onClick(View view) { int id = view.getId(); ... ...省略 switch (id) { ... ...省略 case R.id.addButton: ... ...省略 mInCallScreen.handleOnscreenButtonClick(id); break; ... ...省略 } }
依然调用了InCallScreen中的handleOnscreenButtonClick方法,代码如下:
public void handleOnscreenButtonClick(int id) { ... ...省略 switch (id) { ... ...省略 case R.id.addButton: onAddCallClick(); break; ... ...省略
然后调用onAddClick方法来实现增加一路通话的功能,代码如下:
private void onAddCallClick() { PhoneUtils.startNewCall(mCM); }
继续追踪可以找到PhoneUtils中的startNewCall方法,代码如下:
/* package */ static boolean startNewCall(final CallManager cm) { final PhoneGlobals app = PhoneGlobals.getInstance(); ... ...省略 // 将当前的通话静音 if (cm.hasActiveFgCall()) { setMuteInternal(cm.getActiveFgCall().getPhone(), true); app.setRestoreMuteOnInCallResume(true); } Intent intent = new Intent(Intent.ACTION_DIAL); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(ADD_CALL_MODE_KEY, true); try { //跳转到拨号界面 app.startActivity(intent); } catch (ActivityNotFoundException e) { ... ...省略 } return true; }
需要注意的是,当我们点击addButton后,当前通话会开启静音功能,在我们拨打新一路通话时,先前的通话将会自动切换到呼叫保持状态。这里的Intent.ACTION_DIAL查找对应的具体字符串为“android.intent.action.DIAL”,查找对应的Activity找到Contacts中的NoPhoneActivity,该Activity用于加载拨号盘。
(4)时序图
endButton挂断当前通话
控制流程
(1)布局文件
Phone/res/layout/incall_touch_ui.xml
<ImageButton android:id="@+id/endButton" style="@style/InCallEndButton" android:layout_width="match_parent" android:layout_weight="1" android:src="@drawable/ic_end_call" android:background="@drawable/end_call_background" android:contentDescription="@string/onscreenEndCallText" />
(2)初始化文件
Phone/src/com/android/phone/inCallTouchUi.java
(3)工作流程
控件初始化代码如下:
endButton控件初始化:
private CompoundButton mEndButton; mEndButton = (CompoundButton) mInCallControls.findViewById(R.id.addButton); mEndButton.setOnClickListener(this);
这里只注册了onClick的监听事件,没有LongOnClick的监听事件。找到endButton的onClick实现方法,如下:
@Override public void onClick(View view) { int id = view.getId(); ... ...省略 switch (id) { ... ...省略 case R.id.endButton: ... ...省略 mInCallScreen.handleOnscreenButtonClick(id); break; ... ...省略 } }
依然调用了InCallScreen中的handleOnscreenButtonClick方法,代码如下:
public void handleOnscreenButtonClick(int id) { ... ...省略 switch (id) { ... ...省略 case R.id.endButton: internalHangup(); break; ... ...省略
然后调用internalHangup方法来实现挂断当前通话,代码如下:
private void internalHangup() { ... ...省略 PhoneUtils.hangup(mCM); ... ...省略 } }
继续追踪可以找到PhoneUtils中的hangup方法,代码如下:
public static boolean hangup(CallManager cm) { ... ...省略 ringing = cm.getFirstActiveRingingCall(); fg = cm.getActiveFgCall(); bg = cm.getFirstActiveBgCall(); ... ...省略 //因为选择的是挂断当前通话,因此fg.isIdle()为false } else if (!fg.isIdle() || fg.state == Call.State.DISCONNECTING) { if (DBG) log("hangup(): hanging up foreground call"); hungup = hangup(fg); ... ...省略 }
这里我们需要注意,fg=cm.getActiveFgCall返回类型是Call类型的,这里的hangup(fg)实际处理代码为:
static boolean hangup(Call call) { ... ...省略 //挂断当前通话 call.hangup(); ... ...省略 }
到了这里需要注意下,这里的call对象是什么呢?因为我们这里使用的是GSM卡(WCDMA卡也一样),因此这里我们实际上得到的call对象是GsmCall.java的对象,从而直接找到GsmCall中的hangup方法,代码如下:
public void hangup() throws CallStateException { owner.hangup(this); }
这里最终会调用到GsmCallTracker中的hangup(GsmCall)方法中去,这里就不详解了。
(4)时序图
InCallScreen CallCard通话信息显示
CallCard说起来感觉很陌生,实际上在我们通话过程中,除了InCallTouchUi之外,InCallScreen所展示的界面就是CallCard了,如图17:
图 17 CallCard通话信息
如图17所显示,CallCard包含了通话时间、通话背景、通话对象号码(10086)、来电/去电状态、号码归属地、SIM卡运营商类型。
CallCard布局文件
通话信息界面实际上也在incall_screen.xml的布局中,但实际引用的布局为call_card.xml,整个call_card的布局较为复杂,因为通话可以是一路通话但也可以是两路通话,可以是来电也可以是去电,因此CallCard的布局较为繁杂,通过Hierarchy Viewer可以查看到。
CallCard界面更新
因为CallCard界面主要用于告知用户当前的通话状态,主要反映的是一些状态信息,因此我们主要查看其更新的代码,在InCallScreen的onResume中,我们可以看到有关于同步Phone状态的代码如下:
SyncWithPhoneStateStatus status = syncWithPhoneState();
而这里的syncWithPhoneState方法就是用于同步Phone的状态,代码如下:
private SyncWithPhoneStateStatus syncWithPhoneState() { ... ...省略 if (mCM.hasActiveFgCall() || mCM.hasActiveBgCall() || mCM.hasActiveRingingCall() || hasPendingMmiCodes || hasPendingMmiCodes2 || showProgressIndication || showScreenEvenAfterDisconnect) { if (VDBG) log("syncWithPhoneState: it's ok to be here; update the screen..."); updateScreen(); return SyncWithPhoneStateStatus.SUCCESS; } ... ...省略 }
这里会执行到updateScreen方法中,继续查看代码:
private void updateScreen() { ... ...省略 mCallCard.updateState(mCM); ... ...省略 }
因为这里我们主要关注CallCard的更新,其它内容就省略掉了。这里调用了CallCard的对象mCallCard,并调用其中的updataState方法。如下:
/* package */ void updateState(CallManager cm) { ... ...省略 //更新来电信息 updateRingingCall(cm); ... ...省略 //更新当前通话信息 updateForegroundCall(cm); ... ...省略 //更新已断开连接的通话信息 updateAlreadyDisconnected(cm); ... ...省略 //更新没有通话的界面信息 updateNoCall(cm); ... ...省略 }
在updateState中会根据不同的条件选择更新不同的界面,从而在InCallScreen中展示不同的结果。有以下四个方法用于更新不同状态下的CallCard信息:
- updateRingingCall:显示/更新来电界面信息;
- updateForegroundCall:显示/更新当前通话界面信息;
- updateAlreadyDisconnected:更新/显示已断开连接的通话界面信息,该界面只是一瞬间状态;
- updateNoCall:更新/显示没有通话时的通话界面信息;该界面一般不会出现,只为以防万一;
CallCard更新时序图
小结
在MTK的Android 4.2平台上,InCallScreen相对于原生的界面改动不算大,但其也增加了一些属于自己的东西,如:视屏通话界面,双卡控制界面,来电归属地等等,这些功能的添加使得Android手机在使用上更加便捷,也增加了用户体验。
在弄清楚了这界面UI的控制流程之后,对于修改InCallScreen界面有很大帮助,便于后续对InCallScreen进行个性化定制。
本文旨在分析InCallScreen上的UI控制流程,基于MTK Android 4.2的源码分析,其中不乏缺漏之处还恳请各位看官见谅。