歡迎轉(zhuǎn)載,但請(qǐng)保留作者鏈接:http://www.itdecent.cn/p/ca4ab4e9817f
作為Dialer Owner,作一下基于M版本的總結(jié)吧。
在線源碼閱讀:http://androidxref.com
總體輪廓
手機(jī)之所以被稱為手機(jī),是因?yàn)樗且粋€(gè)通訊工具,而完成這一核心功能的軟件模塊,即為Telephony。
Telephony包含的范圍非常廣泛,單拿上層來說,大致可以劃分成五大部分:Telephony應(yīng)用(Dialer、Contacts、Mms),service Telephony和service Telecomm,framework Telephony和framework Telecomm。
現(xiàn)在這一架構(gòu)的主要變化是從L版本開始的,相較舊版的主要變遷可以參考:Android 4.4 Kitkat Phone工作流程淺析(十二)__4.4小結(jié)與5.0概覽
圖片資料
本文只關(guān)注Dialer,那么先看幾張Nexus 6p的實(shí)機(jī)截圖來個(gè)感性的認(rèn)識(shí):



架構(gòu)分析
Dialer主要涉及的包有:
1)/packages/apps下
Dialer,InCallUI,ContactsCommon,PhoneCommon,VoiceDialer
憑借makefile,分包可以非常的自由隨意,看如下片斷:
6incallui_dir := ../InCallUI
7contacts_common_dir := ../ContactsCommon
8phone_common_dir := ../PhoneCommon
9
10src_dirs := src \
11 $(incallui_dir)/src \
12 $(contacts_common_dir)/src \
13 $(phone_common_dir)/src
Dialer,InCallUI,PhoneCommon,ContactsCommon全都在src_dirs路徑下了,于是最終的Dialer.apk由這四個(gè)包下的代碼編譯生成。
VoiceDialer提供語(yǔ)音相關(guān)功能,入口看下圖:

但是,此功能侵略性過強(qiáng),在天朝是基本殘廢的,在海外多數(shù)運(yùn)營(yíng)商也不喜歡表示要去除,所以不予關(guān)注。
2)/packages/services下
Mms,Telephony,Telecomm,生成MmsService.apk,Telecom.apk與TeleService.apk,對(duì)Dialer來說是提供通話菜單功能的。
應(yīng)該說不管從邏輯還是物理上,切分出來都是大有好處,這樣才能讓Android能夠良好支持第三方通訊類應(yīng)用。
3)/packages/providers下
TelephonyProvider,ContactsProvider,數(shù)據(jù)創(chuàng)建及查詢,當(dāng)然也是要切分的部分。
4)frameworks/opt和frameworks/base下
telephony等和上面類似的眼熟名字,具體關(guān)系到各種功能點(diǎn)如MmiCode,Clear Code,Number match,Number format,DTMF,F(xiàn)DN等等等等。
具體分析
看完整體架構(gòu)之后,單單一個(gè)Dialer包的定位也變得很清晰了:它就只是一個(gè)撥號(hào)器而已。
1.層次結(jié)構(gòu)
Dialer的UI是否美觀是個(gè)見仁見智的問題,我個(gè)人還是挺喜歡的。
這里我們只談其實(shí)現(xiàn)原理。
這是Dialer的主layout dialtacts_activity.xml:
16<FrameLayout
17 xmlns:android="http://schemas.android.com/apk/res/android"
18 android:id="@+id/dialtacts_mainlayout"
19 android:layout_width="match_parent"
20 android:layout_height="match_parent"
21 android:orientation="vertical"
22 android:focusable="true"
23 android:focusableInTouchMode="true"
24 android:clipChildren="false"
25 android:background="@color/background_dialer_light">
26
27 <FrameLayout
28 android:id="@+id/dialtacts_container"
29 android:layout_width="match_parent"
30 android:layout_height="match_parent"
31 android:clipChildren="false">
32 <!-- The main contacts grid -->
33 <FrameLayout
34 android:layout_height="match_parent"
35 android:layout_width="match_parent"
36 android:id="@+id/dialtacts_frame"
37 android:clipChildren="false" />
38 </FrameLayout>
39
40 <FrameLayout
41 android:id="@+id/floating_action_button_container"
42 android:background="@drawable/fab_blue"
43 android:layout_width="@dimen/floating_action_button_width"
44 android:layout_height="@dimen/floating_action_button_height"
45 android:layout_marginBottom="@dimen/floating_action_button_margin_bottom"
46 android:layout_gravity="center_horizontal|bottom">
47
48 <ImageButton
49 android:id="@+id/floating_action_button"
50 android:background="@drawable/floating_action_button"
51 android:layout_width="match_parent"
52 android:layout_height="match_parent"
53 android:contentDescription="@string/action_menu_dialpad_button"
54 android:src="@drawable/fab_ic_dial"/>
55
56 </FrameLayout>
57
58 <!-- Host container for the contact tile drag shadow -->
59 <FrameLayout
60 android:id="@+id/activity_overlay"
61 android:layout_height="match_parent"
62 android:layout_width="match_parent">
63 <ImageView
64 android:id="@+id/contact_tile_drag_shadow_overlay"
65 android:layout_width="wrap_content"
66 android:layout_height="wrap_content"
67 android:visibility="gone"
68 android:importantForAccessibility="no" />
69 </FrameLayout>
70
71</FrameLayout>
雖然不同情況下顯示的部分不同,但總得來說以z軸從大到小做一個(gè)側(cè)向剖面圖排列的話,主要元素是這樣的:


