車(chē)載Android應(yīng)用開(kāi)發(fā)與分析 - 初試 SystemUI Plugin
在前面的視頻、文章中我們介紹完了整個(gè)車(chē)載Android應(yīng)用開(kāi)發(fā)所需要的基礎(chǔ)知識(shí):
【視頻文稿】車(chē)載Android應(yīng)用開(kāi)發(fā)與分析 - 走進(jìn)車(chē)載操作系統(tǒng) - 掘金【視頻文稿】車(chē)載Android應(yīng)用開(kāi)發(fā)與分析 - AOSP的下載與編譯 - 掘金【視頻文稿】車(chē)載Android應(yīng)用開(kāi)發(fā)與分析 - 開(kāi)發(fā)系統(tǒng)應(yīng)用 - 掘金【視頻文稿】車(chē)載Android應(yīng)用開(kāi)發(fā)與分析 - AIDL實(shí)踐與封裝(上) - 掘金【視頻文稿】車(chē)載Android應(yīng)用開(kāi)發(fā)與分析 - AIDL實(shí)踐與封裝(下) - 掘金
本期內(nèi)容,我們介紹原生Android Automotive中車(chē)載應(yīng)用的實(shí)現(xiàn)方式和它的原理。首先要介紹的就是車(chē)載應(yīng)用開(kāi)發(fā)中非常重要的一個(gè)系統(tǒng)應(yīng)用,Android系統(tǒng)的UI - SystemUI。
由于原生Android系統(tǒng)的SystemUI代碼量很大、內(nèi)容也非常龐雜,這里我會(huì)挑選出對(duì)車(chē)載SystemUI開(kāi)發(fā)具有參考意義的模塊進(jìn)行介紹,大約會(huì)有4-5期的內(nèi)容,主要分為以下幾個(gè)模塊:
車(chē)載Android應(yīng)用開(kāi)發(fā)與分析 - SystemUI 「功能」與「源碼結(jié)構(gòu)」分析 - 掘金車(chē)載Android應(yīng)用開(kāi)發(fā)與分析 - 初試 SystemUI Plugin
SystemUI的源代碼可能是所有Android原生應(yīng)用中最復(fù)雜的一個(gè),當(dāng)我們需要定制SystemUI時(shí),龐大的源碼量會(huì)對(duì)的定制化開(kāi)發(fā)帶來(lái)巨大的潛在風(fēng)險(xiǎn)。所以目前車(chē)載SystemUI常見(jiàn)的做法就是,從原生SystemUI中移植少量必須的源碼,然后從頭定制一個(gè)源碼、功能完全可控的SystemUI。
重新開(kāi)發(fā)一個(gè)SystemUI就是唯一的選項(xiàng)嗎?當(dāng)然不是!Google官方早就注意到了這個(gè)問(wèn)題,所以SystemUI中提供插件化的開(kāi)發(fā)方式?- SystemUI Plugin。
本文源碼地址:/frameworks/base/+/refs/heads/main/packages/SystemUI/plugin/ExamplePlugin/
本文源碼環(huán)境基于Android 13
SystemUI Plugin
SystemUI plugin機(jī)制是一種讓SystemUI的功能可以被動(dòng)態(tài)替換或修改的方法,它可以讓開(kāi)發(fā)者快速創(chuàng)建和迭代SystemUI的原型,而盡可能少的修改SystemUI的主框架。
注意:使用Plugin并不能保證我們完全不需要修改SystemUI的主框架,畢竟需求永遠(yuǎn)是多變的。
Plugin Hooks
Plugin hooks是一些預(yù)定義的插件接口,它們可以讓?xiě)?yīng)用實(shí)現(xiàn)一些特定的功能,并通過(guò)Intent和注解來(lái)注冊(cè)和聲明插件的類(lèi)型和版本。Plugin hooks有多種類(lèi)型,例如OverlayPlugin, QSFactory, VolumeDialog等,每種類(lèi)型都有一個(gè)對(duì)應(yīng)的action和expected interface,用于標(biāo)識(shí)插件的功能和要求。
Android 13中Plugin hooks預(yù)定義接口主要有以下幾種:
BcSmartspaceDataPlugin:這個(gè)plugin可以讓?xiě)?yīng)用提供自定義的數(shù)據(jù)給鎖屏界面上的智能空間(BcSmartspace),例如天氣、日歷、新聞等。ClockProviderPlugin:這個(gè)plugin可以讓?xiě)?yīng)用提供自定義的時(shí)鐘樣式給鎖屏界面和始終應(yīng)用。DozeServicePlugin:這個(gè)plugin可以讓?xiě)?yīng)用自定義Doze模式的行為,例如控制屏幕亮度、顯示內(nèi)容、傳感器等。FalsingPlugin:這個(gè)plugin可以讓?xiě)?yīng)用自定義對(duì)誤觸(Falsing)事件的檢測(cè)和處理,例如判斷用戶(hù)是否真的想滑動(dòng)通知欄或解鎖屏幕等。GlobalActions:這個(gè)plugin可以讓?xiě)?yīng)用自定義全局操作(GlobalActions)對(duì)話(huà)框的外觀和行為,例如添加新的操作按鈕或改變對(duì)話(huà)框樣式。GlobalActionsPanelPlugin:這個(gè)plugin可以讓?xiě)?yīng)用在全局操作對(duì)話(huà)框中添加一個(gè)可展開(kāi)的面板,用于顯示更多的操作選項(xiàng)或信息。IntentButtonProvider:這個(gè)plugin可以讓?xiě)?yīng)用在鎖屏界面上添加一個(gè)自定義的按鈕,用于啟動(dòng)一個(gè)指定的Intent。NavigationEdgeBackPlugin:這個(gè)plugin可以讓?xiě)?yīng)用自定義導(dǎo)航欄邊緣返回(NavigationEdgeBack)手勢(shì)的行為,例如改變觸發(fā)區(qū)域或動(dòng)畫(huà)效果。NotificationListenerController:這個(gè)plugin可以讓?xiě)?yīng)用控制通知監(jiān)聽(tīng)器(NotificationListener)服務(wù)的連接和斷開(kāi),以及獲取通知事件和數(shù)據(jù)。NotificationMenuRowPlugin:這個(gè)plugin可以讓?xiě)?yīng)用自定義通知菜單欄(NotificationMenuRow)的外觀和行為,例如添加新的菜單項(xiàng)或改變菜單樣式。NotificationPersonExtractorPlugin:這個(gè)plugin可以讓?xiě)?yīng)用自定義從通知中提取人物信息(NotificationPersonExtractor)的邏輯,例如識(shí)別通知中包含的聯(lián)系人或頭像等。OverlayPlugin:這個(gè)plugin可以讓?xiě)?yīng)用自定義覆蓋在通知欄上方的視圖(OverlayView),用于顯示一些額外的內(nèi)容或功能。PluginFragment:這個(gè)plugin可以讓?xiě)?yīng)用在SystemUI中嵌入一個(gè)Fragment,用于顯示一些自定義的界面或功能。QSFactory:這個(gè)plugin可以讓?xiě)?yīng)用提供自定義的快速設(shè)置工廠(QSFactory),用于創(chuàng)建快速設(shè)置圖塊或面板。SensorManagerPlugin:這個(gè)plugin可以讓?xiě)?yīng)用使用SensorManager服務(wù)來(lái)注冊(cè)和取消注冊(cè)傳感器監(jiān)聽(tīng)器,以及獲取傳感器事件和數(shù)據(jù)。ToastPlugin:這個(gè)plugin可以讓?xiě)?yīng)用自定義Toast消息(Toast)的外觀和行為,例如改變Toast位置或持續(xù)時(shí)間等。ViewProvider:這個(gè)plugin可以讓?xiě)?yīng)用提供一個(gè)自定義的視圖(View),用于替換SystemUI中某些組件或功能。VolumeDialog:這個(gè)plugin可以讓?xiě)?yīng)用自定義音量調(diào)節(jié)對(duì)話(huà)框(VolumeDialog)的外觀和行為,例如添加新的音量控制選項(xiàng)或改變音量條的樣式。
Plugin 上手
創(chuàng)建一個(gè)AndroidStudio的SystemUI plugin項(xiàng)目,可以參考以下的步驟:
1)編譯SystemUIPluginLib.jar
使用Plugin之前我們需要編譯出SystemUIPluginLib.jar,在AOSP源碼根目錄執(zhí)行下面的指令。
make SystemUIPluginLib
然后就可以在下面的目錄中得到SystemUIPluginLib.jar
out/target/product/emulator_x86/obj/JAVA_LIBRARIES/SystemUIPluginLib_intermediates/javalib.jar
在AOSP的文檔中建議使用 frameworks/base/packages/SystemUI/plugin/update_plugin_lib.sh 腳本編譯 SystemUIPluginLib.jar,不過(guò)我編譯時(shí)出現(xiàn)了環(huán)境配置問(wèn)題。
2)配置系統(tǒng)簽名
在build.gradle中配置系統(tǒng)簽名。
android {
...
signingConfigs {
sign {
storeFile file('system.keystore')
storePassword '123456'
keyAlias 'cardemo'
keyPassword '123456'
}
}
buildTypes {
release {
minifyEnabled false
signingConfig signingConfigs.sign
}
debug {
minifyEnabled false
signingConfig signingConfigs.sign
}
}
}
關(guān)于如何制作系統(tǒng)簽名,請(qǐng)參考:車(chē)載Android應(yīng)用開(kāi)發(fā)與分析 - 開(kāi)發(fā)系統(tǒng)應(yīng)用 - 掘金
3)創(chuàng)建一個(gè)Plugin
在plugin項(xiàng)目中定義一個(gè)類(lèi),實(shí)現(xiàn)自Plugin中已經(jīng)提供的各種插件,并使用Requires注解聲明target和version字段,這些字段用于標(biāo)識(shí)插件的類(lèi)型和版本。
@Requires(target = OverlayPlugin.class, version = OverlayPlugin.VERSION)
public class SampleOverlayPlugin implements OverlayPlugin {
private static final String TAG = "SampleOverlayPlugin";
private Context mPluginContext;
private View mStatusBarView;
private View mNavBarView;
@Override
public void onCreate(Context sysuiContext, Context pluginContext) {
Log.d(TAG, "onCreate");
mPluginContext = pluginContext;
}
@Override
public void onDestroy() {
if (mInputSetup) {
mStatusBarView.getViewTreeObserver().removeOnComputeInternalInsetsListener(
onComputeInternalInsetsListener);
}
Log.d(TAG, "onDestroy");
if (mStatusBarView != null) {
mStatusBarView.post(() -> ((ViewGroup) mStatusBarView.getParent()).removeView(mStatusBarView));
}
if (mNavBarView != null) {
mNavBarView.post(() -> ((ViewGroup) mNavBarView.getParent()).removeView(mNavBarView));
}
}
@Override
public void setup(View notificationShadeWindowView, View navBar) {
Log.d(TAG, "Setup");
if (notificationShadeWindowView instanceof ViewGroup) {
mStatusBarView = LayoutInflater.from(mPluginContext)
.inflate(R.layout.colored_overlay, (ViewGroup) notificationShadeWindowView, false);
((ViewGroup) notificationShadeWindowView).removeAllViews();
((ViewGroup) notificationShadeWindowView).addView(mStatusBarView);
}
if (navBar instanceof ViewGroup) {
mNavBarView = LayoutInflater.from(mPluginContext)
.inflate(R.layout.colored_overlay, (ViewGroup) navBar, false);
((ViewGroup) navBar).removeAllViews();
((ViewGroup) navBar).addView(mNavBarView);
}
}
}
注意:Android不同版本中SystemUI的代碼存在不小的差異,例如:Android13中setup(View statusBar, View navBar)中返回的statusBar實(shí)際上是NotificationShadeWindowView。
4)注冊(cè)Plugin
在plugin項(xiàng)目的AndroidManifest中注冊(cè)一個(gè)service,使用action和permission屬性指定插件的接口和權(quán)限,這樣SystemUI就可以通過(guò)Intent找到插件。
android:name=".SampleOverlayPlugin" android:exported="false" android:label="@string/plugin_label" tools:ignore="Instantiatable">
的name可以在我們實(shí)現(xiàn)的plugin接口中找到。
SystemUI為了保證系統(tǒng)安全,對(duì)于plugin的加載,構(gòu)筑了兩道防線(xiàn):
第一道防線(xiàn)是Build.IS_DEBUGGABLE檢查。SysUI 在掃描或加載設(shè)備上的任何插件之前,會(huì)檢查Build.IS_DEBUGGABLE,以確保構(gòu)建是可調(diào)試的。
第二道防線(xiàn)是就是簽名權(quán)限。所有插件都必須被系統(tǒng)簽名且持有com.android.systemui.permission.PLUGIN權(quán)限才能加載其任何代碼,否則將記錄違規(guī)行為,并忽略插件。
5)運(yùn)行Plugin
將plugin.apk push 到Android 13 模擬器的/system/priv-app/ 目錄下,重啟??梢钥吹饺缦碌男Ч?/p>
NavBar的所有子View被移除,并添加了一個(gè)紅色的View;NotificationShadeWindowView的所有子View被移除,并添加了一個(gè)紅色的View。
總結(jié)
本文初試了SystemUI插件機(jī)制,在編寫(xiě)本文時(shí)發(fā)現(xiàn)Plugin相關(guān)的資料少的可憐,即使是官方資料有的也過(guò)時(shí)了。所以就像標(biāo)題那樣,本文只是簡(jiǎn)單嘗試了Plugin,如何使用Plugin來(lái)詳細(xì)定制一個(gè)完全符合我們需求的SystemUI呢?這個(gè)我們放到以后再寫(xiě),因?yàn)榻酉聛?lái)需要先來(lái)分析SystemUI Plugin的原理,在資料如此稀少的情況下,不了解原理幾乎無(wú)法寫(xiě)出符合需求的Plugin。在分析的原理的過(guò)程中,我們會(huì)逐步補(bǔ)完、理解一些Plugin的概念。
以上就是本文的所有內(nèi)容,感謝你的閱讀,希望對(duì)你所有幫助。
參考資料
Sysui plugin
SystemUI Plugin 簡(jiǎn)介及使用
/SystemUI/docs/plugins.md
推薦閱讀
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場(chǎng)。
轉(zhuǎn)載請(qǐng)注明,如有侵權(quán),聯(lián)系刪除。