本流程圖基於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相關的具體實現我這邊沒有具體詳跟,有興趣的同學可以自己再追下去看看。
部分細節方法
//CallAudioRouteStateMachine.ActiveSpeakerRoute.enter 設置一些狀態
public void enter() {
Log.i("michael","ActiveSpeakerRoute enter");
super.enter();
mWasOnSpeaker = true;
setSpeakerphoneOn(true); //打開speaker
setBluetoothOn(false);
CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_SPEAKER,
mAvailableRoutes);
setSystemAudioState(newState);
updateInternalCallAudioState();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
Mute
整體流程圖
整體流程比較簡單,通過上層一直調用到 AudioService 然後通過 JNI 的方法調用底層的具體實現。
部分細節方法
//CallAudioRouteStateMachine.setSystemAudioState 改變 statusbar 的圖標和audio的狀態
private void setSystemAudioState(CallAudioState newCallAudioState) {
///M: ALPS02797725 @{
// show mute and speaker icon in status bar
mStatusBarNotifier.notifyMute(newCallAudioState.isMuted());
mStatusBarNotifier.notifySpeakerphone(newCallAudioState.getRoute() ==
CallAudioState.ROUTE_SPEAKER);
/// @}
setSystemAudioState(newCallAudioState, false);
}
//CallAudioRouteStateMachine.updateInternalCallAudioState 改變audio的狀態供外部使用
/**
* 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 界面的過程,比較簡單,但是裏面涉及到的一些動畫還是比較有趣的。
部分細節方法
//DialpadView.animateShow 創建動畫
public void animateShow() {
// This is a hack; without this, the setTranslationY is delayed in being applied, and the
// numbers appear at their original position (0) momentarily before animating.
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) {
// Landscape orientation requires translation along the X axis.
// For RTL locales, ensure we translate negative on the X axis.
dialpadKey.setTranslationX((mIsRtl ? -1 : 1) * mTranslateDistance);
animator.translationX(0);
} else {
// Portrait orientation requires translation along the Y axis.
dialpadKey.setTranslationY(mTranslateDistance);
animator.translationY(0);
}
animator.setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
.setStartDelay(delay)
.setDuration(duration)
.setListener(showListener)
.start();
}
}
//CallCardFragment.updateFabPosition 更新hangupbutton的大小和位置
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 /* align base */,
0 /* offsetX */,
offsetY,
true);
mFloatingActionButtonController.resize(
mIsDialpadShowing ? mFabSmallDiameter : mFabNormalDiameter, true);
}
//ProximitySensor.updateProximitySensorMode 更新P-sensor的狀態
....省略部分代碼
/// M: disable Proximity Sensor during VT Call
if (mIsPhoneOffhook && !screenOnImmediately && !isVideoCall) {
Log.d(this, "Turning on proximity sensor");
// Phone is in use! Arrange for the screen to turn off
// automatically when the sensor detects a close object.
/// M: for ALPS01275578 @{
// when reject a incoming call, the call state is INCALL, but we should NOT
// acquire wake lock in this case
if (!shouldSkipAcquireProximityLock()) {
turnOnProximitySensor();
}
} else {
Log.d(this, "Turning off proximity sensor");
// Phone is either idle, or ringing. We don't want any special proximity sensor
// behavior in either case.
/// M: For ALPS01769498 @{
// Screen on immediately for incoming call, this give user a chance to notice
// the new incoming call when speaking on an existed call.
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操作。
部分細節方法
//CallsManager.holdCall 這裏有個細節,如果存在兩路通話,一個hold一個activity,並且是屬於兩個不同的phoneaccount,那麼hold 其中一個,另外一個就會unhold
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();
}
/// M: When have active call and hold call in different account, hold operation will
// swap the two call.
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();
}
/// @}
}
//TelephonyConnection.performHold 如果存在一個call waiting 的來電,那麼就不執行hold操作,讓用戶可以去接聽來電
public void performHold() {
Log.v(this, "performHold");
// TODO: Can dialing calls be put on hold as well since they take up the
// foreground call slot?
if (Call.State.ACTIVE == mConnectionState) {
Log.v(this, "Holding active call");
try {
Phone phone = mOriginalConnection.getCall().getPhone();
Call ringingCall = phone.getRingingCall();
// Although the method says switchHoldingAndActive, it eventually calls a RIL method
// called switchWaitingOrHoldingAndActive. What this means is that if we try to put
// a call on hold while a call-waiting call exists, it'll end up accepting the
// call-waiting call, which is bad if that was not the user's intention. We are
// cheating here and simply skipping it because we know any attempt to hold a call
// while a call-waiting call is happening is likely a request from Telecom prior to
// accepting the call-waiting call.
// TODO: Investigate a better solution. It would be great here if we
// could "fake" hold by silencing the audio and microphone streams for this call
// instead of actually putting it on hold.
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流程
部分細節方法
//TelecomAdapter.addCall 具體實現啓動dialer的過程
void addCall() {
if (mInCallService != null) {
Intent intent = new Intent(Intent.ACTION_DIAL);//ACTION_DIAL = "android.intent.action.DIAL"
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// when we request the dialer come up, we also want to inform
// it that we're going through the "add call" option from the
// InCallScreen / PhoneUtils.
intent.putExtra(ADD_CALL_MODE_KEY, true);
try {
Log.d(this, "Sending the add Call intent");
mInCallService.startActivity(intent);
} catch (ActivityNotFoundException e) {
// This is rather rare but possible.
// Note: this method is used even when the phone is encrypted. At that moment
// the system may not find any Activity which can accept this Intent.
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”的過程。
部分細節方法
//StorageManagerEx.getDefaultPath 拿到默認存儲錄音的路徑
/**
* 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);
//Log.i(TAG, "get path from system property, path=" + path);
} catch (IllegalArgumentException e) {
Log.e(TAG, "IllegalArgumentException when get default path:" + e);
}
// Property will be empty when first boot, should set to default
// For OTA upgrade, path is invalid, need update default path
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)) {
//Log.i(TAG, "DefaultPath invalid! " + "path = " + path + ", set to default.");
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();
//Log.i(TAG, "Find primary and visible volumeInfo, "
//+ "path=" + path + ", volumeInfo:" + vol);
break;
}
}
setDefaultPath(path);
...省略部分代碼
return path;
}
//Recorder.startRecording 創建文件,開始錄音,並往裏面寫入數據
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);
....省略部分代碼
}
//CallCardFragment.updateVoiceRecordIcon 顯示或者隱藏錄音的紅色圖標,並帶有一閃一閃的動畫
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();
}
}
/// M:[RCS] plugin API @{
ExtensionManager.getRCSeCallCardExt().updateVoiceRecordIcon(show);
/// @}
}
//CallButtonFragment.configRecordingButton 更新button 和 button 的text 內容
/**
* M: configure recording button.
*/
@Override
public void configRecordingButton() {
boolean isRecording = InCallPresenter.getInstance().isRecording();
//update for tablet and CT require.
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);//更新button的text 內容
}