FAB button(FloatingActionButton)
layout中R.id.floating_action_button_container位置即是,并不是真正的FloatingActionButton,Google工程師手搓了一個(gè)看上去有著類似效果的控件而已。用來控制Dialpad的展開和收起。Dialpad(Fragment)
實(shí)現(xiàn)類為xref: /packages/apps/Dialer/src/com/android/dialer/dialpad/DialpadFragment.java,填充進(jìn)R.id.dialtacts_container中,自帶號(hào)碼輸入條,按下?lián)芴?hào)鈕后會(huì)構(gòu)造相應(yīng)Intent然后啟動(dòng)service Telecomm中的不可見Activity如UserCallActivity(L中對(duì)應(yīng)為CallActivity,M中為實(shí)現(xiàn)AFW,Android for Work模式引入)開始撥號(hào)處理流程。-
SearchUI(Fragment)
填充在R.id.dialtacts_frame中,注意展開與收起Dialpad時(shí),雖然肉眼感知不到,但卻會(huì)使用不同的Fragment來提供搜索結(jié)果頁(yè)面。一個(gè)是SmartDialSearchFragment,另一個(gè)是RegularSearchFragment。
這是因?yàn)椋湓谠O(shè)計(jì)上還支持更強(qiáng)大的搜索功能,能使用輸入法進(jìn)行輸入:RegularSearchFragment
國(guó)內(nèi)的一般都把這功能直接做掉了-_-||。
Content pages(Fragment)
實(shí)現(xiàn)類為xref: /packages/apps/Dialer/src/com/android/dialer/list/ListsFragment.java,填充進(jìn)R.id.dialtacts_frame,因?yàn)闀r(shí)間上肯定比SearchUI要早,所以在其下面。ListsFragment中使用ViewPager又組織著三個(gè)Fragment作為之前圖示中的三個(gè)Tab頁(yè)(當(dāng)滿足情況時(shí),第四個(gè)頁(yè)面會(huì)出現(xiàn))。這樣的嵌套是否是一個(gè)好的設(shè)計(jì)值得商榷。
2.撥號(hào)盤
實(shí)現(xiàn)代碼為/packages/apps/Dialer/src/com/android/dialer/dialpad/DialpadFragment.java
撥號(hào)盤分三個(gè),全都使用了PhoneCommon包中的同一套資源:
- Dialer中一個(gè)
- InCallUI中一個(gè)
- KeyGuard中有個(gè)“緊急呼叫”按鈕,會(huì)調(diào)用到service Telephony中的一個(gè)弱化版撥號(hào)盤
關(guān)于UI:Google原生設(shè)計(jì)上這三處使用了一致的UI,所以直接復(fù)用即可。但是,其他手機(jī)設(shè)計(jì)有很多都是不一樣的,所以這里是一個(gè)客制化比較麻煩的點(diǎn)。
關(guān)于雙卡撥號(hào):Google對(duì)于雙卡的支持就是:選擇默認(rèn)卡->Dialer中按下?lián)芴?hào)->使用默認(rèn)卡撥號(hào)。國(guó)內(nèi)很多廠商的做法卻都是在Dialpad上提供兩個(gè)按鈕,如卡一“中國(guó)移動(dòng)”卡二“中國(guó)聯(lián)通”,需要按哪個(gè)鍵就用哪張卡進(jìn)行撥號(hào)。其實(shí)在撥號(hào)流程中的service Telecomm中的一環(huán)有使用一個(gè)關(guān)鍵值PhoneAccountHandle來進(jìn)行判定使用哪張SIM卡,所以實(shí)現(xiàn)的方法也就只是很簡(jiǎn)單地Intent傳值、取值、處理即可。L中撥號(hào)流程為CallActivity#processOutgoingCallIntent->CallReceiver#processOutgoingCallIntent->CallsManager#startOutgoingCall,M中略有變動(dòng),開始的變?yōu)榱?code>UserCallActivity,往下找即可。
關(guān)于長(zhǎng)按數(shù)字鍵快速撥號(hào):


