前语
上篇文章《从实体按键看 Android 车载的自界说事情机制》带咱们了解了 Android 车机支撑自界说输入的机制 CustomInputService
。事实上,除了支撑自界说事情,对于中控上常见的音量操控、焦点操控的旋钮事情,Android 车机也是支撑的。
那本篇文章带咱们看下 Android 车机处理旋钮事情的内涵原理:
- 界说
- 监听和订阅
- 接纳
- 处理
- 模仿
1. 界说
和自界说输入所支撑的事情共同,支撑旋钮输入的事情类型也在如下文件 types.hal 中界说。
// hardware/interfaces/automotive/vehicle/2.0/types.hal
/**
* Property to feed H/W rotary events to android
* ...
*/
HW_ROTARY_INPUT = (
0x0A20
| VehiclePropertyGroup:SYSTEM
| VehiclePropertyType:INT32_VEC
| VehicleArea:GLOBAL),
enum RotaryInputType : int32_t {
ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION = 0,
ROTARY_INPUT_TYPE_AUDIO_VOLUME = 1,
};
HW_ROTARY_INPUT
代表该事情在底层的 Property
界说,供 VehicleHal
对其建议监听。
该事情涵盖了一些旋钮所有必要的数据:
- 第 0 位代表哪种旋钮硬件,由
RotaryInputType
枚举细分,包含操控焦点的旋钮 TYPE_SYSTEM_NAVIGATION 和操控音量的旋钮 TYPE_AUDIO_VOLUME - 第 1 位代表旋转计数,正数代表顺时针计数 clockwise,负数代表逆时针计数 counterclockwise
- 第 2 位代表旋钮事情的方针屏幕
VehicleDisplay
,默许是 MAIN,即 center console,中控屏幕 - 第 3 位及今后代表持续计数事情之间的时间差,单位为 ns
2. 监听和订阅
上层处理事情输入的 CarInputService
在初始化的时分,会向调度车机输入的中间层 InputHalService
注册监听。
// packages/services/Car/service/src/com/android/car/CarInputService.java
public class CarInputService ... {
...
@Override
public void init() {
if (!mInputHalService.isKeyInputSupported()) {
return;
}
mInputHalService.setInputListener(this);
...
}
...
}
InputHalService 判别支撑旋钮输入的话,向和 HAL 层交互的 VehicleHal
注册 HW_ROTARY_INPUT
Property 的订阅。
// packages/services/Car/service/src/com/android/car/hal/InputHalService.java
public class InputHalService extends HalServiceBase {
...
public void setInputListener(InputListener listener) {
...
boolean rotaryInputSupported;
synchronized (mLock) {
mListener = listener;
...
rotaryInputSupported = mRotaryInputSupported;
}
...
if (rotaryInputSupported) {
mHal.subscribeProperty(this, HW_ROTARY_INPUT);
}
...
}
public boolean isRotaryInputSupported() {
synchronized (mLock) {
return mRotaryInputSupported;
}
}
...
}
3. 接纳
当旋钮事情发生,将经过 HAL 层抵达上述订阅该 Property 的 VehicleHal,其将找出处理方 HalServiceBase
即 InputHalService
并持续分发。
// packages/services/Car/service/src/com/android/car/hal/VehicleHal.java
public class VehicleHal implements HalClientCallback {
...
@Override
public void onPropertyEvent(ArrayList<HalPropValue> propValues) {
synchronized (mLock) {
for (int i = 0; i < propValues.size(); i++) {
HalPropValue v = propValues.get(i);
int propId = v.getPropId();
HalServiceBase service = mPropertyHandlers.get(propId);
if (service == null) {
continue;
}
service.getDispatchList().add(v);
mServicesToDispatch.add(service);
VehiclePropertyEventInfo info = mEventLog.get(propId);
if (info == null) {
info = new VehiclePropertyEventInfo(v);
mEventLog.put(propId, info);
} else {
info.addNewEvent(v);
}
}
}
for (HalServiceBase s : mServicesToDispatch) {
s.onHalEvents(s.getDispatchList());
s.getDispatchList().clear();
}
mServicesToDispatch.clear();
}
...
}
InputHalService 首先保证上层的 InputListener
的确存在,此后再查看该 HalProperty 是何种类型。HW_ROTARY_INPUT 旋钮事情的话调用 dispatchRotaryInput() 持续。
public class InputHalService extends HalServiceBase {
...
@Override
public void onHalEvents(List<HalPropValue> values) {
InputListener listener;
synchronized (mLock) {
listener = mListener;
}
if (listener == null) {
return;
}
for (int i = 0; i < values.size(); i++) {
HalPropValue value = values.get(i);
switch (value.getPropId()) {
case HW_ROTARY_INPUT:
dispatchRotaryInput(listener, value);
break;
...
}
}
}
...
}
dispatchRotaryInput()
将履行如下步骤:
- 查看必要数据是否完全,即起码包含旋钮硬件类型、旋钮计数、方针屏幕这 3 位
- 依照 index 取出这三位数据
- 查看旋钮计数是否为 0,因为无法判别 0 是顺时针仍是逆时针
- 查看方针屏幕是否为中控屏幕 MAIN、外表屏幕 INSTRUMENT_CLUSTER 中的一个
- 查看旋钮计数的时间差数值位数是否匹配(比如:旋转了 3 格的话,那么时间差有必要要占 2 位)
- 根据旋钮硬件类型转化为
CarInputManager
中界说的事情类型- 焦点操控的话转换为 INPUT_TYPE_ROTARY_NAVIGATION
- 音量操控的话转换为 INPUT_TYPE_ROTARY_VOLUME
- 提取持续计数的时间差到 timestamps 数组中
- 根据旋钮计数方向,转换到的事情类型以及时间差数组封装
RotaryEvent
对象交由 InputListener 持续分发
public class InputHalService extends HalServiceBase {
...
private void dispatchRotaryInput(InputListener listener, HalPropValue value) {
int timeValuesIndex = 3; // remaining values are time deltas in nanoseconds
if (value.getInt32ValuesSize() < timeValuesIndex) {
return;
}
int rotaryInputType = value.getInt32Value(0);
int detentCount = value.getInt32Value(1);
int vehicleDisplay = value.getInt32Value(2);
long timestamp = value.getTimestamp(); // for first detent, uptime nanoseconds
boolean clockwise = detentCount > 0;
detentCount = Math.abs(detentCount);
if (detentCount == 0) { // at least there should be one event
return;
}
if (vehicleDisplay != VehicleDisplay.MAIN
&& vehicleDisplay != VehicleDisplay.INSTRUMENT_CLUSTER) {
return;
}
if (value.getInt32ValuesSize() != (timeValuesIndex + detentCount - 1)) {
return;
}
int carInputManagerType;
switch (rotaryInputType) {
case ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION:
carInputManagerType = CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION;
break;
case ROTARY_INPUT_TYPE_AUDIO_VOLUME:
carInputManagerType = CarInputManager.INPUT_TYPE_ROTARY_VOLUME;
break;
default: ...
}
long[] timestamps = new long[detentCount];
long uptimeToElapsedTimeDelta = CarServiceUtils.getUptimeToElapsedTimeDeltaInMillis();
...
RotaryEvent event = new RotaryEvent(carInputManagerType, clockwise, timestamps);
listener.onRotaryEvent(event, convertDisplayType(vehicleDisplay));
}
...
}
4. 处理
监听章节里说到 InputListener 为 CarInputService,所以将传递到 CarInputService 的 onRotaryEvent() 进行处理。
onRotaryEvent() 先查看是否有运用 InputEventCapture 监听旋钮事情的 Service 存在:
- 假如有监听,交由 Capture 该事情的 Service 专门处理
- 假如没有,转换为 Android 规范 KeyEvent 进行处理
// packages/services/Car/service/src/com/android/car/CarInputService.java
public class CarInputService ... {
...
@Override
public void onRotaryEvent(RotaryEvent event, @DisplayTypeEnum int targetDisplay) {
if (!mCaptureController.onRotaryEvent(targetDisplay, event)) {
List<KeyEvent> keyEvents = rotaryEventToKeyEvents(event);
for (KeyEvent keyEvent : keyEvents) {
onKeyEvent(keyEvent, targetDisplay);
}
}
}
...
}
专门处理
Car App 提供了一个专门操控焦点的 RotaryService
,它在绑定时经过 CarInputManager 的 requestInputEventCapture()
请求监听了 INPUT_TYPE_ROTARY_NAVIGATION 类型的旋钮事情。
// packages/apps/Car/RotaryController/src/com/android/car/rotary/RotaryService.java
public class RotaryService ... {
/** Input types to capture. */
private final int[] mInputTypes = new int[]{
CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION,
...};
...
@Override
public void onServiceConnected() {
super.onServiceConnected();
mCar = Car.createCar(this, null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
(car, ready) -> {
mCar = car;
if (ready) {
mCarInputManager =
(CarInputManager) mCar.getCarManager(Car.CAR_INPUT_SERVICE);
...
mCarInputManager.requestInputEventCapture(
CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
mInputTypes,
CarInputManager.CAPTURE_REQ_FLAGS_ALLOW_DELAYED_GRANT,
/* callback= */ this);
}
});
...
}
...
}
自然的,RotaryService 的 onRotaryEvent() 会得到调用,首先将查看方针屏幕是否符合预期,有必要是 MAIN 即中控屏幕。经过的话,调用 handleRotaryEvent()
持续处理。
public class RotaryService ... {
...
@Override
public void onRotaryEvents(int targetDisplayType, @NonNull List<RotaryEvent> events) {
if (!isValidDisplayType(targetDisplayType)) {
return;
}
for (RotaryEvent rotaryEvent : events) {
handleRotaryEvent(rotaryEvent);
}
}
private static boolean isValidDisplayType(int displayType) {
if (displayType == CarOccupantZoneManager.DISPLAY_TYPE_MAIN) {
return true;
}
return false;
}
...
}
handleRotaryEvent() 将查看 RotaryEvent 中的硬件 type,保证的确来自于焦点操控旋钮 INPUT_TYPE_ROTARY_NAVIGATION,经过的话调用 handleRotateEvent()
持续。
public class RotaryService ... {
...
private void handleRotaryEvent(RotaryEvent rotaryEvent) {
if (rotaryEvent.getInputType() != CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION) {
return;
}
boolean clockwise = rotaryEvent.isClockwise();
int count = rotaryEvent.getNumberOfClicks();
long eventTime = rotaryEvent.getUptimeMillisForClick(count - 1);
handleRotateEvent(clockwise, count, eventTime);
}
...
}
handleRotateEvent() 主要是根据屏幕的设置和当时 focus 的 Node 情况来决议是调用 performScrollAction()
履行屏幕翻滚,仍是寻找到方针 Node 调用 performFocusAction()
来履行焦点的移动。
其本质上是经过 InputManager
向体系注入 SCROLL 触摸事情,或者经过 Accessibility
向上面的或下面的待 focus 的 AccessibilityNode
发送 FOCUS Action 操作。
public class RotaryService ... {
...
private void handleRotateEvent(boolean clockwise, int count, long eventTime) {
int rotationCount = getRotateAcceleration(count, eventTime);
if (mInProjectionMode) {
injectMotionEvent(DEFAULT_DISPLAY, clockwise ? rotationCount : -rotationCount);
return;
}
if (initFocus() || mFocusedNode == null) {
return;
}
if (mInDirectManipulationMode) {
if (DirectManipulationHelper.supportRotateDirectly(mFocusedNode)) {
performScrollAction(mFocusedNode, clockwise);
} else {
AccessibilityWindowInfo window = mFocusedNode.getWindow();
if (window == null) {
L.w("Failed to get window of " + mFocusedNode);
return;
}
int displayId = window.getDisplayId();
window.recycle();
injectMotionEvent(displayId, clockwise ? rotationCount : -rotationCount);
}
return;
}
int remainingRotationCount = rotationCount;
int direction = clockwise ? View.FOCUS_FORWARD : View.FOCUS_BACKWARD;
Navigator.FindRotateTargetResult result =
mNavigator.findRotateTarget(mFocusedNode, direction, rotationCount);
if (result != null) {
if (performFocusAction(result.node)) {
remainingRotationCount -= result.advancedCount;
}
Utils.recycleNode(result.node);
} else {
L.w("Failed to find rotate target from " + mFocusedNode);
}
if (remainingRotationCount > 0 && isInFocusedWindow(mFocusedNode)) {
AccessibilityNodeInfo scrollableContainer =
mNavigator.findScrollableContainer(mFocusedNode);
if (scrollableContainer != null) {
injectScrollEvent(scrollableContainer, clockwise, remainingRotationCount);
scrollableContainer.recycle();
}
}
}
...
}
规范处理
和导航旋钮事情不同,体系没有 Capture 音量旋钮事情 INPUT_TYPE_ROTARY_VOLUME 的 Service,那么它得履行规范处理。
首先,得将 RotatryEvent 转换为规范的按键编号 Key Code,详细的履行如下逻辑:
- 焦点操控按钮的话,根据方向 mapping 顺时针为焦点行进的 KEYCODE_NAVIGATE_NEXT,逆时针为焦点撤退的 KEYCODE_NAVIGATE_PREVIOUS
- 音量操控按钮的话,mapping 为音量 +/- Key Code,顺时针为 KEYCODE_VOLUME_UP,逆时针则是 KEYCODE_VOLUME_DOWN
- 依照计数次数批量调用
createKeyEvent()
创立 KeyEvent 对象,并添加到待处理 keyEvents 列表中。
public class CarInputService ... {
...
private static List<KeyEvent> rotaryEventToKeyEvents(RotaryEvent event) {
int numClicks = event.getNumberOfClicks();
int numEvents = numClicks * 2; // up / down per each click
boolean clockwise = event.isClockwise();
int keyCode;
switch (event.getInputType()) {
case CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION:
keyCode = clockwise
? KeyEvent.KEYCODE_NAVIGATE_NEXT
: KeyEvent.KEYCODE_NAVIGATE_PREVIOUS;
break;
case CarInputManager.INPUT_TYPE_ROTARY_VOLUME:
keyCode = clockwise
? KeyEvent.KEYCODE_VOLUME_UP
: KeyEvent.KEYCODE_VOLUME_DOWN;
break;
...
}
ArrayList<KeyEvent> keyEvents = new ArrayList<>(numEvents);
for (int i = 0; i < numClicks; i++) {
long uptime = event.getUptimeMillisForClick(i);
KeyEvent downEvent = createKeyEvent(/* down= */ true, uptime, uptime, keyCode);
KeyEvent upEvent = createKeyEvent(/* down= */ false, uptime, uptime, keyCode);
keyEvents.add(downEvent);
keyEvents.add(upEvent);
}
return keyEvents;
}
...
}
接着,遍历准备好的 keyEvents 列表,逐一处理。
public class CarInputService ... {
...
@Override
public void onRotaryEvent(RotaryEvent event, @DisplayTypeEnum int targetDisplay) {
if (!mCaptureController.onRotaryEvent(targetDisplay, event)) {
List<KeyEvent> keyEvents = rotaryEventToKeyEvents(event);
// 遍历列表,逐一处理
for (KeyEvent keyEvent : keyEvents) {
onKeyEvent(keyEvent, targetDisplay);
}
}
}
...
}
CarInputService 的 onKeyEvent() 直接处理的 Code 只要激活语音帮手的 KEYCODE_VOICE_ASSIST 和拨打电话的 KEYCODE_CALL。其他的 Key Code 履行一般处理:
- 假如方针屏幕是 INSTRUMENT_CLUSTER 即外表屏幕的话,调用
handleInstrumentClusterKey()
让InstrumentClusterKeyListener
履行外表上的事情,貌似是Cluster
app 完结,详细不再展开 - 查看是否有运用 InputEventCapture 监听 NAVIGATE_ 焦点操控、VOLUME_ 音量操控 KeyEvent 的 Service 存在,有的话回调
onKeyEvent()
Callback - 假如没有 Capture 处理的好,奉告
KeyEventListener
进行兜底处理
public class CarInputService ... {
...
@Override
public void onKeyEvent(KeyEvent event, @DisplayTypeEnum int targetDisplayType) {
// Special case key code that have special "long press" handling for automotive
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_VOICE_ASSIST:
handleVoiceAssistKey(event);
return;
case KeyEvent.KEYCODE_CALL:
handleCallKey(event);
return;
default:
break;
}
assignDisplayId(event, targetDisplayType);
// Allow specifically targeted keys to be routed to the cluster
if (targetDisplayType == CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER
&& handleInstrumentClusterKey(event)) {
return;
}
if (mCaptureController.onKeyEvent(targetDisplayType, event)) {
return;
}
mMainDisplayHandler.onKeyEvent(event);
}
...
}
KeyEventListener 在 CarInputService 初始化的时分指定,详细的就是经过 InputManagerHelper
注入 KeyEvent。
public class CarInputService ... {
...
private final KeyEventListener mMainDisplayHandler;
public CarInputService( ... ) {
this(context, inputHalService, userService, occupantZoneService, bluetoothService,
new Handler(CarServiceUtils.getCommonHandlerThread().getLooper()),
context.getSystemService(TelecomManager.class),
event -> InputManagerHelper.injectInputEvent(
context.getSystemService(InputManager.class), event),
() -> Calls.getLastOutgoingCall(context),
() -> getViewLongPressDelay(context),
() -> context.getResources().getBoolean(R.bool.config_callButtonEndsOngoingCall),
new InputCaptureClientController(context));
}
...
}
InputManagerHelper 没啥特别的,直接调用 InputManager
的规范方法 injectInputEvent()
完结注入,后续由 InputManagerService
开端 Dispatch、Transport 等一系列处理。
// packages/services/Car/car-builtin-lib/src/android/car/builtin/input/InputManagerHelper.java
public class InputManagerHelper {
...
public static boolean injectInputEvent(@NonNull InputManager inputManager,
@NonNull android.view.InputEvent event) {
return inputManager.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
}
5. 模仿
当旋钮按键环境没有到位的时分,咱们可以运用 adb 指令模仿旋钮事情来验证代码链路。
格式:
adb shell cmd car_service inject-rotary [-d display] [-i input_type] [-c clockwise] [-dt delta_times_ms]
- display,方针屏幕:0 代表中控屏幕,1 代表外表屏幕,默许是 0
- input_type,按钮类型: 10 代表焦点操控,11 代表音量操控,默许是 10
- clockwise,旋钮方向: true 代表顺时针方向,false 代表逆时针,默许是 false
- delta_times_ms,持续旋转计数的时间间隔:多次旋转事情和当时时间的间隔列表,按降序排列,默许是 0,表明只要一次旋转
下面将介绍几个指令示例,帮助咱们更好地理解该指令的运用。
adb shell cmd car_service inject-rotary
没有指定任何参数,全部都是默许的操作,表明针对中控屏幕发送焦点操控的旋钮事情,方向为逆时针、焦点撤退 1 格。
adb shell cmd car_service inject-rotary -d 1 -i 11 -c true
表明针对外表屏幕发送音量操控的旋钮事情,方向为顺时针、调低 1 格。
adb shell cmd car_service inject-rotary -c true -dt 100 50
表明针对中控屏幕发送焦点操控的旋钮事情,方向为顺时针、3 次计数、焦点行进 3 格。
结语
与自界说输入比较,旋钮事情的处理流程有细微差异,主要体现在 CarInputService
会针对音量、焦点两种的旋钮操控,存在特定的处理逻辑。最终,结合一张图回忆下整体流程:
-
支撑音量操控和焦点操控的两种旋钮硬件发生
HW_ROTARY_INPUT
Propery 变化 -
由和 HAL 层交互的
VehicleHal
订阅到 Propery 变化,将事情提取为HalPropValue
类型 -
并发送给车机输入的中间服务
InputHalService
接纳和进一步地封装为RotaryEvent
类型 -
分发处处理事情输入的专用服务
CarInputService
:a. 假如有 Capture 音量/焦点的 Rotary 事情的交由其专门处理:Car App 的
RotaryService
,其将决议经过InputManager
注入 SCROLL 翻滚仍是经过Accessibility
触发焦点 Focus 操作;b. 假如没有,则履行规范处理:
- 首先依照 Rotary 类型和旋钮方向、计数封装为 Android 规范
KeyEvent
列表 - 假如方针屏幕为外表的话,列表交由
Cluster
App 处理 - 反之查看是否有 Capture 该 KeyEvent 的 Service 需要处理
- 最终交由 InputManager 逐一注入该 KeyEvent,继而由体系的
InputManagerService
进行调度
- 首先依照 Rotary 类型和旋钮方向、计数封装为 Android 规范
引荐阅览
- 从实体按键看 Android 车载的自界说事情机制
- 怎么打造车载语音交互:Google Voice Interaction 给你答案
- Android 车机初体验:Auto,Automotive 傻傻分不清楚?
参阅文档
- developer.android.google.cn/training/ca…
- source.android.google.cn/docs/device…