初學安卓framework系列 二 (安卓framework怎麼給開發者賦能)

語言: CN / TW / HK

在我上一篇文章裡面裡面怎麼科學的學習安卓系統的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,希望大家在繼續看我這篇文章之前先仔細看一遍。

https://developer.android.com/guide/topics/connectivity/telecom/selfManaged

https://developer.android.com/reference/android/telecom/InCallService

其中這兩篇文章重點講了兩個類,一個是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原始碼裡面,我們可以看到這一個方法

https://android.googlesource.com/platform/packages/services/Telecomm/+/master/src/com/android/server/telecom/InCallController.java#1696

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的第三方實在不多,網上資源又比較少,我也不太有時間自己擼一個(主要是懶。。。).我就打算用這個博主寫的例子:

https://medium.com/nerd-for-tech/sample-voip-calling-app-in-android-6db96d6b268b

Screenshot 2022-06-18 at 4.37.51 PM.png

這個博主是使用一個VoIP的第三方SDK來實現的一個Dialer app,大家可以預設所有的網路通話部分的實現(Connection部分)已經又第三方SDK完成了。

這裡最重要的就是這個博主的app自行實現了一個ConnectionService

https://github.com/developerspace-samples/VoIP-Call-Sample/blob/master/app/src/main/java/com/developerspace/voipcalling/utils/CallConnectionService.kt

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類一一對應來理解):

https://android.googlesource.com/platform/packages/services/Telecomm/+/nougat-release/src/com/android/server/telecom/CreateConnectionProcessor.java#121

你會發現,系統不一定會使用你自己寫的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下面的一個模組的設計理念。希望能給大家一些啟發,給讀者們自己在設計平臺的時候帶來一丟丟幫助!