Android Auto 開發指北

語言: CN / TW / HK

背景

我的的產品作為一個海外音樂播放器,在車載場景聽歌是一個很普遍的需求。在使用者反饋中,也有很多使用者提到希望能在車上播放音樂。同時車載音樂也可以作為提升使用者消費時長一個抓手。

出海產品,主要服務於海外使用者。不同於國內的 Android 車載系統,往往是定製的 ROM,國外的 Android 車載也是 Google 一家獨大,主要可以分為 Android Auto、Automotive OS。Android Auto 可以理解為是一套將手機應用「投屏」在車載螢幕上的方案,和 iOS 的 CarPlay 類似。而 Automotive OS 則類似於國內的定製 ROM。如果要支援 Automotive OS,那我們可能要重頭開發一個適配大屏的 mini 版 app,那樣工作量很大。所以我們第一步先從支援 Android Auto 開始

準備知識

Android Auto 支援多種型別的應用,包括導航、媒體、訊息應用等。音樂播放器屬於媒體應用,因此下面給出的參考僅限於媒體應用。

Android Auto 的適配需要做提前瞭解做一些準備知識,具體如下:

  • 官方文件

    • 構建車載媒體應用

      教你明白 Android Auto 開發媒體應用的各種概念以及如何開發。這個網址的中文翻譯可讀性很差,建議直接看英文。另外,如果要開發的是導航類應用而不是媒體應用,那可以直接使用 androidX 的 Car Library 。 - 測試 Android Automotive OS 應用

      教你如何使用車機模擬器(DHU)配合手機上的 Android Auto app 來測試 Auto 應用。但是文件裡有個坑點,文件裡預設你的手機是 Pxiel 系列,Pxiel 系列是自帶 Android Auto app 的,但是國產手機基本都沒有這個 app,需要自己去下載然後安裝在手機上。可以去這裡下載。另外 Android Auto app 第一次啟動的時候會安裝好幾個谷歌服務,華為手機因為 ban 掉了google,這一步在華為手機上會失敗。所以華為手機沒法用來測試 Android Auto。推薦儘量使用 Pixel 系列手機來測試,避免測試過程中遇到奇奇怪怪的問題。

  • 車載裝置

    • 業務功能的測試,一般使用模擬器 DHU 就可以。但是在應用釋出之前,我們肯定要在真實裝置上充分測試。如果你有帶 Android Auto 的真車的話,直接用真車測試就行。但是國內帶 Android Auto 的車比較少,大概率是沒有真車可供測試的。可以去閒魚上單獨購買一個車載主機(閒魚搜尋「187b主機」即可)。
    • 還有一個坑點,應用沒有經過 google play store 分發的話,在真實車機裝置上是不會顯示的。這個問題官方文件沒有明確提到,導致當時我們 debug 的時候莫名其妙的花了很久的時間。
    • 這裡順帶提一下,谷歌分發渠道有四個,internal、closed testing、open testing、production,分別對應內部、內測、公測、產品。當然,我們不可能為了測試直接提 play store 的 production 渠道,用 internal 分發渠道用作真機測試就可以了。closed testing 和 open testing 渠道也可以。
  • 質量規範

    • http://developer.android.google.cn/docs/quality-guidelines/car-app-quality?hl=zh-cn

      因為車載場景事關駕駛員生命安全,所以 google 對 Android Auto 應用稽核很嚴格。所有支援 Android Auto 的應用,必須滿足上面連結中的質量規範才可能通過 Play Store 的稽核。這個規範要求的點很多,坑也不少,後面還會說到。這裡面很重要的一點是必須支援語音搜尋歌曲起播,這是稽核強卡的一個點。

  • 官方 demo

    • 因為文件講得很晦澀,照著文件大概率是依然很難上手開發的。可以參考 google 官方的音樂播放器 uamp,雖然這個 demo 很簡單,但是它也支援 Android Auto,有不懂的開發細節都可以參考這個 demo。
  • Android Auto app(包名為 com.google.android.projection.gearhead

    • 想要把 android auto 連上車機跑起來,必須要在手機上安裝 Android Auto app,在這裡 下載。大部分國內的 rom 是不帶這個 app 的,要自己手動安裝。pixel 系列手機是自帶的,可以在設定中搜索到這個應用。

如何開發

基本概念

首先,Android Auto 是不支援自定義 UI 的,你的應用投屏到車機上,UI 展示已經在車機內部寫死了,你能做的只是把資料傳到車機上。所以支援 Auto 的應用在車機上看起來都差不多。Auto 只允許你在播控介面的自定義操作裡新增自定義圖示。

其次,整個車機的播放流程涉及到三個部分,播放器應用、android auto app、車機。

另外還有幾個概念我們要了解

  • MediaBrowserService

    • 指的就是音樂播放器的 Service。在 Service 裡通過覆寫 onGetRootonLoadChildren 等方法對外提供車機所需要展示的資料。相當於「媒體生產者」,該生產者由你的 app 的提供。
  • MediaBrowser

    • 展示、消費你從上面的 MediaBrowserService 拿到的資料。手機上的 Android Auto app 內部包含了這個類。這個類會來主動 binds 上面提到的的 MediaBrowserService。相當於「媒體消費者」,就是 Android Auto app。
  • MediaSession

    • MediaSession 就是 app 和車機之間進行互動的橋樑。實際上 app 和車機不是直接連線進行通訊。app 是和 Android Auto app 進行 IPC 通訊,Android Auto 再和車機進行通訊。MediaSession 本質上就是一個對 IPC 的封裝。這個封裝不僅能和車機通訊,還可以和耳機線控等外部裝置進行通訊。這些外部裝置共用一個 MediaSession。

image.png

  • MediaItem、MediaMetadata、MediaDescription

    • MediaItem 代表了車機螢幕上的一個媒體元素,比如某一個頁面,一首歌,一張專輯等等,在這個類中對應的屬性設定成什麼,車機上螢幕上就顯示什麼。
    • MediaMetadata 代表 MediaItem 中各個屬性對應的值,用來構造 MediaDescription。MediaItem 持有 MediaDescription 用以描述該媒體元素該如何在車機上展示。
  • BROWSABLE 和 PLAYABLE

    • 這個一個列舉標記,車機上每一個 MediaItem 要麼是 BROWSABLE 的,要麼是 PLAYABLE 的。
    • PLAYABLE 意味著該 MediaItem 可播,這樣當你在車機螢幕上點選該 MediaItem 時,會直接進行播放。這種 MediaItem 一般是歌曲,可直接起播。
    • BROWSABLE 意味著該 MediaItem 是可瀏覽的,也就是說點選該 MediaItem 時,會載入一個新的媒體集合用於展示。比如專輯、歌單等,點選的時候會進入一個新的頁面展示歌曲列表。

上面就是做 Android Auto 開發需要理解的基本的概念,可以理解為一套車機的框架,把手機和車機互動中的各個角色都定義好了。我們的工作就是在這套框架上開發我們的業務。

車機連線手機

  • 真車上和模擬器上都可以參考官方文件 http://developer.android.com/training/cars/testing?hl=zh-cn#test-automotive-os
  • 或者可以下載這個壓縮包,執行 main.py 即可。該指令碼把上述文件裡的操作步驟都整合進去了。http://github.com/ultimateHandsomeBoy666/AndroidAutoTest

  • 連上以後,音樂自動會從車機上播放,像耳機連上手機之後音訊自動從耳機播放一樣。不需要額外在程式碼中設定。

檢測連線

  • 這是 auto 開發中的一個坑點。以往有一個方法可以檢測手機是否處在車載模式,但是該方法只對 android 12 以下的系統生效,android 12 上無效:

public static boolean isCarUiMode(Context c) { UiModeManager uiModeManager = (UiModeManager) c.getSystemService(Context.UI_MODE_SERVICE); if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) { LogHelper.d(TAG, "Running in Car mode"); return true; } else { LogHelper.d(TAG, "Running on a non-Car mode"); return false; } }

  • 官方有一個新的 CarLibrary 「androidx.car.app:app」,這個庫中使用 CarConnection 類來檢測車機和手機的連線。

    • CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
    • 但是該方法只適合於 android 6-12 的系統,並且我們只想使用連線檢測,匯入整個庫的話未免顯得太笨重。
  • 基於上述 CarLibrary 「androidx.car.app:app」的方法,我們把核心程式碼抽出來即可。具體程式碼我在 stackoverflow 上貼出來作為一個回答,也被採納了。對於 android 5 的系統,使用前述的 isCarUiMode 方法即可。

    http://stackoverflow.com/questions/39320048/how-to-detect-if-phone-is-connected-to-android-auto/72881651#72881651

