Android M Dialer完全總結

歡迎轉載,但請保留作者鏈接:http://www.lxweimin.com/p/ca4ab4e9817f
作為Dialer Owner,作一下基于M版本的總結吧。
在線源碼閱讀:http://androidxref.com

總體輪廓

手機之所以被稱為手機,是因為它是一個通訊工具,而完成這一核心功能的軟件模塊,即為Telephony。
Telephony包含的范圍非常廣泛,單拿上層來說,大致可以劃分成五大部分:Telephony應用(DialerContactsMms),service Telephony和service Telecomm,framework Telephony和framework Telecomm。
現在這一架構的主要變化是從L版本開始的,相較舊版的主要變遷可以參考:Android 4.4 Kitkat Phone工作流程淺析(十二)__4.4小結與5.0概覽


圖片資料

本文只關注Dialer,那么先看幾張Nexus 6p的實機截圖來個感性的認識:

Dialer_示例1

Dialer_示例2

Dialer_示例3

架構分析

Dialer主要涉及的包有:
1)/packages/apps下
DialerInCallUIContactsCommonPhoneCommonVoiceDialer

憑借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

DialerInCallUIPhoneCommonContactsCommon全都在src_dirs路徑下了,于是最終的Dialer.apk由這四個包下的代碼編譯生成。

VoiceDialer提供語音相關功能,入口看下圖:

VoiceDialer

但是,此功能侵略性過強,在天朝是基本殘廢的,在海外多數運營商也不喜歡表示要去除,所以不予關注。

2)/packages/services下
MmsTelephonyTelecomm,生成MmsService.apk,Telecom.apk與TeleService.apk,對Dialer來說是提供通話菜單功能的。
應該說不管從邏輯還是物理上,切分出來都是大有好處,這樣才能讓Android能夠良好支持第三方通訊類應用。

3)/packages/providers下
TelephonyProviderContactsProvider,數據創建及查詢,當然也是要切分的部分。

4)frameworks/opt和frameworks/base下
telephony等和上面類似的眼熟名字,具體關系到各種功能點如MmiCode,Clear Code,Number match,Number format,DTMF,FDN等等等等。


具體分析

看完整體架構之后,單單一個Dialer包的定位也變得很清晰了:它就只是一個撥號器而已。

1.層次結構

Dialer的UI是否美觀是個見仁見智的問題,我個人還是挺喜歡的。

這里我們只談其實現原理。
這是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軸從大到小做一個側向剖面圖排列的話,主要元素是這樣的:

Dialer UI剖面圖
層次結構
  • FAB button(FloatingActionButton)
    layout中R.id.floating_action_button_container位置即是,并不是真正的FloatingActionButton,Google工程師手搓了一個看上去有著類似效果的控件而已。用來控制Dialpad的展開和收起。

  • Dialpad(Fragment)
    實現類為xref: /packages/apps/Dialer/src/com/android/dialer/dialpad/DialpadFragment.java,填充進R.id.dialtacts_container中,自帶號碼輸入條,按下撥號鈕后會構造相應Intent然后啟動service Telecomm中的不可見Activity如UserCallActivity(L中對應為CallActivity,M中為實現AFW,Android for Work模式引入)開始撥號處理流程。

  • SearchUI(Fragment)
    填充在R.id.dialtacts_frame中,注意展開與收起Dialpad時,雖然肉眼感知不到,但卻會使用不同的Fragment來提供搜索結果頁面。一個是SmartDialSearchFragment,另一個是RegularSearchFragment
    這是因為,其在設計上還支持更強大的搜索功能,能使用輸入法進行輸入:

    RegularSearchFragment

    國內的一般都把這功能直接做掉了-_-||。

  • Content pages(Fragment)
    實現類為xref: /packages/apps/Dialer/src/com/android/dialer/list/ListsFragment.java,填充進R.id.dialtacts_frame,因為時間上肯定比SearchUI要早,所以在其下面。ListsFragment中使用ViewPager又組織著三個Fragment作為之前圖示中的三個Tab頁(當滿足情況時,第四個頁面會出現)。這樣的嵌套是否是一個好的設計值得商榷。


2.撥號盤

實現代碼為/packages/apps/Dialer/src/com/android/dialer/dialpad/DialpadFragment.java
撥號盤分三個,全都使用了PhoneCommon包中的同一套資源:

  • Dialer中一個
  • InCallUI中一個
  • KeyGuard中有個“緊急呼叫”按鈕,會調用到service Telephony中的一個弱化版撥號盤