如設(shè)置2鍵撥號(hào)119,則長(zhǎng)按2能夠立即進(jìn)行撥號(hào)。這個(gè)功能很長(zhǎng)一段時(shí)間以來由MTK的MTK Plugin提供,然而在L版本中Google提供了SpeedDial,即Dialer_示例1中的Tab頁(yè)面,對(duì)聯(lián)系人收藏之后就變成了這個(gè)樣子,多了一個(gè)小卡片可以直接點(diǎn)擊呼叫:

雖然本身完全不是同一個(gè)東西,但MTK表示此功能將廢棄。
如果要自行實(shí)現(xiàn)的話,實(shí)現(xiàn)原理大略是這樣:遵循Fragment寄宿于Activity的思路寫一個(gè)類似組件,關(guān)鍵回調(diào)中調(diào)用同名方法。建立一個(gè)數(shù)據(jù)庫(kù),用戶設(shè)置相應(yīng)鍵位的快速撥號(hào)時(shí),如果是設(shè)置號(hào)碼,那就簡(jiǎn)單保存號(hào)碼;如果設(shè)置的是聯(lián)系人,那么置標(biāo)志位,保存聯(lián)系人條目的主鍵,每次撥號(hào)或顯示時(shí)之前,使用該主鍵查詢數(shù)據(jù)庫(kù)獲取所需信息。
3.設(shè)置
設(shè)置頁(yè)的UI非常糟糕,毫無設(shè)計(jì)可言,而且層次多得不像話,以下演示的是如何進(jìn)行通話帳戶設(shè)置:




示例5跟6都只是簡(jiǎn)單羅列多行單調(diào)的文字,而且風(fēng)格還不統(tǒng)一,一個(gè)有分隔線另一個(gè)沒有(示例5在Dialer包中,示例6則在services/Telephony中,可以推斷不是同一撥人做的,而且沒有溝通好,于是出現(xiàn)了如此明顯的差異),只有示例7看出了點(diǎn)兒Material Design的影子,但是只有一個(gè)分類的話Category又變得沒有意義了。
我期待的設(shè)置頁(yè)是層次合理,有分類帶說明文字的,下圖來自于我的開源練習(xí)之作PureNote:


示例5的頁(yè)面由Dialer中的DialerSettingsActivity.java提供,并且,會(huì)根據(jù)單雙卡而添加不同的Header,而往后的通話設(shè)置則主要由service Telephony提供支持,參見如下代碼:
59 // Show "Call Settings" if there is one SIM and "Phone Accounts" if there are more.
60 if (telephonyManager.getPhoneCount() <= 1) {
61 Header callSettingsHeader = new Header();
62 Intent callSettingsIntent = new Intent(TelecomManager.ACTION_SHOW_CALL_SETTINGS);
63 callSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
64
65 callSettingsHeader.titleRes = R.string.call_settings_label;
66 callSettingsHeader.intent = callSettingsIntent;
67 target.add(callSettingsHeader);
68 } else {
69 Header phoneAccountSettingsHeader = new Header();
70 Intent phoneAccountSettingsIntent =
71 new Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS);
72 phoneAccountSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
73
74 phoneAccountSettingsHeader.titleRes = R.string.phone_account_settings_label;
75 phoneAccountSettingsHeader.intent = phoneAccountSettingsIntent;
76 target.add(phoneAccountSettingsHeader);
77 }
單卡為CallFeaturesSetting,雙卡為PhoneAccountSettingsActivity,PhoneAccountSettingsActivity只是顯示兩行卡名,如行一中國(guó)移動(dòng)行二中國(guó)聯(lián)通,然后點(diǎn)擊后再?gòu)?fù)用CallFeaturesSetting。
4.數(shù)據(jù)獲取
有Dialer特色的自然就是CallLog部分還有SearchUI部分了。
關(guān)于SearchUI,可以參考這篇文章Android撥號(hào)搜索機(jī)制源碼分析(原)。
對(duì)于CallLog,其數(shù)據(jù)查詢與更新采用的是AsyncQueryHandler+ContentObserver,Google應(yīng)該考慮用Loader來取代它們。
參考Android4.4 Telephony流程分析——撥號(hào)應(yīng)用(Dialer)的通話記錄加載過程、Handler官方范例AsyncQueryHandler源碼解析
我只想說:讀CallLog的代碼(這里指得是數(shù)據(jù)獲取+內(nèi)容顯示),不啻于去地獄走一遭,一坨一坨的極為恐怖。
代碼細(xì)節(jié)
Dialer中的一些代碼細(xì)節(jié)。
組合模式
設(shè)置監(jiān)聽器分兩種情況:setOnXXXListener,addOnXXXListener,通常來講,后者要優(yōu)于前者,所以許多類都增加了add方法而廢棄了set方法??杉偃缯f你使用的這個(gè)類現(xiàn)在只有set方法可得怎么辦呢?只需要使用組合模式即可達(dá)成效果,具體使用場(chǎng)景可以參考ListsFragment。以下代碼為一個(gè)示例:
public class ViewPagerListenersUtil implements ViewPager.OnPageChangeListener {
private ArrayList<OnPageChangeListener> mOnPageChangeListeners = new rrayList<OnPageChangeListener>();
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
final int count = mOnPageChangeListeners.size();
for (int i = 0; i < count; i++) {
mOnPageChangeListeners.get(i).onPageScrolled(position, positionOffset,positionOffsetPixels);
}
}
@Override
public void onPageSelected(int position) {
final int count = mOnPageChangeListeners.size();
for (int i = 0; i < count; i++) {
mOnPageChangeListeners.get(i).onPageSelected(position);
}
}
@Override
public void onPageScrollStateChanged(int state) {
final int count = mOnPageChangeListeners.size();
for (int i = 0; i < count; i++) {
mOnPageChangeListeners.get(i).onPageScrollStateChanged(state);
}
}
public void addOnPageChangeListener(OnPageChangeListener onPageChangeListener) {
if (!mOnPageChangeListeners.contains(onPageChangeListener)) {
mOnPageChangeListeners.add(onPageChangeListener);
}
}
}
異步設(shè)置TextWatcher
DialpadFragment中為了實(shí)現(xiàn)i18n號(hào)碼處理,需要給號(hào)碼輸入條添加一個(gè)TextWatcher,Google工程師是這樣做的,其中mDigits為TextView:
370 PhoneNumberFormatter.setPhoneNumberFormattingTextWatcher(getActivity(), mDigits);
跟蹤代碼:
26public final class PhoneNumberFormatter {
27 private PhoneNumberFormatter() {}
28
29 /**
30 * Load {@link TextWatcherLoadAsyncTask} in a worker thread and set it to a {@link TextView}.
31 */
32 private static class TextWatcherLoadAsyncTask extends
33 AsyncTask<Void, Void, PhoneNumberFormattingTextWatcher> {
34 private final String mCountryCode;
35 private final TextView mTextView;
36
37 public TextWatcherLoadAsyncTask(String countryCode, TextView textView) {
38 mCountryCode = countryCode;
39 mTextView = textView;
40 }
41
42 @Override
43 protected PhoneNumberFormattingTextWatcher doInBackground(Void... params) {
44 return new PhoneNumberFormattingTextWatcher(mCountryCode);
45 }
46
47 @Override
48 protected void onPostExecute(PhoneNumberFormattingTextWatcher watcher) {
49 if (watcher == null || isCancelled()) {
50 return; // May happen if we cancel the task.
51 }
52 // Setting a text changed listener is safe even after the view is detached.
53 mTextView.addTextChangedListener(watcher);
54
55 // Note changes the user made before onPostExecute() will not be formatted, but
56 // once they type the next letter we format the entire text, so it's not a big deal.
57 // (And loading PhoneNumberFormattingTextWatcher is usually fast enough.)
58 // We could use watcher.afterTextChanged(mTextView.getEditableText()) to force format
59 // the existing content here, but that could cause unwanted results.
60 // (e.g. the contact editor thinks the user changed the content, and would save
61 // when closed even when the user didn't make other changes.)
62 }
63 }
64
65 /**
66 * Delay-set {@link PhoneNumberFormattingTextWatcher} to a {@link TextView}.
67 */
68 public static final void setPhoneNumberFormattingTextWatcher(Context context,
69 TextView textView) {
70 new TextWatcherLoadAsyncTask(GeoUtil.getCurrentCountryIso(context), textView)
71 .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
72 }
73}
合一


