初学安卓framework系列 二 (安卓framework怎么给开发者赋能)

在我上一篇文章里面里面怎么科学的学习安卓系统的framework 我曾经提出过一个观点。就是我们不应该神话安卓framework里面的代码,framework里面的代码也不值得我们逐行分析。因为framework经过这么多年的迭代,很多地方很臃肿,而且谷歌的工程师也不是各个都是一顶一的天才,也会不细心写出各式各样的bug。

所以当我们学习framework代码的时候,应该站在一个更高层次去分析每个componenet的设计初衷。举个例子,

  1. 某一个功能或者模块,为什么谷歌的工程师要将其放在AOSP framework里面,而不是放在google play service (gms) 里面,同样反之亦然。

  2. 某两个gms的模块为什么不能直接通过AIDL通信,而是被设计成要通过framework通信,这样的好处是什么。

我觉得多思索这些问题,能对架构有更清晰的认识,同时也可以锻炼自己设计的能力。而不是拘泥于一些小细节。比如,熟读View.java里面事件分发的代码我觉得是无用的,你只需要粗略的认识到安卓framework是利用DFS算法(深度优先)来 遍历父子节点就行了。认识到这一点,你就可以自己理解为什么我们可以在子节点设置,通过不允许父节点事件拦截来达到取消nested scroll的效果 (NestedScrollView里面的Webview无法滑动)。

说了上面这么多废话,其实也是想强调我们要先有一个学习目的,也就是学习framework上设计好的模块,提升自己的设计能力。这次的文章我想用打电话这个framework里面的功能来作为一个例子,阐述一个比较笼统的问题:

作为一个平台,你能给开发者提供什么 (Framework API,开发规范),

作为一个平台,你希望开发者给你提供什么 (第三方实现)

友情提示,本文有些链接需要大家学习如何科学上网之后才能访问。。。

打电话?

大家都知道安卓手机里面有一个打电话的app,Dialer app。但是Dialer app并不是100%独立实现通话功能的。任何一个Dialer app,比如华为,OPPO手机自带的Dialer app都需要和framework做通信。我们先不说为什么,先看看怎么做。

首先我们要认识一点,现代的通话方式千变万化,最有名的,也是安卓当前支持的有三种

  1. Telephony call,也就是我们常说的2G通话,或者4G/LTE 通话,就是通过运营商的网络来通话,需要用户有一个有效的电话卡在手机里面(当然也可以是虚拟电话卡eSIM)

  2. VoIP call, 比如微信,WhatsAPP 等等即时通信软件的语音通话 (很多人会好奇这些不都是第三方开发的功能,为什么会和framework有关系? 我会在下面的章节继续解释),

  3. 蓝牙通话,比如大家都知道安卓手机可以连接蓝牙耳机,用户可以通过操作蓝牙耳机来进行接,挂电话的操作。但是大家有没有想过一个安卓设备也可以充当“蓝牙耳机”的角色?安卓设备(不一定是一个手机,可以是一个任何跑安卓系统的硬件)也可以通过自己本身的操作,控制与其连接的安卓手机上VoIP,Teleohony call。

怎么写一个Dialer App

安卓官方曾经发过一篇文章,讲述怎么让开发者自己写一个自己的Dialer app,希望大家在继续看我这篇文章之前先仔细看一遍。

developer.android.com/guide/topic…

developer.android.com/reference/a…

其中这两篇文章重点讲了两个类,一个是ConnectionService,一个是InCallService。其中前者是必须的,后者只在Dialer app想取代系统自带的Dialer app成为default dialer的时候才需要。

前者是开发者暴露给framework的一个类,通过实现自己的Connection,告诉framework自己当前的Dialer app如何进行具体的通话来。比如说ConnectionService需要实现一个callback来告诉framework自己的connection如何工作

Screenshot 2022-06-18 at 3.41.32 PM.png

比如说,如果你的Dialer app是通过网络把当前用户的语音发出去的话,你可能就需要再这个callback里面实现一个Connection,并且这个connection是具体实现怎么把语音转成byte data并且通过网络传出去的逻辑。

后者InCallService可以当成是一个Global callback,是系统告诉当前default dialer app应该怎么渲染UI的。比如,当你的InCallService实现接收到onCallAdded的时候,

Screenshot 2022-06-18 at 3.44.31 PM.png

你可能最好就要调用你的正在通话的Activity,告诉用户当前正在通话中,

反之,当onCallRemoved被调用的时候

Screenshot 2022-06-18 at 3.46.16 PM.png

你可能最好就要dismiss你当前通话的UI,并且用一个Toast message 告诉用户当前通话已经结束了。

