歡迎轉(zhuǎn)載评矩,但請保留作者鏈接:http://www.reibang.com/p/ca4ab4e9817f
作為Dialer Owner块请,作一下基于M版本的總結吧榜掌。
在線源碼閱讀:http://androidxref.com
總體輪廓
手機之所以被稱為手機窘游,是因為它是一個通訊工具,而完成這一核心功能的軟件模塊么伯,即為Telephony。
Telephony包含的范圍非常廣泛卡儒,單拿上層來說田柔,大致可以劃分成五大部分:Telephony應用(Dialer
俐巴、Contacts
、Mms
)硬爆,service Telephony和service Telecomm欣舵,framework Telephony和framework Telecomm。
現(xiàn)在這一架構的主要變化是從L版本開始的缀磕,相較舊版的主要變遷可以參考:Android 4.4 Kitkat Phone工作流程淺析(十二)__4.4小結與5.0概覽
圖片資料
本文只關注Dialer缘圈,那么先看幾張Nexus 6p的實機截圖來個感性的認識:
架構分析
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由這四個包下的代碼編譯生成聪舒。
VoiceDialer
提供語音相關功能辨液,入口看下圖:
但是,此功能侵略性過強过椎,在天朝是基本殘廢的室梅,在海外多數(shù)運營商也不喜歡表示要去除,所以不予關注疚宇。
2)/packages/services下
Mms
亡鼠,Telephony
,Telecomm
敷待,生成MmsService.apk间涵,Telecom.apk與TeleService.apk,對Dialer來說是提供通話菜單功能的榜揖。
應該說不管從邏輯還是物理上勾哩,切分出來都是大有好處,這樣才能讓Android能夠良好支持第三方通訊類應用举哟。
3)/packages/providers下
TelephonyProvider
思劳,ContactsProvider
,數(shù)據(jù)創(chuàng)建及查詢妨猩,當然也是要切分的部分潜叛。
4)frameworks/opt和frameworks/base下
telephony
等和上面類似的眼熟名字,具體關系到各種功能點如MmiCode,Clear Code威兜,Number match销斟,Number format,DTMF椒舵,F(xiàn)DN等等等等蚂踊。
具體分析
看完整體架構之后,單單一個Dialer包的定位也變得很清晰了:它就只是一個撥號器而已笔宿。
1.層次結構
Dialer的UI是否美觀是個見仁見智的問題犁钟,我個人還是挺喜歡的。
這里我們只談其實現(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軸從大到小做一個側(cè)向剖面圖排列的話,主要元素是這樣的:
FAB button(FloatingActionButton)
layout中R.id.floating_action_button_container
位置即是侥加,并不是真正的FloatingActionButton
,Google工程師手搓了一個看上去有著類似效果的控件而已粪躬。用來控制Dialpad的展開和收起担败。Dialpad(Fragment)
實現(xiàn)類為xref: /packages/apps/Dialer/src/com/android/dialer/dialpad/DialpadFragment.java,填充進R.id.dialtacts_container
中镰官,自帶號碼輸入條提前,按下?lián)芴栤o后會構造相應Intent
然后啟動service Telecomm中的不可見Activity如UserCallActivity
(L中對應為CallActivity,M中為實現(xiàn)AFW泳唠,Android for Work模式引入)開始撥號處理流程狈网。-
SearchUI(Fragment)
填充在R.id.dialtacts_frame
中,注意展開與收起Dialpad時笨腥,雖然肉眼感知不到拓哺,但卻會使用不同的Fragment來提供搜索結果頁面。一個是SmartDialSearchFragment脖母,另一個是RegularSearchFragment士鸥。
這是因為,其在設計上還支持更強大的搜索功能谆级,能使用輸入法進行輸入:RegularSearchFragment
國內(nèi)的一般都把這功能直接做掉了-_-||烤礁。
Content pages(Fragment)
實現(xiàn)類為xref: /packages/apps/Dialer/src/com/android/dialer/list/ListsFragment.java,填充進R.id.dialtacts_frame
肥照,因為時間上肯定比SearchUI要早脚仔,所以在其下面。ListsFragment中使用ViewPager
又組織著三個Fragment作為之前圖示中的三個Tab頁(當滿足情況時舆绎,第四個頁面會出現(xiàn))鲤脏。這樣的嵌套是否是一個好的設計值得商榷。
2.撥號盤
實現(xiàn)代碼為/packages/apps/Dialer/src/com/android/dialer/dialpad/DialpadFragment.java
撥號盤分三個亿蒸,全都使用了PhoneCommon包中的同一套資源:
- Dialer中一個
- InCallUI中一個
- KeyGuard中有個“緊急呼叫”按鈕凑兰,會調(diào)用到service Telephony中的一個弱化版撥號盤
關于UI:Google原生設計上這三處使用了一致的UI掌桩,所以直接復用即可。但是姑食,其他手機設計有很多都是不一樣的波岛,所以這里是一個客制化比較麻煩的點。
關于雙卡撥號:Google對于雙卡的支持就是:選擇默認卡->Dialer中按下?lián)芴?>使用默認卡撥號音半。國內(nèi)很多廠商的做法卻都是在Dialpad上提供兩個按鈕则拷,如卡一“中國移動”卡二“中國聯(lián)通”,需要按哪個鍵就用哪張卡進行撥號曹鸠。其實在撥號流程中的service Telecomm中的一環(huán)有使用一個關鍵值PhoneAccountHandle
來進行判定使用哪張SIM卡煌茬,所以實現(xiàn)的方法也就只是很簡單地Intent
傳值、取值彻桃、處理即可坛善。L中撥號流程為CallActivity#processOutgoingCallIntent
->CallReceiver#processOutgoingCallIntent
->CallsManager#startOutgoingCall
,M中略有變動邻眷,開始的變?yōu)榱?code>UserCallActivity眠屎,往下找即可。
關于長按數(shù)字鍵快速撥號:
如設置2鍵撥號119肆饶,則長按2能夠立即進行撥號改衩。這個功能很長一段時間以來由MTK的MTK Plugin提供,然而在L版本中Google提供了SpeedDial驯镊,即Dialer_示例1
中的Tab頁面葫督,對聯(lián)系人收藏之后就變成了這個樣子,多了一個小卡片可以直接點擊呼叫:
雖然本身完全不是同一個東西板惑,但MTK表示此功能將廢棄橄镜。
如果要自行實現(xiàn)的話,實現(xiàn)原理大略是這樣:遵循Fragment寄宿于Activity的思路寫一個類似組件洒放,關鍵回調(diào)中調(diào)用同名方法蛉鹿。建立一個數(shù)據(jù)庫,用戶設置相應鍵位的快速撥號時往湿,如果是設置號碼妖异,那就簡單保存號碼;如果設置的是聯(lián)系人领追,那么置標志位他膳,保存聯(lián)系人條目的主鍵,每次撥號或顯示時之前绒窑,使用該主鍵查詢數(shù)據(jù)庫獲取所需信息棕孙。
3.設置
設置頁的UI非常糟糕,毫無設計可言,而且層次多得不像話蟀俊,以下演示的是如何進行通話帳戶設置:
示例5跟6都只是簡單羅列多行單調(diào)的文字钦铺,而且風格還不統(tǒng)一,一個有分隔線另一個沒有(示例5在Dialer包中肢预,示例6則在services/Telephony中矛洞,可以推斷不是同一撥人做的,而且沒有溝通好烫映,于是出現(xiàn)了如此明顯的差異)沼本,只有示例7看出了點兒Material Design的影子,但是只有一個分類的話Category又變得沒有意義了锭沟。
我期待的設置頁是層次合理抽兆,有分類帶說明文字的,下圖來自于我的開源練習之作PureNote:
示例5的頁面由Dialer
中的DialerSettingsActivity.java
提供族淮,并且辫红,會根據(jù)單雙卡而添加不同的Header,而往后的通話設置則主要由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
只是顯示兩行卡名较幌,如行一中國移動行二中國聯(lián)通,然后點擊后再復用CallFeaturesSetting
白翻。
4.數(shù)據(jù)獲取
有Dialer
特色的自然就是CallLog部分還有SearchUI部分了乍炉。
關于SearchUI,可以參考這篇文章Android撥號搜索機制源碼分析(原)滤馍。
對于CallLog岛琼,其數(shù)據(jù)查詢與更新采用的是AsyncQueryHandler
+ContentObserver
,Google應該考慮用Loader
來取代它們巢株。
參考Android4.4 Telephony流程分析——撥號應用(Dialer)的通話記錄加載過程槐瑞、Handler官方范例AsyncQueryHandler源碼解析
我只想說:讀CallLog的代碼(這里指得是數(shù)據(jù)獲取+內(nèi)容顯示),不啻于去地獄走一遭阁苞,一坨一坨的極為恐怖困檩。
代碼細節(jié)
Dialer中的一些代碼細節(jié)。
組合模式
設置監(jiān)聽器分兩種情況:setOnXXXListener
那槽,addOnXXXListener
悼沿,通常來講,后者要優(yōu)于前者骚灸,所以許多類都增加了add
方法而廢棄了set
方法糟趾。可假如說你使用的這個類現(xiàn)在只有set
方法可得怎么辦呢?只需要使用組合模式即可達成效果义郑,具體使用場景可以參考ListsFragment
蝶柿。以下代碼為一個示例:
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);
}
}
}
異步設置TextWatcher
DialpadFragment
中為了實現(xiàn)i18n號碼處理,需要給號碼輸入條添加一個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}
合一
王自如評價三星手機內(nèi)置應用時說“不同的產(chǎn)品經(jīng)理也許從來都沒有交流過,所以才會做出風格這么不統(tǒng)一的產(chǎn)品”院尔,這話放在Android M的Dialer與Contacts身上也是十分貼切啊蜻展,肉眼可辨的坑爹啊,強迫癥能忍么邀摆?
從功能上來講纵顾,你會發(fā)現(xiàn)與Dialer相比較,Contacts的存在感簡直弱爆了,它能做到的事情,Dialer全都能做击你。而且因為Contacts只能操作聯(lián)系人數(shù)據(jù)斤彼,幾乎讓人沒有想點它的興趣。
實際上蜡塌,在4.2版本中Contacts與Dialer就是同一個應用Contacts.apk,只不過分出了兩個應用入口來而已。當然蠕搜,雖然實際上是同一個應用,但在用戶感受上則是兩個收壕。
而國內(nèi)UI如MIUI還有錘子Rom都很明智地對這兩個應用進行了代碼與用戶感受上的“合一”妓灌,只不過小米是保留了兩個入口,但啟動的都是同一個應用蜜宪;而錘子是只提供Dialer應用虫埂,給你兩個應用的功能。華為最殘暴圃验,大手一揮把聯(lián)系不太大的Mms
都合了掉伏,號稱“三合一”(不過,最新版本的EMUI又只有二合一了):
如何達到這一效果澳窑?不復用原生代碼的話斧散,自己刷刷刷開寫可以解決問題,但這實在是費力照捡。復用原生代碼的同時達到這一效果颅湘,主要就三點,一是前面提到過的makefile的修改栗精,刪除兩個makefile闯参,修改最后一個makefile讓它們編譯出一個應用來瞻鹏;二是修改manifest,善用alias讓應用能正常被使用鹿寨;最后則是應用重構想辦法讓它一個頂仨了新博。