工作原理

如前面 MediaSession 的介紹所述,這個工作過程涉及到車機、android auto app、應用三方。為了敘述方便,後面提到的「車機回撥客戶端方法」實際上均是中介作用的 Android Auto App 通過 IPC 完成的。

下文中的 PlayerService 指的是應用中用於播放音樂的 Service。

整體的工作流程如下面的時序圖所示,後面會逐一解釋。

image.png

bindService
  • 當我們的車機和手機連線上之後,Android Auto app 就會主動地 bind 我們的 PlayerService。因此 Service 必須宣告 android:exported="true" ,這樣才可以被外部拉起。同時整個 app 程序也被拉起了。

頁面樹
  • 整個車機螢幕的 UI 可以看做一個頁面樹,客戶端應用負責把定義各個頁面樹的節點 ID 和傳輸節點資料對應的 MediaItem。而車機在拿到這些資料以後就可以渲染螢幕 UI。
  • 車機通過 IPC 回撥到客戶端程式碼中的 onGetRoot() 方法和 onLoadChildren() 方法來獲取頁面 ID 和資料。接下來詳細說下這兩個方法。
  • 頁面樹的節點 ID 可以自己定義,只要保證每個節點對應唯一的 ID 即可。
  • 一般來說,葉子節點是 PLAYABLE 型別的 MediaItem,而非葉子節點對應 BROWSABLE 型別的 MediaItem。
