一文了解 Android 车机如何处理中控的旋钮输入?

前言

上篇文章《从实体按键看 Android 车载的自定义事件机制》带大家了解了 Android 车机支持自定义输入的机制 CustomInputService。事实上,除了支持自定义事件,对于中控上常见的音量控制、焦点控制的旋钮事件,Android 车机也是支持的。

那本篇文章带大家看下 Android 车机处理旋钮事件的内在原理:

  1. 定义
  2. 监听和订阅
  3. 接收
  4. 处理
  5. 模拟

1. 定义

和自定义输入所支持的事件一致,支持旋钮输入的事件类型也在如下文件 types.hal 中定义。

h 复制代码
// 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 注册监听。

java 复制代码
// 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 的订阅。

java 复制代码
// 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,其将找出处理方 HalServiceBaseInputHalService 并继续分发。

java 复制代码
// 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() 继续。

java 复制代码
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() 将执行如下步骤:

  1. 检查必要数据是否齐全,即起码包括旋钮硬件类型、旋钮计数、目标屏幕这 3 位
  2. 按照 index 取出这三位数据
  3. 检查旋钮计数是否为 0,因为无法判断 0 是顺时针还是逆时针
  4. 检查目标屏幕是否为中控屏幕 MAIN 、仪表屏幕 INSTRUMENT_CLUSTER 中的一个
  5. 检查旋钮计数的时间差数值位数是否匹配(比如:旋转了 3 格的话,那么时间差必须要占 2 位)
  6. 根据旋钮硬件类型转化为 CarInputManager 中定义的事件类型
    • 焦点控制的话转换为 INPUT_TYPE_ROTARY_NAVIGATION
    • 音量控制的话转换为 INPUT_TYPE_ROTARY_VOLUME
  7. 提取持续计数的时间差到 timestamps 数组中
  8. 根据旋钮计数方向,转换到的事件类型以及时间差数组封装 RotaryEvent 对象交由 InputListener 继续分发
java 复制代码
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 进行处理
java 复制代码
// 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 类型的旋钮事件。

java 复制代码
// 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() 继续处理。

java 复制代码
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() 继续。

java 复制代码
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 操作。

java 复制代码
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,具体的执行如下逻辑:

  1. 焦点控制按钮的话,依据方向 mapping 顺时针为焦点前进的 KEYCODE_NAVIGATE_NEXT ,逆时针为焦点后退的 KEYCODE_NAVIGATE_PREVIOUS
  2. 音量控制按钮的话,mapping 为音量 +/- Key Code,顺时针为 KEYCODE_VOLUME_UP ,逆时针则是 KEYCODE_VOLUME_DOWN
  3. 按照计数次数批量调用 createKeyEvent() 创建 KeyEvent 对象,并添加到待处理 keyEvents 列表中。
java 复制代码
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 列表,逐个处理。

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);
            }
        }
    }
    ...
}

CarInputService 的 onKeyEvent() 直接处理的 Code 只有激活语音助手的 KEYCODE_VOICE_ASSIST 和拨打电话的 KEYCODE_CALL。其他的 Key Code 执行一般处理:

  1. 如果目标屏幕是 INSTRUMENT_CLUSTER 即仪表屏幕的话,调用 handleInstrumentClusterKey()InstrumentClusterKeyListener 执行仪表上的事件,貌似是 Cluster app 完成,具体不再展开
  2. 检查是否有使用 InputEventCapture 监听 NAVIGATE_ 焦点控制、VOLUME_ 音量控制 KeyEvent 的 Service 存在,有的话回调 onKeyEvent() Callback
  3. 如果没有 Capture 处理的好,告知 KeyEventListener 进行兜底处理
java 复制代码
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。

java 复制代码
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 等一系列处理。

java 复制代码
// 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 命令模拟旋钮事件来验证代码链路。

格式:

bash 复制代码
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,表示只有一次旋转

下面将介绍几个命令示例,帮助大家更好地理解该命令的使用。

bash 复制代码
adb shell cmd car_service inject-rotary

没有指定任何参数,全部都是默认的操作,表示针对中控屏幕 发送焦点控制 的旋钮事件,方向为逆时针、焦点后退 1 格

bash 复制代码
adb shell cmd car_service inject-rotary -d 1 -i 11 -c true

表示针对仪表屏幕 发送音量控制 的旋钮事件,方向为顺时针、调低 1 格

bash 复制代码
adb shell cmd car_service inject-rotary -c true -dt 100 50

表示针对中控屏幕发送焦点控制的旋钮事件,方向为顺时针 、3 次计数、焦点前进 3 格

结语

与自定义输入相比,旋钮事件的处理流程有细微差异,主要体现在 CarInputService 会针对音量、焦点两种的旋钮控制,存在特定的处理逻辑。最后,结合一张图回顾下整体流程:

  1. 支持音量控制焦点控制 的两种旋钮硬件产生 HW_ROTARY_INPUT Propery 变化

  2. 由和 HAL 层交互的 VehicleHal 订阅到 Propery 变化,将事件提取为 HalPropValue 类型

  3. 并发送给车机输入的中间服务 InputHalService 接收和进一步地封装为 RotaryEvent 类型

  4. 分发到处理事件输入的专用服务 CarInputService

    a. 如果有 Capture 音量/焦点的 Rotary 事件的交由其专门处理 :Car App 的 RotaryService,其将决定通过 InputManager 注入 SCROLL 滚动还是通过 Accessibility 触发焦点 Focus 操作;

    b. 如果没有,则执行标准处理:

    • 首先按照 Rotary 类型和旋钮方向、计数封装为 Android 标准 KeyEvent 列表
    • 如果目标屏幕为仪表的话,列表交由 Cluster App 处理
    • 反之检查是否有 Capture 该 KeyEvent 的 Service 需要处理
    • 最后交由 InputManager 逐个注入该 KeyEvent,继而由系统的 InputManagerService 进行调度

推荐阅读

参考文档

相关推荐
水瓶丫头站住15 分钟前
安卓APP如何适配不同的手机分辨率
android·智能手机
xvch1 小时前
Kotlin 2.1.0 入门教程(五)
android·kotlin
xvch5 小时前
Kotlin 2.1.0 入门教程(七)
android·kotlin
望风的懒蜗牛5 小时前
编译Android平台使用的FFmpeg库
android
浩宇软件开发5 小时前
Android开发,待办事项提醒App的设计与实现(个人中心页)
android·android studio·android开发
ac-er88886 小时前
Yii框架中的多语言支持:如何实现国际化
android·开发语言·php
苏金标7 小时前
The maximum compatible Gradle JVM version is 17.
android
zhangphil7 小时前
Android BitmapShader简洁实现马赛克,Kotlin(一)
android·kotlin
iofomo11 小时前
Android平台从上到下,无需ROOT/解锁/刷机,应用级拦截框架的最后一环,SVC系统调用拦截。
android
我叫特踏实12 小时前
SensorManager开发参考
android·sensormanager