架构概述
备注:以下分析基于Android 9.0
SystemUI 意即 系统界面,按照原生的职责划分,在车机上 SystemUI 更多是提供通用功能(与手机)的状态的展示,如状态栏对时间电量等的展示,导航栏对各功能页面提供跳转入口,仅此而已,至于其他的如 HVAC 等硬件相关的控制,都是交给对应的模块去做的.
HVAC是 Heating, Ventilation and Air Conditioning 的英文缩写,就是供热通风与空气调节, 而车机上对 HVAC 相关功能的控制是放到一个独立 apk – CarHvacApp 里面去做的,通过在 SystemUI 的导航栏中提供功能入口来实现跳转
如下,原生导航栏大部分的按钮均是其他功能模块的跳转入口,其中 HVAC 按钮的跳转页面是一个 HVAC 管理面板,该面板可设置诸如座椅温度,前后挡风玻璃除雾器的开关,主副驾的温度等
但很多时候产商定制需要在导航栏直接提供对部分硬件相关功能的操作入口,如直接在导航栏操作座椅加热功能.而谷歌在开发的时候也已经对这种潜在的扩展提供了支持,即通过将硬件相关模块的操作封装成通用lib库,允许需要的应用直接进行导包引用
而这些 lib 库封装了对底层 HAL模块(硬件抽象层模块) 的调用细节, 而底层的HAL模块则包含了对相应硬件的具体实现细节,下面看看其架构图
一般的硬件开发流程是这样的:
- 1 .在内核层开发硬件驱动程序
- 2 .在硬件抽象层(HAL)开发对应的硬件抽象层模块
- 2.1 说明: 硬件抽象层以模块的形式管理各个硬件访问接口,每一个硬件模块都对应有一个动态链接库文件,每一个hal模块在内核都对应有一个驱动程序,hal模块就是通过这些驱动程序来访问硬件设备的,它们是通过读写设备文件来进行通信
- 3 .在framework层开发硬件访问服务
- 4 .硬件上层访问者通过访问硬件访问服务进而间接操作硬件
有兴趣了解 HAL 开发的可以看看罗升阳的《Android系统源代码情景分析》这本书, 同时 Android 8.0 之后谷歌提供了 HIDL 语言来方便 HAL模块的开发,这块可以看看
Android HIDL HAL 接口定义语言详解
对应上面的硬件开发流程,我们简述下该架构图,其中:
-
- 对于驱动层,暂未找到对应的驱动程序文件,先略过
-
hardware/interfaces/automotive/vehicle/2.0/IVehicle.hal
对应于硬件抽象层模块,对应实现在hardware/interfaces/automotive/vehicle/2.0/default/impl/vhal_v2_0
下,其核心实现就是通过调用驱动接口函数open / read / write
来实现设备文件的打开 / 读 / 写 从而实现与硬件设备的交互的。
-
car-lib
对应于硬件访问服务,其内部封装了对 HAL层的调用,例如对于HVAC硬件接口,car-lib
对外提供了类CarHvacManager
,而上层访问者均可通过该类对象直接查询/设置HVAC相关属性,下文展开介绍
-
- CarHvacApp 与 SystemUI 等应用对应于 硬件上层访问者,均通过访问硬件访问服务来实现与硬件的交互
了解了该架构之后,我们就可以知道,CarHvacApp / SystemUI 等应用其实就是对应于架构中的硬件上层访问者,我们的开发工作就是找到硬件对应的硬件访问服务,在用户操作我们的界面是将操作反馈给硬件访问服务即可,底层实现可以不关心
下面分别介绍:
- 1.CarHvacApp 中与底层 Hvac 硬件的交互原理
- 2.SystemUI 中与底层 Hvac 硬件的交互原理以及扩展实现
CarHvacApp 与硬件的交互
下面我们先来看看 CarHvacApp 这个应用是如何初始化 HVAC 面板 以及 在用户操作 HVAC 面板时,如何将用户操作结果反馈给底层硬件的
前面我们说到,谷歌将对 HVAC 相关功能的控制放到一个独立 apk – CarHvacApp 中,代码位于packags/apps/Car/Havc
,主体构成很简单,看下 manifest 文件
[packages/apps/Car/Hvac/AndroidManifest.xml]
<application android:label="@string/hvac_label"
android:icon="@drawable/ic_launcher_hvac"
android:persistent="true">
<service android:name=".HvacController"
android:singleUser="true"
android:exported="false" />
<service android:name=".HvacUiService"
android:singleUser="true"
android:exported="false"/>
<receiver android:name=".BootCompleteReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
复制代码
仅由两个服务以及一个开机广播组成,其中开机广播负责在开机的时候启动服务HvacUiService
HvacUiService
[packages/apps/Car/Hvac/src/com/android/car/hvac/HvacUiService.java]
public class HvacUiService extends Service {}
复制代码
该服务主要做了以下几件事:
- 1.在
layoutHvacUi()
方法中完成HVAC面板的构建,该页面由五个窗口组成,窗口类型皆为WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY
,会盖在sysui上面,分别是- 1.1 界面中间部分座椅加热,前后挡风除雾开关等元素构成的窗口,对应布局
hvac_panel
- 1.2 左边的主驾温度控制条窗口,对应布局
hvac_temperature_bar_overlay
,下同 - 1.3 左边的主驾温度控制条收起(Collapsed)窗口,代码中通过配置
config_showCollapsedBars
字段来控制该窗口显示与否,默认为false
,该窗口其实就是在控制面板收起时提供一个非完全收起的温度控制条以便调起控制面板 - 1.4 右边的副驾温度控制条窗口
- 1.5 右边的副驾温度控制条收起(Collapsed)窗口,同上
- 1.1 界面中间部分座椅加热,前后挡风除雾开关等元素构成的窗口,对应布局
- 2.注册监听广播
android.car.intent.action.TOGGLE_HVAC_CONTROLS
,后续sysui会通过该广播来通知该服务做展开/收起HVAC面板
@Override
public void onCreate() {
......
IntentFilter filter = new IntentFilter();
filter.addAction(CAR_INTENT_ACTION_TOGGLE_HVAC_CONTROLS);
// Register receiver such that any user with climate control permission can call it.
registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter,
Car.PERMISSION_CONTROL_CAR_CLIMATE, null);
}
复制代码
- 3.创建
HvacPanelController
对象,将以上构建的五个窗口对应的View传进去
private void layoutHvacUi() {
......
mHvacPanelController = new HvacPanelController(this /* context */, mContainer,
mWindowManager, mDriverTemperatureBar, mPassengerTemperatureBar,
mDriverTemperatureBarCollapsed, mPassengerTemperatureBarCollapsed
);
Intent bindIntent = new Intent(this /* context */, HvacController.class);
if (!bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE)) {
Log.e(TAG, "Failed to connect to HvacController.");
}
}
复制代码
- 4.绑定服务
HvacController
,并在绑定成功时将服务对象HvacController
给到HvacPanelController
,这里可以看到,相对于sysui,HvacUiService
是服务端,而相对于HvacUiService
,HvacController
才是服务端
HvacPanelController
[packages/apps/Car/Hvac/src/com/android/car/hvac/controllers/HvacPanelController.java]
public class HvacPanelController {}
复制代码
该控制器是一个HVAC UI 布局元素状态机,负责整个 HVAC 面板UI元素的初始化,响应各布局元素的事件(用户点击 & 系统内部硬件属性变化)以及执行状态切换(如出现消失动画)
上文介绍HvacUiService
时我们看到,HvacUiService
在创建了各个窗口之后,将各窗口的View对象传了进来,而HvacPanelController
则进一步对这些View对象做初始化,下面看看这个类的主要职责:
- 1.在构造方法中取出各个子元素(findViewById),并设置其Icon资源
- 2.前面说到,当
HvacUiService
成功绑定服务HvacController
后,会将HvacController
服务对象传进HvacPanelController
,此时HvacPanelController
会构造各类子元素View对应的控制器,去HvacController
中获取各子元素的初始状态并设置给各子元素,以及设置各子元素的点击事件
public void updateHvacController(HvacController controller) {
// 构造各类子元素View对应的控制器
mFanSpeedBarController = new FanSpeedBarController(mFanSpeedBar, mHvacController);
mSeatWarmerController = new SeatWarmerController(mPassengerSeatWarmer,
mDriverSeatWarmer, mHvacController);
......
// 初始化按钮状态,并设置点击事件
mAcButton.setIsOn(mHvacController.getAcState());
mAcButton.setToggleListener(new ToggleButton.ToggleListener() {
@Override
public void onToggled(boolean isOn) {
mHvacController.setAcState(isOn);
}
});
// 注册回调
mHvacController.registerCallback(mToggleButtonCallbacks);
}
复制代码
- 3.负责View的 show/hide 以及 动画的执行(transition),前面介绍
HvacUiService
时第2点我们说过,HvacUiService
会去监听一个广播来展开/收起HVAC面板,相关逻辑如下,其中transitionState
就是最终展开/收起面板的地方
[packages/apps/Car/Hvac/src/com/android/car/hvac/HvacUiService.java]
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if(action.equals(CAR_INTENT_ACTION_TOGGLE_HVAC_CONTROLS)){
mHvacPanelController.toggleHvacUi();
}
}
};
[packages/apps/Car/Hvac/src/com/android/car/hvac/controllers/HvacPanelController.java]
public void toggleHvacUi() {
if(mCurrentState != STATE_COLLAPSED) {
mCollapseHvac.onClick(null);
} else {
mExpandHvac.onClick(null);
}
}
public View.OnClickListener mExpandHvac = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mInAnimation) {
return;
}
if (mCurrentState != STATE_FULL_EXPANDED) {
transitionState(mCurrentState, STATE_FULL_EXPANDED);
}
}
};
复制代码
car-lib库
前面介绍架构时我们说过,car-lib
对应于硬件访问服务,其内部封装了对 HAL层的调用,例如对于HVAC硬件接口,car-lib
对外提供了类CarHvacManager,而上层访问者均可通过该类对象直接查询/设置HVAC相关属性
介绍HvacController
之前我们先来看看car-lib
库下的两个关键类
[packages/services/Car/car-lib/src/android/car/Car.java]
// 源码对该类的说明:针对Android车载开发的顶级car API
public final class Car {
// 1.外界获取硬件访问服务的入口
public static Car createCar(Context context, ServiceConnection serviceConnectionListener,@Nullable Handler handler) {
return new Car(context, serviceConnectionListener, handler);
}
// 2.缓存起来的 mServiceConnectionListenerClient 后面会用到
private Car(Context context, ServiceConnection serviceConnectionListener,@Nullable Handler handler) {
mServiceConnectionListenerClient = serviceConnectionListener;
}
// 3.调用该函数时会触发 startCarService 去创建 CarService服务
public void connect() throws IllegalStateException {
startCarService();
}
private void startCarService() {
// 4.intent 指向了CarService服务,该服务真正的实现在ICarImpl中,这里不展开
boolean bound = mContext.bindServiceAsUser(intent, mServiceConnectionListener,Context.BIND_AUTO_CREATE, UserHandle.CURRENT_OR_SELF);
}
private final ServiceConnection mServiceConnectionListener = new ServiceConnection () {
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (Car.this) {
mService = ICar.Stub.asInterface(service);
mConnectionState = STATE_CONNECTED;
}
// 5.连接成功时通知 mServiceConnectionListenerClient, 即前面createCar时传进来的服务连接监听器,
// 进而上层访问者可以开始获取诸如 CarHvacManager 等对象
mServiceConnectionListenerClient.onServiceConnected(name, service);
}
};
}
复制代码
上面第5点中连接CarService
成功时会通知调用者,意在告诉调用者你可以获取诸如 CarHvacManager 等对象了,因为诸如CarHvacManager
等对象的获取需要依赖到 CarService.
同时Car
类中不仅可以获取CarHvacManager
对象,还可以获取Android Car开发相关的各种管理类,如CarAudioManager
,CarSensorManager
等,详见其createCarManager
函数
整个Car
主要的功能就是
- 1.作为外界获取硬件访问服务的入口,上层调用者可通过
createCar
来获取一个Car
对象 - 2.当调用者调用其
connect
函数时去连接CarService
服务,这个服务是连接车载底层模块的桥梁,并在连接成功时通知调用者(通过ServiceConnection) - 3.提供入口供开发者获取所需的Manager,比如HVAC模块中所需的
CarHvacManager
,这是调用者后续操作相关硬件属性的入口
下面看看CarHvacManager
的功能
CarHvacManager使用了一个数据结构,这是一个可跨进程传输的数据结构,封装了车辆相关的属性
[packages/services/Car/car-lib/src/android/car/hardware/CarPropertyValue.java]
public class CarPropertyValue<T> implements Parcelable {
private final int mPropertyId;
private final int mAreaId;
private final int mStatus;
private final long mTimestamp;
private final T mValue;
}
[packages/services/Car/car-lib/src/android/car/hardware/hvac/CarHvacManager.java]
// 源码对该类的说明:用于控制HVAC系统的API
public final class CarHvacManager implements CarManagerBase {
// 步骤1 : 定义了一系列常量,用来对应车辆各个区域(zone)的属性
// 空调温度
public static final int ID_ZONED_TEMP_SETPOINT = 0x15600503;
// 座椅温度,负值制冷,正值制热,
public static final int ID_ZONED_SEAT_TEMP = 0x1540050b;
// 镜子除雾器,布尔值
public static final int ID_MIRROR_DEFROSTER_ON = 0x1440050c;
// 窗口除雾器,布尔值
public static final int ID_WINDOW_DEFROSTER_ON = 0x13200504;
// 步骤2 : 回调 可通过注册该回调来监听各 CarPropertyValue 的变化
public interface CarHvacEventCallback {
void onChangeEvent(CarPropertyValue value);
}
public synchronized void registerCallback(CarHvacEventCallback callback) {}
public synchronized void unregisterCallback(CarHvacEventCallback callback) {}
// 步骤3 : 提供对各 HVAC 属性的 状态查询 与 状态设置接口
public boolean getBooleanProperty(@PropertyId int propertyId, int area)
throws CarNotConnectedException {
return mCarPropertyMgr.getBooleanProperty(propertyId, area);
}
public float getFloatProperty(@PropertyId int propertyId, int area)
throws CarNotConnectedException {
return mCarPropertyMgr.getFloatProperty(propertyId, area);
}
}
复制代码
首先封装了一个可序列化的类CarPropertyValue
, 其对应一个车辆属性,一个车辆属性主要由两部分构成:[propertyId, area]
propertyId
定义了车辆属性,如空调温度,座椅温度等,具体值定义在CarHvacManager
类中,见步骤1area
定义了属性对应的区域,因为同一个属性会有多个位置需要控制,如同是座椅温度,有主驾座椅温度,副驾座椅温度,所以就通过该值来对同一属性的不同位置做区分.
而CarHvacManager
主要做了以下三件事
- 步骤1 : 定义车辆属性
- 步骤2 : 提供
CarHvacEventCallback
回调器及其注册接口,其会去监听底层硬件的状态变化,以便在底层硬件属性变化时告诉上层调用者,可想而知,这个就是我们开发时需要关注的一个接口 - 步骤3 : 提供查询/设置硬件属性的上层接口,同样这也是我们开发时需要关注的一个接口
这两个类均位于packages/services/Car/car-lib
下,我们看下其mk文件
[packages/services/Car/car-lib/Android.mk]
...
LOCAL_MODULE := android.car
...
复制代码
即该模块会被编译成共享库android.car
,可被其他应用引用,例如CarHvacApp中就引用了该库
[packages/apps/Car/Hvac/Android.mk]
LOCAL_JAVA_LIBRARIES += android.car
复制代码
HvacController
回到前面对 CarHvacApp 的分析,前面介绍HvacPanelController
第2点中的updateHvacController()
方法时我们看到,HvacController
对象会被传进各类子元素View对应的控制器中,同时也是获取/设置各类子元素View状态的地方,很明显,这是个总控制器,同时维护了各开关的状态
来看看这个类的主要职责是什么
- 1.服务启动时利用
car-lib
库去创建一个Car
对象并调用其connect()
函数,并在连接成功时通过该对象获取一个CarHvacManager
对象
[packages/apps/Car/Hvac/src/com/android/car/hvac/HvacController.java]
public class HvacController extends Service {
@Override
public void onCreate() {
super.onCreate();
if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
mCarApiClient = Car.createCar(this, mCarConnectionCallback);
mCarApiClient.connect();
}
}
private final CarConnectionCallback mCarConnectionCallback = new CarConnectionCallback() {
@Override
public void onConnected(Car car) {
synchronized (mHvacManagerReady) {
try {
initHvacManager((CarHvacManager) mCarApiClient.getCarManager(android.car.Car.HVAC_SERVICE));
mHvacManagerReady.notifyAll();
......
}
private void initHvacManager(CarHvacManager carHvacManager) {
mHvacManager = carHvacManager;
List<CarPropertyConfig> properties = null;
try {
properties = mHvacManager.getPropertyList();
mPolicy = new HvacPolicy(HvacController.this, properties);
mHvacManager.registerCallback(mHardwareCallback);
}
}
}
复制代码
-
- 提供接口供查询 / 设置 各HVAC开关的状态值,这里便是利用了
CarHvacManager
去获取状态值并保存到mDataStore
中的,以及利用CarHvacManager
将属性变化告知底层硬件的
- 提供接口供查询 / 设置 各HVAC开关的状态值,这里便是利用了
public boolean getAirCirculationState() {
return mDataStore.getAirCirculationState();
}
public void setFanDirection(final int direction) {
mDataStore.setAirflow(SEAT_ALL, direction);
setFanDirection(SEAT_ALL, direction);
}
复制代码
- 3.向
CarHvacManager
注册一个监听器监听硬件状态变化,并通知各子元素去做UI刷新
private void initHvacManager(CarHvacManager carHvacManager) {
......
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;
......
}
}
};
复制代码
简单总结下就是,CarHvacApp 利用 CarHvacManager 完成了对 HVAC 硬件的状态查询,状态设置以及状态监听
SystemUI 与硬件的交互
其实看懂上文CarHvacApp 与硬件的交互之后,这一节的内容也就就十分简单了,跟 CarHvacApp一样,无非就是利用car-lib
库来获取Car
对象,连接服务,获取CarHvacManager
对象,查询硬件状态,用户操作UI时设置硬件状态,监听硬件的内部变化
下面我们围绕”如何在SystemUI导航栏中实现 直接操作座椅加热功能”这一目的,来看看实现方案:
- 1.导入
car-lib
库(原生已导入)
[frameworks/base/packages/SystemUI/Android.mk]
LOCAL_JAVA_LIBRARIES := android.car
复制代码
- 2.获取
Car
对象,连接服务,获取CarHvacManager
对象,同时向CarHvacManager
注册监听硬件的内部变化
[frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java]
public void start() {
Dependency.get(HvacController.class).connectToCarService();
}
[frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/car/hvac/HvacController.java]
public void connectToCarService() {
// 获取 Car 对象
mCar = Car.createCar(mContext, mServiceConnection, mHandler);
if (mCar != null) {
// 连接服务
mCar.connect();
}
}
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
try {
// 获取 CarHvacManager 对象
mHvacManager = (CarHvacManager) mCar.getCarManager(Car.HVAC_SERVICE);
// 注册监听硬件的内部变化
mHvacManager.registerCallback(mHardwareCallback);
}
}
};
复制代码
代码很简单,看注释即可,其中HvacController
是原生提供的用于处理与 Hvac 相关事物的一个类,可将对 Hvac 相关功能的扩展放到这个类中
原生在SystemUI中做的与Hvac硬件相关的操作很少,只有一个查询空调温度的逻辑,流程很简单,就是利用上面连接服务成功时获取到的CarHvacManager
对象去查询温度状态,并利用向CarHvacManager
注册的回调监听温度变化
private void initComponent(TemperatureView view) {
int id = view.getPropertyId();
int zone = view.getAreaId();
// 调 CarHvacManager 的 getFloatProperty 去查询指定位置的温度
view.setTemp(mHvacManager.getFloatProperty(id, zone));
}
private final CarHvacEventCallback mHardwareCallback = new CarHvacEventCallback() {
@Override
public void onChangeEvent(final CarPropertyValue val) {
try {
// 温度属性上报变化时,刷新UI
int areaId = val.getAreaId();
int propertyId = val.getPropertyId();
List<TemperatureView> temperatureViews = mTempComponents.get(new HvacKey(propertyId, areaId));
if (temperatureViews != null && !temperatureViews.isEmpty()) {
float value = (float) val.getValue();
for (TemperatureView tempView : temperatureViews) {
tempView.setTemp(value);
}
}
}
复制代码
而我们想要实现”在SystemUI导航栏中实现 直接操作座椅加热功能”,只需要做简单的扩展:
- 1.UI构建
- 2.在
HvacController
中新增座椅加热状态 查询 / 设置 接口,如
private static final int DRIVER_ZONE_ID = VehicleAreaSeat.SEAT_ROW_1_LEFT |
VehicleAreaSeat.SEAT_ROW_2_LEFT | VehicleAreaSeat.SEAT_ROW_2_CENTER;
public float getDriverZoneTemperature() {
if (mHvacManager != null) {
try {
return mHvacManager.getFloatProperty(CarHvacManager.ID_ZONED_TEMP_SETPOINT, DRIVER_ZONE_ID);
}
}
return -1;
}
public void setDriverSeatWarmerLevel(int level) {
setSeatWarmerLevel(DRIVER_ZONE_ID, level);
}
public void setSeatWarmerLevel(final int zone, final int level) {
final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
protected Void doInBackground(Void... unused) {
if (mHvacManager != null) {
try {
mHvacManager.setIntProperty(CarHvacManager.ID_ZONED_SEAT_TEMP, zone, level);
}
}
return null;
}
};
task.execute();
}
复制代码
- 3.在向
CarHvacManager
注册的监听器回调中 新增对座椅加热状态的处理
private final CarHvacEventCallback mHardwareCallback = new CarHvacEventCallback() {
@Override
public void onChangeEvent(final CarPropertyValue val) {
try {
int areaId = val.getAreaId();
int propertyId = val.getPropertyId();
switch (propertyId) {
case CarHvacManager.ID_ZONED_SEAT_TEMP:
handleSeatWarmerUpdate(areaId, getValue(val));
break;
}
}
复制代码
至此,我们就实现了该功能了,而扩展其他功能时如法炮制即可