本流程图基于MTK平台 Android 7.0,普通来电,本流程只作为沟通学习使用
前面介绍了一下 来电界面 的一些信息,接下来我们继续分析,看看通话界面中的 CallButtonFragment 的功能和作用。
相关类图
说明:
- BaseFragment 是 incallUI 中所有 fragment 的基类,这个类里面主要是调用了相关presenter的一些UI相关的方法,和通过了createPresenter、getUi的接口
- Presenter 是incallUI中所有presenter的基类,这个类主要实现了几个方法,onUiReady 在fragment的onViewCreated执行后调用,onUiDestroy在fragment执行onDestroyView后调用,onUiUnready的接口,主要是提供给它的子类在fragment已经销毁但是UI还没有为null这段时间的一些listen的移除等
- CallButtonFragment 具体的界面实现类,控制着界面的显示和隐藏
- call_button_fragment 界面的布局文件
- CallButtonPresenter 界面的逻辑处理类,处理和这个界面相关的一些逻辑
- CallCardFragment 可以理解为一个界面容器,CallButtonFragment 就是显示在这个容器中
- InCallPresenter 监听call的一些状态并转发给相关的presenter,并控制着InCallActivity的显示和隐藏,越来越像一个状态机,以后可能会更名
- InCallActivity 所有fragment的容器,整个通话界面,负责控制显示哪个fragment,和一些按键事件的处理
整体界面
上图红框中的部分就是本次讲解的界面 CallButtonFragment ,这里目前只考虑普通语音(voice)电话,我们可以看到其中包含了audio、mute、dialpad、hold、add_call、record等几个按钮,下面我们就会分别对它们的功能流程做介绍。
Audio
整体流程图
这里主要介绍了 audio 相关的流程,这里其实还是有点儿绕的,因为这里涉及到了多个状态,包括:通过蓝牙传递声音,通过有线耳机传递声音,通过扬声器传递声音,通过听筒传递声音等,在这个流程中,CallAudioRouteStateMachine 这个类很重要,因为这些状态的区分以及各自的逻辑都写在这个类里面,读者可以认真去看看这个类收获应该会很多。
我们这里就只画了从听筒变为扬声器的过程,最终会调用到 AudioManager 中去,audio相关的具体实现我这边没有具体详跟,有兴趣的同学可以自己再追下去看看。
部分细节方法
public void enter() { Log.i("michael","ActiveSpeakerRoute enter"); super.enter(); mWasOnSpeaker = true; setSpeakerphoneOn(true); setBluetoothOn(false); CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_SPEAKER, mAvailableRoutes); setSystemAudioState(newState); updateInternalCallAudioState(); }
Mute
整体流程图
整体流程比较简单,通过上层一直调用到 AudioService 然后通过 JNI 的方法调用底层的具体实现。
部分细节方法
private void setSystemAudioState(CallAudioState newCallAudioState) { mStatusBarNotifier.notifyMute(newCallAudioState.isMuted()); mStatusBarNotifier.notifySpeakerphone(newCallAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER); setSystemAudioState(newCallAudioState, false); } /** * Updates the CallAudioState object from current internal state. The result is used for * external communication only. */ private void updateInternalCallAudioState() { IState currentState = getCurrentState(); if (currentState == null) { Log.e(this, new IllegalStateException(), "Current state should never be null" + " when updateInternalCallAudioState is called."); mCurrentCallAudioState = new CallAudioState( mIsMuted, mCurrentCallAudioState.getRoute(), mAvailableRoutes); return; } int currentRoute = mStateNameToRouteCode.get(currentState.getName()); mCurrentCallAudioState = new CallAudioState(mIsMuted, currentRoute, mAvailableRoutes); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
Dialpad
整体流程图
这个流程主要是显示 dialpadfragment 界面的过程,比较简单,但是里面涉及到的一些动画还是比较有趣的。
部分细节方法
public void animateShow() { final AnimatorListenerAdapter showListener = new AnimatorListenerAdapter() {}; for (int i = 0; i < mButtonIds.length; i++) { int delay = (int)(getKeyButtonAnimationDelay(mButtonIds[i]) * DELAY_MULTIPLIER); int duration = (int)(getKeyButtonAnimationDuration(mButtonIds[i]) * DURATION_MULTIPLIER); final DialpadKeyButton dialpadKey = (DialpadKeyButton) findViewById(mButtonIds[i]); ViewPropertyAnimator animator = dialpadKey.animate(); if (mIsLandscape) { dialpadKey.setTranslationX((mIsRtl ? -1 : 1) * mTranslateDistance); animator.translationX(0); } else { dialpadKey.setTranslationY(mTranslateDistance); animator.translationY(0); } animator.setInterpolator(AnimUtils.EASE_OUT_EASE_IN) .setStartDelay(delay) .setDuration(duration) .setListener(showListener) .start(); } } private void updateFabPosition() { /** * M: skip update Fab position with animation when FAB is not visible and size is 0X0, * hwui will throw exception when draw view size is 0 and hardware layertype. @{ */....省略部分代码 mFloatingActionButtonController.align( FloatingActionButtonController.ALIGN_MIDDLE , 0 , offsetY, true); mFloatingActionButtonController.resize( mIsDialpadShowing ? mFabSmallDiameter : mFabNormalDiameter, true); } ....省略部分代码 if (mIsPhoneOffhook && !screenOnImmediately && !isVideoCall) { Log.d(this, "Turning on proximity sensor"); if (!shouldSkipAcquireProximityLock()) { turnOnProximitySensor(); } } else { Log.d(this, "Turning off proximity sensor"); if (InCallPresenter.getInstance().getPotentialStateFromCallList(callList) == InCallState.INCOMING) { Log.d(this, "Screen on immediately for incoming call"); screenOnImmediately = true; } turnOffProximitySensor(screenOnImmediately); }....省略部分代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
Hold
整体流程图
这个流程比较简单,从上层一层层的调用到 RILJ 然后执行hold操作。
部分细节方法
public void holdCall(Call call) { if (!mCalls.contains(call)) { Log.d(this, "Unknown call (%s) asked to be put on hold", call); } else { Log.d(this, "Putting call on hold: (%s)", call); call.hold(); } Call heldCall = getHeldCall(); Log.i("michael"," call ="+call.getTargetPhoneAccount()+" "+" heldCall ="+heldCall.getTargetPhoneAccount()); if (heldCall != null && !Objects.equals(call.getTargetPhoneAccount(), heldCall.getTargetPhoneAccount())) { Log.i("michael"," into heldCall"); heldCall.unhold(); } } public void performHold() { Log.v(this, "performHold"); if (Call.State.ACTIVE == mConnectionState) { Log.v(this, "Holding active call"); try { Phone phone = mOriginalConnection.getCall().getPhone(); Call ringingCall = phone.getRingingCall(); if (ringingCall.getState() != Call.State.WAITING) { phone.switchHoldingAndActive(); } ....省略部分代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
Add_call 和 Record
整体流程图
Add_call
流程图中红色方框部分就是 addcall 按钮的执行过程,我们可以看到其实逻辑很简单,就是再次打开 dialer 应用让用户启动第二路通话MO流程
部分细节方法
void addCall() { if (mInCallService != null) { Intent intent = new Intent(Intent.ACTION_DIAL); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(ADD_CALL_MODE_KEY, true); try { Log.d(this, "Sending the add Call intent"); mInCallService.startActivity(intent); } catch (ActivityNotFoundException e) { Log.e(this, "Activity for adding calls isn't found.", e); } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
Record
上面流程图中,除了红色方框的部分其它都是 record 的流程逻辑,乍一看感觉比较复杂,其实还是很简单,只是跨越类很多,最终通过 MediaRecorder 类以 JNI 的形式调用底层 C/C++ 的具体实现代码,这里还画出了,当 record 的状态发生了变化通过一层层的 listener,最终通知fragment 显示 record 的红色录制图标,以及将button的text内容从“Start recording”变成“Stop recording”的过程。
部分细节方法
/** * Returns default path for writing. * @hide * @internal */ public static String getDefaultPath() { String path = ""; boolean deviceTablet = false; boolean supportMultiUsers = false; try { path = SystemProperties.get(PROP_SD_DEFAULT_PATH); } catch (IllegalArgumentException e) { Log.e(TAG, "IllegalArgumentException when get default path:" + e); } if (path.equals("") || path.equals(STORAGE_PATH_SD1_ICS) || path.equals(STORAGE_PATH_SD1) || path.equals(STORAGE_PATH_SD2_ICS) || path.equals(STORAGE_PATH_SD2)) { try { IMountService mountService = IMountService.Stub.asInterface(ServiceManager.getService("mount")); if (mountService == null) { Log.e(TAG, "mount service is not initialized!"); return ""; } int userId = UserHandle.myUserId(); VolumeInfo[] volumeInfos = mountService.getVolumes(0); for (int i = 0; i < volumeInfos.length; ++i) { VolumeInfo vol = volumeInfos[i]; if (vol.isVisibleForWrite(userId) && vol.isPrimary()) { path = vol.getPathForUser(userId).getAbsolutePath(); break; } } setDefaultPath(path);...省略部分代码 return path; } public void startRecording(int outputfileformat, String extension) throws IOException { log("startRecording"); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss"); String prefix = dateFormat.format(new Date()); File sampleDir = new File(StorageManagerEx.getDefaultPath());....省略部分代码 mRecorder.prepare(); mRecorder.start(); mSampleStart = System.currentTimeMillis(); setState(RECORDING_STATE);....省略部分代码 } public void updateVoiceRecordIcon(boolean show) { mVoiceRecorderIcon.setVisibility(show ? View.VISIBLE : View.INVISIBLE); AnimationDrawable ad = (AnimationDrawable) mVoiceRecorderIcon.getDrawable(); if (ad != null) { if (show && !ad.isRunning()) { ad.start(); } else if (!show && ad.isRunning()) { ad.stop(); } } ExtensionManager.getRCSeCallCardExt().updateVoiceRecordIcon(show); } /** * M: configure recording button. */ @Override public void configRecordingButton() { boolean isRecording = InCallPresenter.getInstance().isRecording(); mRecordVoiceButton.setSelected(isRecording); mRecordVoiceButton .setContentDescription(getString(isRecording ? R.string.stop_record : R.string.start_record)); if (mOverflowPopup == null) { return; } String recordTitle = isRecording ? getString(R.string.stop_record) : getString(R.string.start_record); updatePopMenuItemTitle(BUTTON_SWITCH_VOICE_RECORD, recordTitle); }