Framework中通话的流程图

可能大家在阅读了这些内容之后还是有点懵逼,我来画一个图来再详细解释一下通话的流程,

无标题绘图1.jpeg

第一步: 我自己写的Dialer app实现了一个叫QingConnectionService 的ConnectionService

和一个叫QingInCallService的QingInCallService 的InCallService

通过调用Framework的API TelecomManager#placeCall() -> 文档 来告诉framework我们需要开启一个通话。

无标题绘图2.jpeg

第二步当系统接收到Dialer app通话的请求之后,会向Dialer app索取Dialer app自己实现的Connection,也就是QingConnectionService的Connection实现。并且开启Connection具体通话实现的逻辑。

无标题绘图3.jpeg

第三步当通话开始之后,系统会检索当前default的InCallService,并且调用onCallAdded 回调,提示Dialer app应该刷新UI啦! 所以在这一步QingIncallService#onCallAdded会被调用,我们的Dialer app就应该在此时更新我们的通话Activity里面的UI了。

无标题绘图4.jpeg

第四步,通话结束,系统同样会检索当前default的InCallService,并且调用onCallRemoved 回调,提示Dialer app应该告诉用户通话结束了!

Framework怎么检索这些Service的?

其实大家如果仔细看了文档就会发现这两个Service都被要求在Manifest里面做一些特殊的标识

ConnectionService:

Screenshot 2022-06-18 at 4.07.47 PM.png

InCallService

Screenshot 2022-06-18 at 4.08.59 PM.png

做这些特殊的标识就是为了Framework在遍历所有app的时候,能通过这些标识知道当前Service是ConnectionService或者InCallService。

比如InCallService。在InCallController.java源码里面,我们可以看到这一个方法

android.googlesource.com/platform/pa…

Screenshot 2022-06-18 at 4.25.15 PM.png

这个方法就是系统查找InCallService实现的地方,没有什么神奇的地方,无非就是创建Intent,给Intent设置我们在文档里面看到的那个action name,然后通过intent来遍历当前App里面的所有Service直到找到一个IncallService。

ConnectionService也是类似的方法,不过逻辑更复杂一些,这里就不多解释了,有兴趣的朋友们可以自己查阅代码看看。

这个方法也是系统常用的招数,当系统需要你实现某个Service的时候,按照开发规范一般都会要求开发者在Manifest里面填写一些特殊的标识,达到方便系统查找遍历该Service的目的。这个招数同样适用于Activity。

Sample App

因为做Dialer的第三方实在不多,网上资源又比较少,我也不太有时间自己撸一个(主要是懒。。。).我就打算用这个博主写的例子:

medium.com/nerd-for-te…

Screenshot 2022-06-18 at 4.37.51 PM.png

这个博主是使用一个VoIP的第三方SDK来实现的一个Dialer app,大家可以默认所有的网络通话部分的实现(Connection部分)已经又第三方SDK完成了。

这里最重要的就是这个博主的app自行实现了一个ConnectionService

github.com/developersp…

Screenshot 2022-06-18 at 4.41.03 PM.png

并且实现了自己的Connection,也是就是在onAwswer 里面调用SDK 的calling api

Screenshot 2022-06-18 at 4.41.23 PM.png

而且自习观察,你会发现该App并没有实现自己的IncallService,也就是说这个App无意取代系统自带的Dialer app,同时也说明这个App在通话的时候,系统自带的Dialer app的通话UI也会被显示出来。

为了证明我们猜想,看看sample app的使用视频:

Screenshot 2022-06-18 at 4.45.15 PM.png

当用户点击通话之前,这个UI是sample app的通讯录UI

Screenshot 2022-06-18 at 4.45.27 PM.png

当用户开始通话之后,InCallService#onCallAdded 被调用了。但是这个InCallService是系统自带的Dialer App的InCallService, 所以系统自带Dialer的UI被唤醒,而不是Sample 的UI。

Screenshot 2022-06-18 at 4.45.40 PM.png

即使用户把系统自带Dialer 的 界面最小化,还是可以看到系统自带Dialer app的悬浮按钮(因为当前系统自带的Dialer app已经被唤醒,同时监听着InCallService的callback,当onCallRemoved被调用后,这个悬浮按钮会消失)

以上App的行为证实了我们之前的猜想!!

Framework为什么要这么要求

说了这么多,是时候解释一下写一个打电话的App为啥要这么复杂,为什么placeCall最好不要绕过系统,要让系统通过回调来告诉Calling app UI怎么渲染。为啥我们不能把所有功能都实现在App里面呢?

答案是,你当然可以这么做,但是这样做会失去一些系统对通话体验的帮助。

