西丽建设网站,工信部网站信息查询,网站首页上的动画是咋做的,推广资讯前言
上篇文章《从实体按键看 Android 车载的自定义事件机制》带大家了解了 Android 车机支持自定义输入的机制 CustomInputService。事实上#xff0c;除了支持自定义事件#xff0c;对于中控上常见的音量控制、焦点控制的旋钮事件#xff0c;Android 车机也是支持的。
那…
前言
上篇文章《从实体按键看 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 ... {...Overridepublic 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 {...Overridepublic void onPropertyEvent(ArrayListHalPropValue 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 {...Overridepublic void onHalEvents(ListHalPropValue 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 nanosecondsif (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 nanosecondsboolean clockwise detentCount 0;detentCount Math.abs(detentCount);if (detentCount 0) { // at least there should be one eventreturn;}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 ... {...Overridepublic void onRotaryEvent(RotaryEvent event, DisplayTypeEnum int targetDisplay) {if (!mCaptureController.onRotaryEvent(targetDisplay, event)) {ListKeyEvent 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,...};...Overridepublic 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 ... {...Overridepublic void onRotaryEvents(int targetDisplayType, NonNull ListRotaryEvent 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 ListKeyEvent rotaryEventToKeyEvents(RotaryEvent event) {int numClicks event.getNumberOfClicks();int numEvents numClicks * 2; // up / down per each clickboolean 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;...}ArrayListKeyEvent 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 ... {...Overridepublic void onRotaryEvent(RotaryEvent event, DisplayTypeEnum int targetDisplay) {if (!mCaptureController.onRotaryEvent(targetDisplay, event)) {ListKeyEvent 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 ... {...Overridepublic void onKeyEvent(KeyEvent event, DisplayTypeEnum int targetDisplayType) {// Special case key code that have special long press handling for automotiveswitch (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 clusterif (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 代表仪表屏幕默认是 0input_type按钮类型: 10 代表焦点控制11 代表音量控制默认是 10clockwise旋钮方向: true 代表顺时针方向false 代表逆时针默认是 falsedelta_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 进行调度
推荐阅读
从实体按键看 Android 车载的自定义事件机制如何打造车载语音交互Google Voice Interaction 给你答案Android 车机初体验AutoAutomotive 傻傻分不清楚
参考文档
https://developer.android.google.cn/training/carshttps://source.android.google.cn/docs/devices/automotive/hmi/rotary_controller/app_developers