onGetRoot()

override fun onGetRoot( clientPackageName: String, clientUid: Int, rootHints: Bundle? ): MediaBrowserServiceCompat.BrowserRoot

  • onGetRoot 是車機和手機互動的入口方法,只要車機和手機連線上了,就會回撥這個方法。在這個方法中,需要構造一個 BrowserRoot 返回,作為車機頁面樹的根節點。整個頁面樹構造 BrowserRoot 需要傳入 rootId。
  • clientPackageName -- 是喚起客戶端是包名,一般來說是 Android Auto App,包名是 com.google.android.projection.gearhead。但是有時候通過語音搜尋播歌喚起客戶端,包名就是 com.google.android.googlequicksearchbox,是 google 語音助手。通過這個引數可以過濾你認為有效的包名。
  • 其他兩個引數一般用不上。
onLoadChildren()

override fun onLoadChildren( parentMediaId: String, result: Result<List<MediaItem>> )

  • onLoadChildren 方法一旦被回撥,說明在車機上真正開始載入手機客戶端對應的車機頁面樹了。所以這個方法可以認為是車機端的 launch 點,可以用這個方法統計車機 DAU。
  • parentMediaId -- 之前我們會在 onGetRoot 中返回了 rootId,接下來車機會回撥 onLoadChildren, rootId 就會作為該方法的 parentMediaId 傳入,代表在此次 onLoadChildren 方法回撥中我們需要載入 parentMediaId 對應的頁面樹節點對應的 List,也就是頁面樹中該節點的子節點。
  • 載入好了子節點後,以 List 的形式,通過 result.sendResult(List) 方法將結果返回給車機。