王自如評(píng)價(jià)三星手機(jī)內(nèi)置應(yīng)用時(shí)說“不同的產(chǎn)品經(jīng)理也許從來都沒有交流過,所以才會(huì)做出風(fēng)格這么不統(tǒng)一的產(chǎn)品”,這話放在Android M的Dialer與Contacts身上也是十分貼切啊,肉眼可辨的坑爹啊,強(qiáng)迫癥能忍么?
從功能上來講,你會(huì)發(fā)現(xiàn)與Dialer相比較,Contacts的存在感簡(jiǎn)直弱爆了,它能做到的事情,Dialer全都能做。而且因?yàn)镃ontacts只能操作聯(lián)系人數(shù)據(jù),幾乎讓人沒有想點(diǎn)它的興趣。
實(shí)際上,在4.2版本中Contacts與Dialer就是同一個(gè)應(yīng)用Contacts.apk,只不過分出了兩個(gè)應(yīng)用入口來而已。當(dāng)然,雖然實(shí)際上是同一個(gè)應(yīng)用,但在用戶感受上則是兩個(gè)。
而國(guó)內(nèi)UI如MIUI還有錘子Rom都很明智地對(duì)這兩個(gè)應(yīng)用進(jìn)行了代碼與用戶感受上的“合一”,只不過小米是保留了兩個(gè)入口,但啟動(dòng)的都是同一個(gè)應(yīng)用;而錘子是只提供Dialer應(yīng)用,給你兩個(gè)應(yīng)用的功能。華為最殘暴,大手一揮把聯(lián)系不太大的Mms都合了,號(hào)稱“三合一”(不過,最新版本的EMUI又只有二合一了):

如何達(dá)到這一效果?不復(fù)用原生代碼的話,自己刷刷刷開寫可以解決問題,但這實(shí)在是費(fèi)力。復(fù)用原生代碼的同時(shí)達(dá)到這一效果,主要就三點(diǎn),一是前面提到過的makefile的修改,刪除兩個(gè)makefile,修改最后一個(gè)makefile讓它們編譯出一個(gè)應(yīng)用來;二是修改manifest,善用alias讓應(yīng)用能正常被使用;最后則是應(yīng)用重構(gòu)想辦法讓它一個(gè)頂仨了。
