Android M Dialer完全總結(jié)

歡迎轉(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í):

Dialer_示例1

Dialer_示例2

Dialer_示例3

架構(gòu)分析

Dialer主要涉及的包有:
1)/packages/apps下
DialerInCallUI,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

DialerInCallUI,PhoneCommon,ContactsCommon全都在src_dirs路徑下了,于是最終的Dialer.apk由這四個(gè)包下的代碼編譯生成。

VoiceDialer提供語(yǔ)音相關(guān)功能,入口看下圖:

VoiceDialer

但是,此功能侵略性過強(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è)向剖面圖排列的話,主要元素是這樣的:

Dialer UI剖面圖
層次結(jié)構(gòu)
  • 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)

華為手機(jī)樣圖1
華為手機(jī)樣圖2

如設(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)擊呼叫:

SpeedDial

雖然本身完全不是同一個(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è)置:

Dialer_示例4
Dialer_示例5
Dialer_示例6
Dialer_示例7

示例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}

合一

Contacts_示例1
Contacts_示例2

王自如評(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è)頂仨了。


推薦閱讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程,因...
    小菜c閱讀 7,295評(píng)論 0 17
  • 閑來無事寫了文章,興起投了學(xué)校書刊的稿。年代變化的快,不知什么時(shí)候起,文章不看內(nèi)容甄選,倒是愿意網(wǎng)絡(luò)拉票,敲鑼打鼓...
    又新閱讀 377評(píng)論 0 0
  • 童年的經(jīng)歷往往會(huì)對(duì)一個(gè)人的未來產(chǎn)生深遠(yuǎn)地影響。 小時(shí)候,大概是小學(xué)三年級(jí)開始寫日記,當(dāng)時(shí)我特別喜歡寫,每天都寫,老...
    會(huì)飛的豬pengqing閱讀 563評(píng)論 2 3
  • 下雨的冬夜,顯得特別的黝黑與陰沉,無邊的黑夜,仿佛要將大地上的一切一并吞噬。 加班回來,已是十點(diǎn)多。洗了一個(gè)...
    一窗昏曉鎖流年閱讀 221評(píng)論 0 0

友情鏈接更多精彩內(nèi)容