Android M Dialer完全总结

欢迎转载,但请保留作者链接:http://www.jianshu.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让应用能正常被使用;最后则是应用重构想办法让它一个顶仨了。


推荐阅读

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,351评论 0 17
  • 闲来无事写了文章,兴起投了学校书刊的稿。年代变化的快,不知什么时候起,文章不看内容甄选,倒是愿意网络拉票,敲锣打鼓...
    又新阅读 319评论 0 0
  • 童年的经历往往会对一个人的未来产生深远地影响。 小时候,大概是小学三年级开始写日记,当时我特别喜欢写,每天都写,老...
    会飞的猪pengqing阅读 526评论 2 3
  • 下雨的冬夜,显得特别的黝黑与阴沉,无边的黑夜,仿佛要将大地上的一切一并吞噬。 加班回来,已是十点多。洗了一个...
    一窗昏晓锁流年阅读 138评论 0 0