image.png

整體的頁面樹如上所示。當在車機上點選藍色子節點時,會回撥 onLoadChildren 載入下一級子節點。當點選綠色葉子節點時,會回撥 onPlayFromMediaId 方法進行起播。下面詳細介紹。

MediaSession.Callback
  • MediaSession.Callback 是使用者在車機上進行起播、播控以及語音搜尋等操作的回撥。該抽象類中包含了一系列的回撥方法。下面介紹一些重要的回撥方法。
  • 通過 mediaSession.setCallback 方法設定回撥。

public abstract static class Callback { public boolean onMediaButtonEvent(Intent mediaButtonEvent) {}// 線控耳機回撥 public void onPlay() {} // 播放 public void onPause() {} // 暫停 public void onPlayFromMediaId(String mediaId, Bundle extras) {} // 起播 public void onPlayFromSearch(String query, Bundle extras) {} // 語音搜尋 public void onSkipToQueueItem(long id) {} // 切到播放佇列中的某一首歌 public void onSkipToNext() {} // 切到下一首 public void onSkipToPrevious() {} // 切到上一首 public void onCustomAction(String action, Bundle extras) {} // 自定義操作 .... }

MediaSession.setPlaybackState
  • 當我們執行了上述回撥後,如何通知車機進行播放頁的 UI 更新呢?這時候就需要呼叫 MediaSession.setPlaybackState 方法來通知車機更新 playback 的狀態和 UI。PlaybackState,顧名思義,就是車機播放狀態,所以改變這個狀態意味著車機播控頁 UI 也會更新。
  • 該方法具體如何使用可以參考 UMAP demo 和官方文件,這裡不展開了。
onPlayFromMediaId
  • 上面我們說到,當用戶在車機螢幕上點選歌曲起播時,就會回撥該方法,並將構建頁面樹時賦予的 id 作為 mediaId 引數傳入。因此,在該方法中我們需要呼叫客戶端的起播歌曲的方法來起播。
onPlay 和 onPause
  • 這兩個方法對應車機上的播放和暫停操作。
onSkipToNext 和 onSkipToPrevious
  • 這兩個方法對應車機上的切下一首和切上一首操作。
onSkipToQueueItem
  • 在車機上切換到當前佇列裡的某一首歌,會回撥該方法。傳入對應的歌曲 id。
  • 在佇列切歌之前,需要先給車機構造佇列。呼叫 mediaSessionCompat.setQueue(List) 即可。
onPlayFromSearch(String query, Bundle extras)
  • 當我們在車內使用語音助手起播歌曲時,比如說 「播放周杰倫的夜曲」,會回撥該方法。並且將語音內容識別成文字,分詞後將 「周杰倫 夜曲」 通過入參 query 傳入。這樣我們拿到 query 字串後,呼叫客戶端的搜尋服務就可以獲得搜尋的歌曲結果,並將該結果起播即可。
onCustomAction(String action, Bundle extras)
  • 當你想在車機播控介面新增其他自定義的操作,比如收藏歌曲、單曲迴圈等,就會用到這個方法。
  • 首先需要在構造 PlaybackState 的時候傳入你定義的自定義操作,通過 PlaybackStateBuilder.addCustomAction(CustomAction action) 方法完成。構造 aciton 時需要傳入唯一的標識字串。在重新整理了 PlaybackState 之後,在車機的播控介面就會出現自定義的操作按鈕。
  • 當在車機螢幕點選了自定義操作按鈕後,會回撥 onCustomAction 方法,入參 action 就是唯一標識字串,根據該字串來區分不同的自定義操作。

開發中的坑

Android Auto 在國內滲透率不高,所以大部分開發者對這個東西很陌生,我也是。並且很多國產 ROM 系統層就不支援 Android Auto。作為國內業界少數 Android Auto 的應用,在開發過程中經歷了資料匱乏、機型相容、稽核被拒等很多坑。這裡把之前開發踩坑的經歷分享出來。

圖示快取

車機介面每個 tab 的 icon 在設定完之後是會有在車機裡快取的。如果修改了 icon 樣式,一定要改掉對應的 drawable 的 id,不然車機會從快取中取圖片,icon 修改不生效。

機型不相容

很多國產的 rom 對 Android Auto 的支援有問題。具體表現有:

  • 無法安裝 GMS 導致無法使用 Android Auto App,以華為系手機為代表。
  • 可以安裝並且執行 Android Auto App,但是一旦連上 DHU 測試的時候,DHU 就一直黑屏,無法正常執行。親測小米 11 、vivo S15 機型有該問題。
  • 可以連線 DHU,可以正常執行,但是使用 debug 包在測試的時候,DHU 上不會顯示應用。只有使用 GP 商店分發的包才能顯示出來。 部分 vivo 手機有這個問題。

所以,最好使用 pixel 這樣的原生系統進行測試。

GP 分發

當我們在 DHU 上測完,想使用真實車機進行測試的時候,卻發現真車上不顯示我們的應用。正如之前所述,在真車上測試,需要經過 GP 分發的包才行,沒有經過 GP 分發過的包,即使是 release 包也不行。這個坑當時困擾了我們很久,最後我們也是靠猜測才猜出原因。後來我們和 google 官方進行溝通,也確認了這一點。但是坑爹的是,google 文件裡完全沒有提及這一點。

語音搜尋

語音搜尋這個功能,在 DHU 上經常莫名其妙地不好用。具體有

  • 識別不出語音,語音助手回覆 「對不起,我沒有聽懂」
  • 識別成別的應用,比如無論你怎麼說 「使用 xxx 播放音樂」,它都回復 「好的,我來讓 youtube music 播放音樂」,然後開啟 youtube music 播放音樂
  • 語音說完沒有任何反應。比如你說 「播放音樂」,在語音助手的對話方塊消失後就沒有任何反應了,也沒有回覆,也不會開啟應用播放。
  • 識別率差。我們的應用名,經常會識別錯。但是像 Spotify、微信等應用名,識別率很高。懷疑是 google 語音助手對特定應用名識別做了優化。 上面這些坑是在做語音搜尋功能時經常遇到的。QA 在測的過程中也會經常遇到並且反饋 bug 給我。但大部分時候都是語音助手抽風。

如何判斷到底是 bug 還是語音助手抽風呢,可以用同樣的語音去試下其他應用,比如 spotify 和 YT music。如果也有同樣的問題,那麼可以認為是語音助手又抽風了。

稽核

Google 商店對車載應用的稽核標準很高。詳見 質量規範,其中對車載應用需要滿足什麼樣的條件做了嚴格的要求。對於音樂類應用,有幾點容易忽視的需要格外關注:

  • 必須支援語音搜尋播歌功能。google 認為,使用者在開車時不能分散注意力,所以必須提供語音搜尋播歌的功能,讓使用者可以開車的同時按下方向盤上的麥克風按鈕,直接語音控制歌曲的播放。如果這個功能沒滿足,應用不能過審。

  • 歌曲播放時,如果手機上碰到阻塞,比如出需要手動關閉的廣告、出彈窗、請求許可權,必須讓使用者轉到手機上處理時,這時候必須要在車機螢幕上提示使用者。這時可以使用錯誤提示的 API 來做提示。

  • 新增特定的 Intent Filter

    • <activity ...> <intent-filter> <action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>

    一定要在啟動 Acitivity 裡面新增這個 intent-filter,用來相容古老版本的 android 手機語音播歌。可以參考 http://developer.android.com/guide/components/intents-common 。

    這個點其實在 google 官方文件裡有提,但是沒有明確說必須要有,只是建議新增。但是如果不加的話,應用稽核會被拒絕。所以這裡也是個坑點。