關于UI:Google原生設計上這三處使用了一致的UI,所以直接復用即可。但是,其他手機設計有很多都是不一樣的,所以這里是一個客制化比較麻煩的點。

關于雙卡撥號:Google對于雙卡的支持就是:選擇默認卡->Dialer中按下撥號->使用默認卡撥號。國內很多廠商的做法卻都是在Dialpad上提供兩個按鈕,如卡一“中國移動”卡二“中國聯通”,需要按哪個鍵就用哪張卡進行撥號。其實在撥號流程中的service Telecomm中的一環有使用一個關鍵值PhoneAccountHandle來進行判定使用哪張SIM卡,所以實現的方法也就只是很簡單地Intent傳值、取值、處理即可。L中撥號流程為CallActivity#processOutgoingCallIntent->CallReceiver#processOutgoingCallIntent->CallsManager#startOutgoingCall,M中略有變動,開始的變為了UserCallActivity,往下找即可。

關于長按數字鍵快速撥號

華為手機樣圖1
華為手機樣圖2

如設置2鍵撥號119,則長按2能夠立即進行撥號。這個功能很長一段時間以來由MTK的MTK Plugin提供,然而在L版本中Google提供了SpeedDial,即Dialer_示例1中的Tab頁面,對聯系人收藏之后就變成了這個樣子,多了一個小卡片可以直接點擊呼叫:

SpeedDial

雖然本身完全不是同一個東西,但MTK表示此功能將廢棄。
如果要自行實現的話,實現原理大略是這樣:遵循Fragment寄宿于Activity的思路寫一個類似組件,關鍵回調中調用同名方法。建立一個數據庫,用戶設置相應鍵位的快速撥號時,如果是設置號碼,那就簡單保存號碼;如果設置的是聯系人,那么置標志位,保存聯系人條目的主鍵,每次撥號或顯示時之前,使用該主鍵查詢數據庫獲取所需信息。


3.設置

設置頁的UI非常糟糕,毫無設計可言,而且層次多得不像話,以下演示的是如何進行通話帳戶設置:

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

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

示例5的頁面由Dialer中的DialerSettingsActivity.java提供,并且,會根據單雙卡而添加不同的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,雙卡為PhoneAccountSettingsActivityPhoneAccountSettingsActivity只是顯示兩行卡名,如行一中國移動行二中國聯通,然后點擊后再復用CallFeaturesSetting


4.數據獲取

Dialer特色的自然就是CallLog部分還有SearchUI部分了。
關于SearchUI,可以參考這篇文章Android撥號搜索機制源碼分析(原)
對于CallLog,其數據查詢與更新采用的是AsyncQueryHandler+ContentObserver,Google應該考慮用Loader來取代它們。
參考Android4.4 Telephony流程分析——撥號應用(Dialer)的通話記錄加載過程Handler官方范例AsyncQueryHandler源碼解析

我只想說:讀CallLog的代碼(這里指得是數據獲取+內容顯示),不啻于去地獄走一遭,一坨一坨的極為恐怖。


代碼細節

Dialer中的一些代碼細節。

組合模式

設置監聽器分兩種情況:setOnXXXListeneraddOnXXXListener,通常來講,后者要優于前者,所以許多類都增加了add方法而廢棄了set方法。可假如說你使用的這個類現在只有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中為了實現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}

合一

Contacts_示例1
Contacts_示例2

王自如評價三星手機內置應用時說“不同的產品經理也許從來都沒有交流過,所以才會做出風格這么不統一的產品”,這話放在Android M的Dialer與Contacts身上也是十分貼切啊,肉眼可辨的坑爹啊,強迫癥能忍么?

從功能上來講,你會發現與Dialer相比較,Contacts的存在感簡直弱爆了,它能做到的事情,Dialer全都能做。而且因為Contacts只能操作聯系人數據,幾乎讓人沒有想點它的興趣。

實際上,在4.2版本中Contacts與Dialer就是同一個應用Contacts.apk,只不過分出了兩個應用入口來而已。當然,雖然實際上是同一個應用,但在用戶感受上則是兩個。

而國內UI如MIUI還有錘子Rom都很明智地對這兩個應用進行了代碼與用戶感受上的“合一”,只不過小米是保留了兩個入口,但啟動的都是同一個應用;而錘子是只提供Dialer應用,給你兩個應用的功能。華為最殘暴,大手一揮把聯系不太大的Mms都合了,號稱“三合一”(不過,最新版本的EMUI又只有二合一了):

三合一

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


推薦閱讀

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

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