1. HVAC 功能介紹
HVAC 全稱:供暖通風與空氣調(diào)節(jié)(Heating Ventilation and Air Conditioning)翼悴。用戶可以通過他來控制整個汽車的空調(diào)系統(tǒng),是汽車中非常重要的一個功能幔妨。
汽車的空調(diào)HMI雖然并不復雜鹦赎,但是大多都是用符號來表示功能谍椅,對于還沒有實際用過汽車空調(diào)系統(tǒng)的開發(fā)者來說,理解空調(diào)的各個符號表示的含義也是非常有必要古话。
下面就以Android 12中的HVAC來介紹空調(diào)系統(tǒng)中包含的最基礎(chǔ)的功能雏吭。
1.1 雙區(qū)溫度調(diào)節(jié)
空調(diào)的溫度調(diào)節(jié)功能,默認是華氏度陪踩,可以在系統(tǒng)設(shè)置修改溫度單位杖们。可調(diào)節(jié)范圍是61 - 82華氏度膊毁,對應16 - 28 攝氏度胀莹。
左側(cè)按鈕用來調(diào)節(jié)主駕,右側(cè)按鈕用來調(diào)節(jié)副駕婚温。在以往都是只有高配車型才有雙區(qū)空調(diào)描焰,現(xiàn)在的車上雙區(qū)空調(diào)幾乎已經(jīng)是標配了。
1.2 空調(diào)開關(guān)
開啟關(guān)閉空調(diào)的開關(guān)
1.3 內(nèi)/外循環(huán)
內(nèi)循環(huán)是汽車空氣調(diào)節(jié)系統(tǒng)的一種狀態(tài)栅螟。這種狀態(tài)下荆秦,車內(nèi)外的換氣通道關(guān)閉,風機關(guān)閉時車內(nèi)氣流不循環(huán)力图,風機開啟時步绸,吸入的氣流也僅來自車內(nèi),形成車輛內(nèi)部的氣流循環(huán)吃媒。
外循環(huán)則相反瓤介,風機開啟時,吸入的氣流也僅來自車外赘那,可以更新車內(nèi)的空氣質(zhì)量刑桑,代價是會更耗電。
1.4 風量調(diào)節(jié)
用于增大或減小空調(diào)的風量募舟。
1.5 風向調(diào)節(jié)
從左到右分別是吹臉祠斧、吹臉+吹腳、吹腳拱礁、吹腳+吹擋風玻璃
1.6 A/C開關(guān)
A/C按鍵琢锋,它就是制冷開關(guān),按下A/C按鍵呢灶,也就啟動了壓縮機吴超,通俗地說就是開冷氣。
1.7 主副駕座椅加熱
左邊的按鈕用于調(diào)節(jié)主駕座椅加熱鸯乃,右邊的按鈕用于調(diào)節(jié)副駕座椅加熱
1.8 除霜
左邊的按鈕是開啟/關(guān)閉 前擋風玻璃加熱鲸阻,開啟后用來除去前擋風玻璃上的霧氣。右邊的按鈕是開啟/關(guān)閉后擋風玻璃加熱,開啟后用來除去后擋風玻璃上的霧氣赘娄。
1.9 自動模式
自動空調(diào)其實就是省略了風速、風向等調(diào)節(jié)功能宏蛉,自動空調(diào)是全自動調(diào)節(jié)遣臼,只需要選擇風向和設(shè)定溫度。AUTO按鍵按下后拾并,就會根據(jù)車內(nèi)傳感器來控制出風的溫度揍堰,冬天熱風,夏天冷風嗅义。會保持車內(nèi)有較適宜的溫度屏歹,如果溫度過高或過低,空調(diào)也會自動改變出風口的溫度及風速之碗,調(diào)整車內(nèi)溫度蝙眶。
以上就是車載空調(diào)系統(tǒng)中最基礎(chǔ)的功能了,實際開發(fā)中我們還會遇到如座椅通風褪那、座椅按摩幽纷、智能新風、負離子等等一些近幾年才出現(xiàn)的空調(diào)新功能博敬,在應用開發(fā)上無非就是多幾個界面或按鈕友浸。
2. HVAC 源碼結(jié)構(gòu)
本文中的源碼基于Android 12下HVAC APP,源碼請見:https://github.com/linux-link/CarHvac
原生的Hvac App中不存在Activity偏窝、Fragment等傳統(tǒng)意義上用來顯示HMI的組件收恢,取而代之是使用Service來顯示一個Window。主要原因在于Hvac的界面層級比一般的HMI的層級要高祭往,呼出Hvac時需要部分或全部覆蓋其他的應用上(當然IVI中還是有應用比Hvac的層級要高的)伦意,這時候使用Activity就顯不合適了。
需要注意的是链沼,Havc在Android 12中雖然有一個獨立的app默赂,但是上圖展示空調(diào)并沒有使用這個獨立的app,它的HMI和邏輯實現(xiàn)都是直接寫在SystemUI中的括勺。
我們可以通過adb發(fā)送一個廣播來調(diào)出獨立的Hvac應用缆八。
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)聽開機廣播 -->
<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)的開機廣播后疾捍,會將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 用來托管Hvac UI的Service。從名字上也能看出乱豆,整個HvacUiService都是圍繞著如何將Hvac準確的繪制出來奖恰,基本不含其他的邏輯。
@Override
public void onCreate() {
...
// 由于不存在從服務(wù)內(nèi)部獲取系統(tǒng)ui可見性的方法,因此我們將全屏放置一些東西瑟啃,并檢查其最終測量結(jié)果论泛,作為獲取該信息的黑客手段。
// 一旦我們有了初始狀態(tài)蛹屿,我們就可以安全地從那時開始注冊更改事件屁奏。
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),因此不再需要這個空視圖错负。
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);
// 注冊接收器坟瓢,以便任何具有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);
// 自定義廣播犹撒,用于展開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.注冊事件廣播。這個事件實際并沒有發(fā)送源识颊,因為SystemUI中額外寫了一個Hvac诚镰,不過正是這個廣播讓我們可以把這個單獨的Hvac調(diào)出。
2.繪制UI祥款。HvacUIService在被拉起后并沒有立即開始UI的繪制怕享,而是在屏幕上臨時放置一個用于測量窗口的 windowSizeTest ,當windowSizeTestView開始測量后镰踏,通過比對View的高度和屏幕的高度函筋,即可判斷出systemUI是否已經(jīng)顯示,這時就可以開始著手繪制真正的Hvac的UI了奠伪,并且可以更安全的操作UI跌帐。
接下來就是繪制真正的Hvac界面:
/**
* 在確定最小偏移量后調(diào)用。
* 這將生成HVAC UI所需的所有組件的布局绊率。
* 啟動時谨敛,折疊視圖所需的所有窗口都可見,而展開視圖的窗口已創(chuàng)建并調(diào)整大小滤否,但不可見脸狸。
*/
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) {
// 當systemUi可見時,窗口系統(tǒng)坐標從系統(tǒng)導航欄上方的0開始藐俺。因此炊甲,如果我們想獲得屏幕底部的實際高度,我們需要將y值設(shè)置為導航欄高度的負值欲芹。
y = -mNavBarHeight;
}
setYPosition(mDriverTemperatureBar, y);
setYPosition(mPassengerTemperatureBar, y);
setYPosition(mDriverTemperatureBarCollapsed, y);
setYPosition(mPassengerTemperatureBarCollapsed, y);
setYPosition(mContainer, y);
});
// 頂部填充應根據(jù)屏幕高度和擴展hvac面板的高度進行計算卿啡。由填充物定義的空間意味著可以單擊以關(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)建溫度計bar
createTemperatureBars(inflater);
// UI狀態(tài)控制器菱父,用來控制展開/收起時UI的各種狀態(tài)并執(zhí)行動畫
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的實例傳遞給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控制器從車輛刷新其值后官辽,綁定所有值蛹磺。
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重新啟動后重新連接控制器
}
};
我們接著看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);
// 切換按鈕不需要額外的邏輯來映射硬件和UI設(shè)置。只需使用ToggleListener來處理點擊乓梨。
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界面展開和收起的動畫也是在HvacPanelController 中處理的,不過關(guān)于動畫部分打算以后再開個新坑講一講清酥。
3.5 HvacController
HvacController是HvacApp與CarService之間的信息傳輸控制器扶镀,本質(zhì)上也是一個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ù)的操作都是通過HvacController進行的焰轻,在HvacController啟動時會獲取一個Car實例臭觉,并通過connect方法連接CarService。當連接CarService成功后初始化CarHvacManager并通過CarHvacManager獲取車輛支持的屬性列表辱志,以及獲取界面所需的基礎(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的實例什乙,所以在連接成功后,調(diào)用mHvacManagerReady.notifyAll() 喚醒所有之前等待CarHvacManager實例的線程
// HvacUiService.java - mServiceConnection
{
final Runnable r = () -> {
// hvac控制器從車輛刷新其值后已球,綁定所有值臣镣。
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; //特定于汽車的解決方法。
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ù)據(jù)并存儲在DatStore中忆某。
需要注意一點的是while (mHvacManager == null)
不能替換成if(mHvacManager == null)
,這是因為Java有個叫“spurious wakeup”的現(xiàn)象阔蛉,即線程在不該醒過來的時候醒過來弃舒。
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.
一個線程有可能會在未被通知、打斷状原、或超時的情況下醒來聋呢,這就是所謂的“spurious wakeup”。盡管實際上這種情況很少發(fā)生颠区,應用程序仍然必須對此有所防范坝冕,手段是檢查正常的導致線程被喚醒的條件是否滿足,如果不滿足就繼續(xù)等待瓦呼。
3.6 Car API
Car
是Android汽車平臺最高等級的API喂窟,為外界提供汽車所有服務(wù)和數(shù)據(jù)訪問的接口测暗,提供了一系列與汽車有關(guān)的API。它不僅僅可以提供HvacManger磨澡,像車輛的速度碗啄、檔位狀態(tài)等等所有與汽車有關(guān)的信息都可以從Car API中獲取。
Hvac中的CarHvacManager實現(xiàn)了CarManagerBase
接口稳摄,并且只要是作為CarXXXManager, 都需要實現(xiàn)CarManagerBase
接口稚字,如CarCabinManager
,CarSensorManager
等都實現(xiàn)了該接口厦酬。
CarHvacManager的控制操作是通過CarPropertyManager
來完成的胆描,CarPropertyManager
統(tǒng)一控制汽車屬性相關(guān)的操作。CarHvacManager只是控制與Hvac相關(guān)的操作仗阅,在汽車中還有很多屬性控制的Manager昌讲,如傳感器,座艙等屬性的控制减噪,他們都是通過CarPropertyManager
進行屬性操作短绸,通過在操作時傳入的屬性ID,屬性區(qū)域以及屬性值筹裕,在CarPropertyManager
中會將這些參數(shù)轉(zhuǎn)化為一個CarPropertyValue
對象繼續(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也是通過注冊一個callback來得到 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中每個Property對應的含義如下:
// 全局屬性朝卒,只有一個
ID_MIRROR_DEFROSTER_ON //視鏡除霧
ID_STEERING_WHEEL_HEAT //方向盤溫度
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)電源開關(guān)
ID_ZONED_FAN_SPEED_SETPOINT //風扇設(shè)置的速度
ID_ZONED_FAN_SPEED_RPM //風扇實際的速度
ID_ZONED_FAN_DIRECTION_AVAILABLE //風扇可設(shè)置的方向
ID_ZONED_FAN_DIRECTION //現(xiàn)在風扇設(shè)置的方向
ID_ZONED_SEAT_TEMP //座椅溫度
ID_ZONED_AC_ON //空調(diào)開關(guān)
ID_ZONED_AUTOMATIC_MODE_ON //HVAC自動模式開關(guān)
ID_ZONED_AIR_RECIRCULATION_ON //空氣循環(huán)開關(guān)
ID_ZONED_MAX_AC_ON //空調(diào)最大速度開關(guān)
ID_ZONED_DUAL_ZONE_ON //雙區(qū)模式開關(guān)
ID_ZONED_MAX_DEFROST_ON //最大除霧開關(guān)
ID_ZONED_HVAC_AUTO_RECIRC_ON //自動循環(huán)模式開關(guān)
ID_WINDOW_DEFROSTER_ON //除霧模式開關(guān)
使用Car API時務(wù)必需要注意,注冊的callback
是有可能會非常頻繁的產(chǎn)生回調(diào)的抗斤,應用層需要先將數(shù)據(jù)存儲在DataStore
中進行過濾瑟曲,才能更新到UI上。而且也不要實時的打印日志豪治,否則可能會導致日志緩沖區(qū)EOF洞拨,也會嚴重干擾其它進程的日志輸出。
3.7 DataStore
DataStore 用于存儲HvacController
從 Car API 中獲取的屬性值负拟。
用戶操作IVI界面和使用硬按鍵烦衣,都會更新Hvac的相關(guān)屬性。這兩種不同的更新方式都是從不同的線程更新到當前狀態(tài)掩浙。此外花吟,在某些情況下,Hvac系統(tǒng)可能會發(fā)送虛假的更新厨姚,因此這個類將所有內(nèi)容更新管理合并衅澈,從而確保在用戶看來應用程序的界面是正常的
@GuardedBy("mFanSpeed")
private Integer mFanSpeed = 0;
private static final long COALESCE_TIME_MS = 0L;
public int getFanSpeed() {
synchronized (mFanSpeed) {
return mFanSpeed;
}
}
// 僅用于主動 獲取、設(shè)定 數(shù)據(jù)時更新speed數(shù)據(jù)谬墙。
public void setFanSpeed(int speed) {
synchronized (mFanSpeed) {
mFanSpeed = speed;
mLastFanSpeedSet = SystemClock.uptimeMillis();
}
}
// 從callback中得到數(shù)據(jù)時今布,因為數(shù)據(jù)可能會刷新的很頻繁经备,所以需要先判斷時間戳,確定數(shù)據(jù)是否真的需要更新
public boolean shouldPropagateFanSpeedUpdate(int zone, int speed) {
// TODO:我們暫時忽略風扇速度區(qū)域部默,因為我們沒有多區(qū)域車侵蒙。
synchronized (mFanSpeed) {
if (SystemClock.uptimeMillis() - mLastFanSpeedSet < COALESCE_TIME_MS) {
return false;
}
mFanSpeed = speed;
}
return true;
}
在HvacController
中我們從callback
得到數(shù)據(jù)刷新時,先通過DataStore
判斷以下是否需要更新數(shù)據(jù)傅蹂,如果確實需要更新纷闺,再將更新后的數(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:
// 處理來自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)給各個UI控制器
synchronized (mCallbacks) {
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).onFanSpeedChange(speed);
}
}
}
}
public void setFanSpeed(final int fanSpeed) {
// 更新當前的數(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ù)更新界面的偽時序圖來把Hvac的幾個核心組件串起來
以上就是車載空調(diào)部分的講解份蝴,實際開發(fā)中犁功,空調(diào)模塊功能性需求一般不會出現(xiàn)什么太大的技術(shù)性困難,空調(diào)模塊的技術(shù)性難度幾乎都體現(xiàn)在復雜的動畫和交互上婚夫,有關(guān)車載應用的復雜動畫技術(shù)浸卦,我們以后在來細講解決方案。