两个最重要的例子:

紧急通话

如果大家看一下telecom下面的源代码 (可以把这里面的PhoneAccount类当成一个通话App的ConnectionService类一一对应来理解):

android.googlesource.com/platform/pa…

你会发现,系统不一定会使用你自己写的Dialer App里面的Connection来实现通话的,即使你是在自己实现的Dialer App内调用TelecomManager#placeCall()。其中一个很大的原因就是紧急通话。

大家看源代码里面的adjustForEmergencyCall方法

Screenshot 2022-06-18 at 5.06.37 PM.png

即使你当前想使用自己的Connection,系统也会检查你的Connection是否支持紧急通话,比如拨打110。如果当前拨打号码是紧急号码,而你自己的Dialer app并不支持紧急通话的话,系统会自动使用一个支持紧急通话的ConnectionService,比如(并且很大概率是系统自带的)TelephonyConnection, 该connection的通话都会通过手机modem拨出去。

拒绝通话

不知道大家以前有没有这样的经历。比如正在和家人通过微信语音,突然外卖小帅哥到了给你一个电话提醒上门接受外卖,你的微信语音就自动被挂断了。。。。
复制代码

这个其中的一个原因就是(我猜测的哈),就是微信的语音通话并没有通过实现系统的ConnectionService和调用TelecomManager#placeCall来做。因为微信所有的语音通话实现都在app里面自己实现,系统并不知道用户正在通话中(微信当前的运行状态和其他任何一个使用网络的app没有一点不同),所以当用户同时接收到一个拨入电话的时候,系统默认的Default Dialer就会收回MediaFocus的控制权并且开始响铃,微信在失去MediaFocus之后就自动挂断了微信语音。 (这里有没有微信的开发朋友,问一下为啥不做啊哈哈)

其中一个正面的例子就是Facebook Messenger的电话功能,大家可以看一个实例截图

WechatIMG123.jpeg

大家可以看到,当我正在和朋友进行facebook ip通话的时候,即使有拨入电话,也会经由系统在通知栏来显示该通话正在拨入中。这样可以把选择权交给用户来决定是否接受。而不是粗暴的结束当前的Ip 通话。

我自己看了一下logcat,可以看到facebook messenger实现了一个 叫 InCallForegroundService的类,而且log也对应通话进行或者结束。

Screenshot 2022-06-18 at 5.28.15 PM.png

盲猜就是fb的通话软件的确是通过系统来实现的啦!

所以,

让系统知道当前用户在使用自己的App通话,是一件十分重要的事情。

Telecom Framework设计的初衷

回到我们这篇文章的最核心问题。Telecom Framework的设计者肯定不想,也没有权利干涉第三方app对其设计通话的实现方式。但是同时又希望第三方软件能接受系统的帮助,去自动获得一些系统级别能提供的功能(比如拒绝通话,自动选择紧急通话方式等等)。

要达到以上目的,我们就自然的需要第三方软件告知系统他当前具体实现通话的方式。怎么通知?当然就靠我们自己实现的ConnectionService了。

同时系统也通过IncallService来告知App当前的通话状态,是否开始是否已经结束。这样可以大大简化第三方App开发的UI的流程。

所以最初的两个话题

作为一个平台,你能给开发者提供什么 (Framework API,开发规范,系统级别的功能),

在通话的功能中,系统提供了一些抽象类,强迫开发者实现一些抽象方法。同时也因为系统知晓这些抽象的interface,系统可以因此对这些功能(通话)进行统一管控,提供系统级别的功能(拒绝通话)。

系统也通过发布开发者文档(规范),告知第三方开发者需要怎么样暴露系统需要的实现.(在manifest里面做一些特殊标识)。

作为一个平台,你希望开发者给你提供什么 (第三方实现)

正因为系统定义了实现的抽象,第三方开发者可以在框架下,实现自己的功能(怎么进行通话?)

所以对于一个简单的通话功能,系统和第三方app之间会进行来回的信息交换,增加了程序的复杂度,这也解释了为什么很多第三方app并不想做这样的接入。

比如ConnectionService,是API23之后才加入系统的。像微信这样庞大的app,语音通话功能早在API 23之前就有了。现在要做这样的系统级别的适配想必是非常复杂的,同时收益也没有那么大(比如很少人会用微信电话打110吧。。。),所以微信没有这样做也可以理解。这也是软件开发的权衡(trade off)。

总结

这篇文章我通过通话功能来简单的介绍了一下Framework下面的一个模块的设计理念。希望能给大家一些启发,给读者们自己在设计平台的时候带来一丢丢帮助!

猜你喜欢

转载自juejin.im/post/7110746560559841311