1. HVAC 功能介紹
HVAC 全稱(chēng):供暖通風(fēng)與空氣調(diào)節(jié)(Heating Ventilation and Air Conditioning)。用戶可以通過(guò)他來(lái)控制整個(gè)汽車(chē)的空調(diào)系統(tǒng),是汽車(chē)中非常重要的一個(gè)功能。
汽車(chē)的空調(diào)HMI雖然并不復(fù)雜,但是大多都是用符號(hào)來(lái)表示功能,對(duì)于還沒(méi)有實(shí)際用過(guò)汽車(chē)空調(diào)系統(tǒng)的開(kāi)發(fā)者來(lái)說(shuō),理解空調(diào)的各個(gè)符號(hào)表示的含義也是非常有必要。
下面就以Android 12中的HVAC來(lái)介紹空調(diào)系統(tǒng)中包含的最基礎(chǔ)的功能。
1.1 雙區(qū)溫度調(diào)節(jié)
空調(diào)的溫度調(diào)節(jié)功能,默認(rèn)是華氏度,可以在系統(tǒng)設(shè)置修改溫度單位。可調(diào)節(jié)范圍是61 - 82華氏度,對(duì)應(yīng)16 - 28 攝氏度。
左側(cè)按鈕用來(lái)調(diào)節(jié)主駕,右側(cè)按鈕用來(lái)調(diào)節(jié)副駕。在以往都是只有高配車(chē)型才有雙區(qū)空調(diào),現(xiàn)在的車(chē)上雙區(qū)空調(diào)幾乎已經(jīng)是標(biāo)配了。
1.2 空調(diào)開(kāi)關(guān)
開(kāi)啟關(guān)閉空調(diào)的開(kāi)關(guān)
1.3 內(nèi)/外循環(huán)
內(nèi)循環(huán)是汽車(chē)空氣調(diào)節(jié)系統(tǒng)的一種狀態(tài)。這種狀態(tài)下,車(chē)內(nèi)外的換氣通道關(guān)閉,風(fēng)機(jī)關(guān)閉時(shí)車(chē)內(nèi)氣流不循環(huán),風(fēng)機(jī)開(kāi)啟時(shí),吸入的氣流也僅來(lái)自車(chē)內(nèi),形成車(chē)輛內(nèi)部的氣流循環(huán)。
外循環(huán)則相反,風(fēng)機(jī)開(kāi)啟時(shí),吸入的氣流也僅來(lái)自車(chē)外,可以更新車(chē)內(nèi)的空氣質(zhì)量,代價(jià)是會(huì)更耗電。
1.4 風(fēng)量調(diào)節(jié)
用于增大或減小空調(diào)的風(fēng)量。
1.5 風(fēng)向調(diào)節(jié)
從左到右分別是吹臉、吹臉+吹腳、吹腳、吹腳+吹擋風(fēng)玻璃
1.6 A/C開(kāi)關(guān)
A/C按鍵,它就是制冷開(kāi)關(guān),按下A/C按鍵,也就啟動(dòng)了壓縮機(jī),通俗地說(shuō)就是開(kāi)冷氣。
1.7 主副駕座椅加熱
左邊的按鈕用于調(diào)節(jié)主駕座椅加熱,右邊的按鈕用于調(diào)節(jié)副駕座椅加熱
1.8 除霜
左邊的按鈕是開(kāi)啟/關(guān)閉 前擋風(fēng)玻璃加熱,開(kāi)啟后用來(lái)除去前擋風(fēng)玻璃上的霧氣。右邊的按鈕是開(kāi)啟/關(guān)閉后擋風(fēng)玻璃加熱,開(kāi)啟后用來(lái)除去后擋風(fēng)玻璃上的霧氣。
1.9 自動(dòng)模式
自動(dòng)空調(diào)其實(shí)就是省略了風(fēng)速、風(fēng)向等調(diào)節(jié)功能,自動(dòng)空調(diào)是全自動(dòng)調(diào)節(jié),只需要選擇風(fēng)向和設(shè)定溫度。AUTO按鍵按下后,就會(huì)根據(jù)車(chē)內(nèi)傳感器來(lái)控制出風(fēng)的溫度,冬天熱風(fēng),夏天冷風(fēng)。會(huì)保持車(chē)內(nèi)有較適宜的溫度,如果溫度過(guò)高或過(guò)低,空調(diào)也會(huì)自動(dòng)改變出風(fēng)口的溫度及風(fēng)速,調(diào)整車(chē)內(nèi)溫度。
以上就是車(chē)載空調(diào)系統(tǒng)中最基礎(chǔ)的功能了,實(shí)際開(kāi)發(fā)中我們還會(huì)遇到如座椅通風(fēng)、座椅按摩、智能新風(fēng)、負(fù)離子等等一些近幾年才出現(xiàn)的空調(diào)新功能,在應(yīng)用開(kāi)發(fā)上無(wú)非就是多幾個(gè)界面或按鈕。
2. HVAC 源碼結(jié)構(gòu)
本文中的源碼基于Android 12下HVAC APP,源碼請(qǐng)見(jiàn):https://github.com/linux-link/CarHvac
原生的Hvac App中不存在Activity、Fragment等傳統(tǒng)意義上用來(lái)顯示HMI的組件,取而代之是使用Service來(lái)顯示一個(gè)Window。主要原因在于Hvac的界面層級(jí)比一般的HMI的層級(jí)要高,呼出Hvac時(shí)需要部分或全部覆蓋其他的應(yīng)用上(當(dāng)然IVI中還是有應(yīng)用比Hvac的層級(jí)要高的),這時(shí)候使用Activity就顯不合適了。
需要注意的是,Havc在Android 12中雖然有一個(gè)獨(dú)立的app,但是上圖展示空調(diào)并沒(méi)有使用這個(gè)獨(dú)立的app,它的HMI和邏輯實(shí)現(xiàn)都是直接寫(xiě)在SystemUI中的。
我們可以通過(guò)adb發(fā)送一個(gè)廣播來(lái)調(diào)出獨(dú)立的Hvac應(yīng)用。
adb shell am broadcast -a android.car.intent.action.TOGGLE_HVAC_CONTROLS
以下是Hvac App的關(guān)鍵部分的源碼結(jié)構(gòu)圖
3. HVAC 核心源碼分析
3.1 AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.car.hvac">
<uses-sdk
android:minSdkVersion="22"
android:targetSdkVersion="29" />
<uses-permission android:name="android.car.permission.CONTROL_CAR_CLIMATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Required to use the TYPE_DISPLAY_OVERLAY layout param for the overlay hvac ui-->
<uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" />
<!-- Allow Hvac to go across all users-->
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
<protected-broadcast android:name="android.car.intent.action.TOGGLE_HVAC_CONTROLS" />
<application
android:icon="@drawable/ic_launcher_hvac"
android:label="@string/hvac_label"
android:persistent="true">
<!--用于控制空調(diào)功能的Service-->
<service
android:name=".HvacController"
android:exported="false"
android:singleUser="true" />
<!-- 用于顯示UI的Service-->
<service
android:name=".HvacUiService"
android:exported="false"
android:singleUser="true" />
<!-- 監(jiān)聽(tīng)開(kāi)機(jī)廣播 -->
<receiver
android:name=".BootCompleteReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>
3.2 BootCompleteReceiver
用于監(jiān)聽(tīng)開(kāi)機(jī)的廣播,當(dāng)前收到系統(tǒng)的開(kāi)機(jī)廣播后,會(huì)將HvacUiService拉起。
public class BootCompleteReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Intent hvacUiService = new Intent(context, HvacUiService.class);
context.startService(hvacUiService);
}
}
3.3 HvacUiService
HvacUiService 用來(lái)托管Hvac UI的Service。從名字上也能看出,整個(gè)HvacUiService都是圍繞著如何將Hvac準(zhǔn)確的繪制出來(lái),基本不含其他的邏輯。
@Override
public void onCreate() {
...
// 由于不存在從服務(wù)內(nèi)部獲取系統(tǒng)ui可見(jiàn)性的方法,因此我們將全屏放置一些東西,并檢查其最終測(cè)量結(jié)果,作為獲取該信息的黑客手段。
// 一旦我們有了初始狀態(tài),我們就可以安全地從那時(shí)開(kāi)始注冊(cè)更改事件。
View windowSizeTest = new View(this) {
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Log.i(TAG, "onLayout: changed" + changed + ";left:" + left + ";top:" + top + ";right:" + right + ";bottom" + bottom);
boolean sysUIShowing = (mDisplayMetrics.heightPixels != bottom);
mInitialYOffset = (sysUIShowing) ? -mNavBarHeight : 0;
Log.i(TAG, "onLayout: sysUIShowing:" + sysUIShowing + ";mInitialYOffset" + mInitialYOffset);
layoutHvacUi();
// 我們現(xiàn)在有了初始狀態(tài),因此不再需要這個(gè)空視圖。
mWindowManager.removeView(this);
mAddedViews.remove(this);
}
};
addViewToWindowManagerAndTrack(windowSizeTest, testparams);
// 接收事件的廣播
IntentFilter filter = new IntentFilter();
filter.addAction(CAR_INTENT_ACTION_TOGGLE_HVAC_CONTROLS);
filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
// 注冊(cè)接收器,以便任何具有CONTROL_CAR_CLIMATE權(quán)限的用戶都可以調(diào)用它。
registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter,
Car.PERMISSION_CONTROL_CAR_CLIMATE, null);
}
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.i(TAG, "onReceive: " + action);
// 自定義廣播,用于展開(kāi)Hvac的HMI
if (action.equals(CAR_INTENT_ACTION_TOGGLE_HVAC_CONTROLS)) {
mHvacPanelController.toggleHvacUi();
} else if (action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) {
// home 按鍵的廣播,收起Hvac的HMI
mHvacPanelController.collapseHvacUi();
}
}
};
// 添加View到WindowManager中
private void addViewToWindowManagerAndTrack(View view, WindowManager.LayoutParams params) {
mWindowManager.addView(view, params);
mAddedViews.add(view);
}
HvacUIService在onCreate()中主要完成兩件事:
1.注冊(cè)事件廣播。這個(gè)事件實(shí)際并沒(méi)有發(fā)送源,因?yàn)镾ystemUI中額外寫(xiě)了一個(gè)Hvac,不過(guò)正是這個(gè)廣播讓我們可以把這個(gè)單獨(dú)的Hvac調(diào)出。
2.繪制UI。HvacUIService在被拉起后并沒(méi)有立即開(kāi)始UI的繪制,而是在屏幕上臨時(shí)放置一個(gè)用于測(cè)量窗口的 windowSizeTest ,當(dāng)windowSizeTestView開(kāi)始測(cè)量后,通過(guò)比對(duì)View的高度和屏幕的高度,即可判斷出systemUI是否已經(jīng)顯示,這時(shí)就可以開(kāi)始著手繪制真正的Hvac的UI了,并且可以更安全的操作UI。
接下來(lái)就是繪制真正的Hvac界面:
/**
* 在確定最小偏移量后調(diào)用。
* 這將生成HVAC UI所需的所有組件的布局。
* 啟動(dòng)時(shí),折疊視圖所需的所有窗口都可見(jiàn),而展開(kāi)視圖的窗口已創(chuàng)建并調(diào)整大小,但不可見(jiàn)。
*/
private void layoutHvacUi() {
LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
& ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
PixelFormat.TRANSLUCENT);
params.packageName = this.getPackageName();
params.gravity = Gravity.BOTTOM | Gravity.LEFT;
params.x = 0;
params.y = mInitialYOffset;
params.width = mScreenWidth;
params.height = mScreenBottom;
params.setTitle("HVAC Container");
disableAnimations(params);
// required of the sysui visiblity listener is not triggered.
params.hasSystemUiListeners = true;
mContainer = inflater.inflate(R.layout.hvac_panel, null);
mContainer.setLayoutParams(params);
mContainer.setOnSystemUiVisibilityChangeListener(visibility -> {
Log.i(TAG, "layoutHvacUi: visibility:" + visibility);
boolean systemUiVisible = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0;
int y = 0;
if (systemUiVisible) {
// 當(dāng)systemUi可見(jiàn)時(shí),窗口系統(tǒng)坐標(biāo)從系統(tǒng)導(dǎo)航欄上方的0開(kāi)始。因此,如果我們想獲得屏幕底部的實(shí)際高度,我們需要將y值設(shè)置為導(dǎo)航欄高度的負(fù)值。
y = -mNavBarHeight;
}
setYPosition(mDriverTemperatureBar, y);
setYPosition(mPassengerTemperatureBar, y);
setYPosition(mDriverTemperatureBarCollapsed, y);
setYPosition(mPassengerTemperatureBarCollapsed, y);
setYPosition(mContainer, y);
});
// 頂部填充應(yīng)根據(jù)屏幕高度和擴(kuò)展hvac面板的高度進(jìn)行計(jì)算。由填充物定義的空間意味著可以單擊以關(guān)閉hvac面板。
int topPadding = mScreenBottom - mPanelFullExpandedHeight;
mContainer.setPadding(0, topPadding, 0, 0);
mContainer.setFocusable(false);
mContainer.setFocusableInTouchMode(false);
View panel = mContainer.findViewById(R.id.hvac_center_panel);
panel.getLayoutParams().height = mPanelCollapsedHeight;
addViewToWindowManagerAndTrack(mContainer, params);
// 創(chuàng)建溫度計(jì)bar
createTemperatureBars(inflater);
// UI狀態(tài)控制器,用來(lái)控制展開(kāi)/收起時(shí)UI的各種狀態(tài)并執(zhí)行動(dòng)畫(huà)
mHvacPanelController = new HvacPanelController(this /* context */, mContainer,
mWindowManager, mDriverTemperatureBar, mPassengerTemperatureBar,
mDriverTemperatureBarCollapsed, mPassengerTemperatureBarCollapsed
);
// 綁定 HvacController Service
Intent bindIntent = new Intent(this /* context */, HvacController.class);
if (!bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE)) {
Log.e(TAG, "Failed to connect to HvacController.");
}
}
HvacPanelController是空調(diào)的面板控制器,在與HvacController綁定成功后,將HvacController的實(shí)例傳遞給HvacPanelController。
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
mHvacController = ((HvacController.LocalBinder) service).getService();
final Context context = HvacUiService.this;
final Runnable r = () -> {
// hvac控制器從車(chē)輛刷新其值后,綁定所有值。
mHvacPanelController.updateHvacController(mHvacController);
};
if (mHvacController != null) {
mHvacController.requestRefresh(r, new Handler(context.getMainLooper()));
}
}
@Override
public void onServiceDisconnected(ComponentName className) {
mHvacController = null;
mHvacPanelController.updateHvacController(null);
//TODO:b/29126575重新啟動(dòng)后重新連接控制器
}
};
我們接著看HvacPanelController
3.4 HvacPanelController
HvacPanelController 主要作用是初始化其他界面Controller,并從HvacController中獲取數(shù)據(jù),顯示在UI上。
private FanSpeedBarController mFanSpeedBarController;
private FanDirectionButtonsController mFanDirectionButtonsController;
private TemperatureController mTemperatureController;
private TemperatureController mTemperatureControllerCollapsed;
private SeatWarmerController mSeatWarmerController;
public void updateHvacController(HvacController controller) {
mHvacController = controller;
mFanSpeedBarController = new FanSpeedBarController(mFanSpeedBar, mHvacController);
mFanDirectionButtonsController
= new FanDirectionButtonsController(mFanDirectionButtons, mHvacController);
mTemperatureController = new TemperatureController(
mPassengerTemperatureBarExpanded,
mDriverTemperatureBarExpanded,
mPassengerTemperatureBarCollapsed,
mDriverTemperatureBarCollapsed,
mHvacController);
mSeatWarmerController = new SeatWarmerController(mPassengerSeatWarmer,
mDriverSeatWarmer, mHvacController);
// 切換按鈕不需要額外的邏輯來(lái)映射硬件和UI設(shè)置。只需使用ToggleListener來(lái)處理點(diǎn)擊。
mAcButton.setIsOn(mHvacController.getAcState());
mAcButton.setToggleListener(new ToggleButton.ToggleListener() {
@Override
public void onToggled(boolean isOn) {
mHvacController.setAcState(isOn);
}
});
...
setAutoMode(mHvacController.getAutoModeState());
mHvacPowerSwitch.setIsOn(mHvacController.getHvacPowerState());
mHvacPowerSwitch.setToggleListener(isOn -> mHvacController.setHvacPowerState(isOn));
mHvacController.registerCallback(mToggleButtonCallbacks);
mToggleButtonCallbacks.onHvacPowerChange(mHvacController.getHvacPowerState());
}
Hvac界面展開(kāi)和收起的動(dòng)畫(huà)也是在HvacPanelController 中處理的,不過(guò)關(guān)于動(dòng)畫(huà)部分打算以后再開(kāi)個(gè)新坑講一講。
3.5 HvacController
HvacController是HvacApp與CarService之間的信息傳輸控制器,本質(zhì)上也是一個(gè)Service。
public class HvacController extends Service {
private final Binder mBinder = new LocalBinder();
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public class LocalBinder extends Binder {
HvacController getService() {
return HvacController.this;
}
}
...
}
在Hvac中的設(shè)置及獲取數(shù)據(jù)的操作都是通過(guò)HvacController進(jìn)行的,在HvacController啟動(dòng)時(shí)會(huì)獲取一個(gè)Car實(shí)例,并通過(guò)connect方法連接CarService。當(dāng)連接CarService成功后初始化CarHvacManager并通過(guò)CarHvacManager獲取車(chē)輛支持的屬性列表,以及獲取界面所需的基礎(chǔ)數(shù)據(jù)。
@Override
public void onCreate() {
super.onCreate();
if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
// 連接 CarService
mCarApiClient = Car.createCar(this, mCarServiceConnection);
mCarApiClient.connect();
}
}
private final ServiceConnection mCarServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mHvacManagerReady) {
try {
// 連接上CarService后,獲取到其中的HvacManager.
initHvacManager((CarHvacManager) mCarApiClient.getCarManager(Car.HVAC_SERVICE));
// 連接成功后,喚醒正在等待CarHvacManager的線程
mHvacManagerReady.notifyAll();
} catch (CarNotConnectedException e) {
Log.e(TAG, "Car not connected in onServiceConnected");
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
向CarService獲取數(shù)據(jù)需要先得到CarHvacManager的實(shí)例,所以在連接成功后,調(diào)用mHvacManagerReady.notifyAll() 喚醒所有之前等待CarHvacManager實(shí)例的線程
// HvacUiService.java - mServiceConnection
{
final Runnable r = () -> {
// hvac控制器從車(chē)輛刷新其值后,綁定所有值。
mHvacPanelController.updateHvacController(mHvacController);
};
if (mHvacController != null) {
mHvacController.requestRefresh(r, new Handler(context.getMainLooper()));
}
}
// HvacController.java
public void requestRefresh(final Runnable r, final Handler h) {
final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... unused) {
synchronized (mHvacManagerReady) {
while (mHvacManager == null) {
try {
mHvacManagerReady.wait();
} catch (InterruptedException e) {
// We got interrupted so we might be shutting down.
return null;
}
}
}
// 刷新數(shù)據(jù)
fetchTemperature(DRIVER_ZONE_ID);
fetchTemperature(PASSENGER_ZONE_ID);
fetchFanSpeed();
...
return null;
}
@Override
protected void onPostExecute(Void unused) {
// 切換到主線程中執(zhí)行runnable
h.post(r);
}
};
task.execute();
}
private void fetchFanSpeed() {
if (mHvacManager != null) {
int zone = SEAT_ALL; //特定于汽車(chē)的解決方法。
try {
int speed = mHvacManager.getIntProperty(CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT, zone);
mDataStore.setFanSpeed(speed);
} catch (android.car.CarNotConnectedException e) {
Log.e(TAG, "Car not connected in fetchFanSpeed");
}
}
}
上面的代碼就是利用AsyncTask在子線程中等待CarHvacManager的實(shí)例,然后刷新數(shù)據(jù)并存儲(chǔ)在DatStore中。
需要注意一點(diǎn)的是while (mHvacManager == null)
不能替換成if(mHvacManager == null)
,這是因?yàn)镴ava有個(gè)叫“spurious wakeup”的現(xiàn)象,即線程在不該醒過(guò)來(lái)的時(shí)候醒過(guò)來(lái)。
A thread can wake up without being notified, interrupted, or timing out, a so-called spurious wakeup. While this will rarely occur in practice, applications must guard against it by testing for the condition that should have caused the thread to be awakened, and continuing to wait if the condition is not satisfied.
一個(gè)線程有可能會(huì)在未被通知、打斷、或超時(shí)的情況下醒來(lái),這就是所謂的“spurious wakeup”。盡管實(shí)際上這種情況很少發(fā)生,應(yīng)用程序仍然必須對(duì)此有所防范,手段是檢查正常的導(dǎo)致線程被喚醒的條件是否滿足,如果不滿足就繼續(xù)等待。
3.6 Car API
Car
是Android汽車(chē)平臺(tái)最高等級(jí)的API,為外界提供汽車(chē)所有服務(wù)和數(shù)據(jù)訪問(wèn)的接口,提供了一系列與汽車(chē)有關(guān)的API。它不僅僅可以提供HvacManger,像車(chē)輛的速度、檔位狀態(tài)等等所有與汽車(chē)有關(guān)的信息都可以從Car API中獲取。
Hvac中的CarHvacManager實(shí)現(xiàn)了CarManagerBase
接口,并且只要是作為CarXXXManager, 都需要實(shí)現(xiàn)CarManagerBase
接口,如CarCabinManager
,CarSensorManager
等都實(shí)現(xiàn)了該接口。
CarHvacManager的控制操作是通過(guò)CarPropertyManager
來(lái)完成的,CarPropertyManager
統(tǒng)一控制汽車(chē)屬性相關(guān)的操作。CarHvacManager只是控制與Hvac相關(guān)的操作,在汽車(chē)中還有很多屬性控制的Manager,如傳感器,座艙等屬性的控制,他們都是通過(guò)CarPropertyManager
進(jìn)行屬性操作,通過(guò)在操作時(shí)傳入的屬性ID,屬性區(qū)域以及屬性值,在CarPropertyManager
中會(huì)將這些參數(shù)轉(zhuǎn)化為一個(gè)CarPropertyValue
對(duì)象繼續(xù)往CarService
傳遞。
mHvacManager.getIntProperty(CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT, zone);
private final CarPropertyManager mCarPropertyMgr;
public int getIntProperty(int propertyId, int area) {
return this.mCarPropertyMgr.getIntProperty(propertyId, area);
}
CarHvacManager也是通過(guò)注冊(cè)一個(gè)callback來(lái)得到 Car API 的數(shù)據(jù)回調(diào)。
mHvacManager.registerCallback(mHardwareCallback);
private final CarHvacManager.CarHvacEventCallback mHardwareCallback = new CarHvacManager.CarHvacEventCallback() {
@Override
public void onChangeEvent(final CarPropertyValue val) {
int areaId = val.getAreaId();
switch (val.getPropertyId()) {
case CarHvacManager.ID_ZONED_AC_ON:
handleAcStateUpdate(getValue(val));
break;
case CarHvacManager.ID_ZONED_FAN_DIRECTION:
handleFanPositionUpdate(areaId, getValue(val));
break;
case CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT:
handleFanSpeedUpdate(areaId, getValue(val));
break;
case CarHvacManager.ID_ZONED_TEMP_SETPOINT:
handleTempUpdate(val);
break;
case CarHvacManager.ID_WINDOW_DEFROSTER_ON:
handleDefrosterUpdate(areaId, getValue(val));
break;
case CarHvacManager.ID_ZONED_AIR_RECIRCULATION_ON:
handleAirCirculationUpdate(getValue(val));
break;
case CarHvacManager.ID_ZONED_SEAT_TEMP:
handleSeatWarmerUpdate(areaId, getValue(val));
break;
case CarHvacManager.ID_ZONED_AUTOMATIC_MODE_ON:
handleAutoModeUpdate(getValue(val));
break;
case CarHvacManager.ID_ZONED_HVAC_POWER_ON:
handleHvacPowerOn(getValue(val));
break;
default:
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unhandled HVAC event, id: " + val.getPropertyId());
}
}
}
@Override
public void onErrorEvent(final int propertyId, final int zone) {
}
};
Hvac中每個(gè)Property對(duì)應(yīng)的含義如下:
// 全局屬性,只有一個(gè)
ID_MIRROR_DEFROSTER_ON //視鏡除霧
ID_STEERING_WHEEL_HEAT //方向盤(pán)溫度
ID_OUTSIDE_AIR_TEMP //室外溫度
ID_TEMPERATURE_DISPLAY_UNITS //在使用的溫度
// 區(qū)域?qū)傩裕稍诓煌瑓^(qū)域設(shè)置
ID_ZONED_TEMP_SETPOINT //用戶設(shè)置的溫度
ID_ZONED_TEMP_ACTUAL //區(qū)域?qū)嶋H溫度
ID_ZONED_HVAC_POWER_ON //HVAC系統(tǒng)電源開(kāi)關(guān)
ID_ZONED_FAN_SPEED_SETPOINT //風(fēng)扇設(shè)置的速度
ID_ZONED_FAN_SPEED_RPM //風(fēng)扇實(shí)際的速度
ID_ZONED_FAN_DIRECTION_AVAILABLE //風(fēng)扇可設(shè)置的方向
ID_ZONED_FAN_DIRECTION //現(xiàn)在風(fēng)扇設(shè)置的方向
ID_ZONED_SEAT_TEMP //座椅溫度
ID_ZONED_AC_ON //空調(diào)開(kāi)關(guān)
ID_ZONED_AUTOMATIC_MODE_ON //HVAC自動(dòng)模式開(kāi)關(guān)
ID_ZONED_AIR_RECIRCULATION_ON //空氣循環(huán)開(kāi)關(guān)
ID_ZONED_MAX_AC_ON //空調(diào)最大速度開(kāi)關(guān)
ID_ZONED_DUAL_ZONE_ON //雙區(qū)模式開(kāi)關(guān)
ID_ZONED_MAX_DEFROST_ON //最大除霧開(kāi)關(guān)
ID_ZONED_HVAC_AUTO_RECIRC_ON //自動(dòng)循環(huán)模式開(kāi)關(guān)
ID_WINDOW_DEFROSTER_ON //除霧模式開(kāi)關(guān)
使用Car API時(shí)務(wù)必需要注意,注冊(cè)的callback
是有可能會(huì)非常頻繁的產(chǎn)生回調(diào)的,應(yīng)用層需要先將數(shù)據(jù)存儲(chǔ)在DataStore
中進(jìn)行過(guò)濾,才能更新到UI上。而且也不要實(shí)時(shí)的打印日志,否則可能會(huì)導(dǎo)致日志緩沖區(qū)EOF,也會(huì)嚴(yán)重干擾其它進(jìn)程的日志輸出。
3.7 DataStore
DataStore 用于存儲(chǔ)HvacController
從 Car API 中獲取的屬性值。
用戶操作IVI界面和使用硬按鍵,都會(huì)更新Hvac的相關(guān)屬性。這兩種不同的更新方式都是從不同的線程更新到當(dāng)前狀態(tài)。此外,在某些情況下,Hvac系統(tǒng)可能會(huì)發(fā)送虛假的更新,因此這個(gè)類(lèi)將所有內(nèi)容更新管理合并,從而確保在用戶看來(lái)應(yīng)用程序的界面是正常的
@GuardedBy("mFanSpeed")
private Integer mFanSpeed = 0;
private static final long COALESCE_TIME_MS = 0L;
public int getFanSpeed() {
synchronized (mFanSpeed) {
return mFanSpeed;
}
}
// 僅用于主動(dòng) 獲取、設(shè)定 數(shù)據(jù)時(shí)更新speed數(shù)據(jù)。
public void setFanSpeed(int speed) {
synchronized (mFanSpeed) {
mFanSpeed = speed;
mLastFanSpeedSet = SystemClock.uptimeMillis();
}
}
// 從callback中得到數(shù)據(jù)時(shí),因?yàn)閿?shù)據(jù)可能會(huì)刷新的很頻繁,所以需要先判斷時(shí)間戳,確定數(shù)據(jù)是否真的需要更新
public boolean shouldPropagateFanSpeedUpdate(int zone, int speed) {
// TODO:我們暫時(shí)忽略風(fēng)扇速度區(qū)域,因?yàn)槲覀儧](méi)有多區(qū)域車(chē)。
synchronized (mFanSpeed) {
if (SystemClock.uptimeMillis() - mLastFanSpeedSet < COALESCE_TIME_MS) {
return false;
}
mFanSpeed = speed;
}
return true;
}
在HvacController
中我們從callback
得到數(shù)據(jù)刷新時(shí),先通過(guò)DataStore
判斷以下是否需要更新數(shù)據(jù),如果確實(shí)需要更新,再將更新后的數(shù)據(jù)回調(diào)給其他的UI控制器。
// HvacController.java
private final CarHvacManager.CarHvacEventCallback mHardwareCallback = new CarHvacManager.CarHvacEventCallback() {
@Override
public void onChangeEvent(final CarPropertyValue val) {
int areaId = val.getAreaId();
switch (val.getPropertyId()) {
case CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT:
// 處理來(lái)自callback的數(shù)據(jù)
handleFanSpeedUpdate(areaId, getValue(val));
break;
// ... 省略
default:
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unhandled HVAC event, id: " + val.getPropertyId());
}
}
}
};
private void handleFanSpeedUpdate(int zone, int speed) {
// 判斷是否需要更新本地的數(shù)據(jù)
boolean shouldPropagate = mDataStore.shouldPropagateFanSpeedUpdate(zone, speed);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Fan Speed Update, zone: " + zone + " speed: " + speed +
" should propagate: " + shouldPropagate);
}
if (shouldPropagate) {
// 將更新后的數(shù)據(jù)回調(diào)給各個(gè)UI控制器
synchronized (mCallbacks) {
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).onFanSpeedChange(speed);
}
}
}
}
public void setFanSpeed(final int fanSpeed) {
// 更新當(dāng)前的數(shù)據(jù)
mDataStore.setFanSpeed(fanSpeed);
final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
int newFanSpeed;
protected Void doInBackground(Void... unused) {
if (mHvacManager != null) {
int zone = SEAT_ALL; // Car specific workaround.
try {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Setting fanspeed to: " + fanSpeed);
}
mHvacManager.setIntProperty(
CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT, zone, fanSpeed);
newFanSpeed = mHvacManager.getIntProperty(
CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT, zone);
} catch (android.car.CarNotConnectedException e) {
Log.e(TAG, "Car not connected in setFanSpeed");
}
}
return null;
}
};
task.execute();
}
4. 總結(jié)
最后我們以一張從Car API的callback
中的數(shù)據(jù)更新界面的偽時(shí)序圖來(lái)把Hvac的幾個(gè)核心組件串起來(lái)
以上就是車(chē)載空調(diào)部分的講解,實(shí)際開(kāi)發(fā)中,空調(diào)模塊功能性需求一般不會(huì)出現(xiàn)什么太大的技術(shù)性困難,空調(diào)模塊的技術(shù)性難度幾乎都體現(xiàn)在復(fù)雜的動(dòng)畫(huà)和交互上,有關(guān)車(chē)載應(yīng)用的復(fù)雜動(dòng)畫(huà)技術(shù),我們以后在來(lái)細(xì)講解決方案。