您好,欢迎来到钮旅网。
搜索
您的当前位置:首页AAOS里特有的车载事件定制CustomInputService

AAOS里特有的车载事件定制CustomInputService

来源:钮旅网
自定义按键

AAOS默认支持的自定义事件Code位于文件hardware/interfaces/automotive/vehicle/2.0/types.hal 中,App可以利用这些预设的事件Code进行监听和自定义处理逻辑。
当然,Car OEM 厂商可以使用任意有符号的 32 位数值来扩展支持自定义输入HW_CUSTOM_INPUT的 CustomInputType 枚举范围,以支持更多的按键 Code,确保处理的范围符合实际的车辆按键需求。

enum CustomInputType : int32_t {
    CUSTOM_EVENT_F1 = 1001,
    CUSTOM_EVENT_F2 = 1002,
    CUSTOM_EVENT_F3 = 1003,
    CUSTOM_EVENT_F4 = 1004,
    CUSTOM_EVENT_F5 = 1005,
    CUSTOM_EVENT_F6 = 1006,
    CUSTOM_EVENT_F7 = 1007,
    CUSTOM_EVENT_F8 = 1008,
    CUSTOM_EVENT_F9 = 1009,
    CUSTOM_EVENT_F10 = 1010,
};

我们利用上述 Code 来自定义一个打开高频 app 的专用控件,比如:接电话、挂电话、音量、语音、微信按钮、地图按钮、音乐控制等等。
官方的 DEMO 源码如下:
https://cs.android.com/android/platform/superproject/+/master:packages/services/Car/tests/SampleCustomInputService/?hl=zh-cn

DEMO 讲解

实战的具体步骤来说,首先得声明特定权限,才能监听 Car 的自定义输入:
android.car.permission.CAR_MONITOR_INPUT
当然,如果涉及到向 Android 系统注入回标准 KeyEvent,还需要申明对应的注入权限:
android.permission.INJECT_EVENTS
总体的 Manifest 定义如下:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.android.car.custominput.sample">

    <uses-permission android:name="android.permission.INJECT_EVENTS" />
    <uses-permission android:name="android.car.permission.CAR_MONITOR_INPUT"/>
    ...

    <application>
        <service android:name=".SampleCustomInputService"
                 android:exported="true" android:enabled="true">
            ...
        </service>
    </application>
</manifest>
// SampleCustomInputService.java
public class SampleCustomInputService extends Service implements
        CarInputManager.CarInputCaptureCallback {
    private Car mCar;
    private CarInputManager mCarInputManager;
    private CustomInputEventListener mEventHandler;

    @Override
    public IBinder onBind(Intent intent) {
        if (intent != null) {
            connectToCarService();
        }
        return null;
    }

    private void connectToCarService() {
        if (mCar != null && mCar.isConnected()) {
            return;
        }

        mCar = Car.createCar(this, /* handler= */ 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,
                                new int[]{CarInputManager.INPUT_TYPE_CUSTOM_INPUT_EVENT},
                                CarInputManager.CAPTURE_REQ_FLAGS_ALLOW_DELAYED_GRANT,
                                /* callback= */ this);
                        mEventHandler = new CustomInputEventListener(getApplicationContext(),
                                (CarAudioManager) mCar.getCarManager(Car.AUDIO_SERVICE),
                                (CarOccupantZoneManager) mCar.getCarManager(
                                        Car.CAR_OCCUPANT_ZONE_SERVICE),
                                this);
                    }
                });
    }

    @Override
    public void onDestroy() {
        if (mCarInputManager != null) {
            mCarInputManager.releaseInputEventCapture(CarOccupantZoneManager.DISPLAY_TYPE_MAIN);
        }
        if (mCar != null) {
            mCar.disconnect();
            mCar = null;
        }
    }

    @Override
    public void onCustomInputEvents(int targetDisplayType,
            @NonNull List<CustomInputEvent> events) {
        for (CustomInputEvent event : events) {
            mEventHandler.handle(targetDisplayType, event);
        }
    }
    ...
}

CustomInputEventListener 的核心逻辑在于 handle():

  1. 首先调用 isValidTargetDisplayType() 验证屏幕类型,决定是否处理

  2. 通过 getInputCode() 从 CustomInputEvent 中提取 KEY CODE

  3. 按照预设的 Event 类型进行对应的处理,比如:
    LAUNCH_MAPS_ACTION 的话,封装启动 Map App 的方法 launchMap(),注意需要根据起初的 DisplayType 获取目标屏幕的 ID:targetDisplayId 并传入INJECT_VOICE_ASSIST_ACTION_DOWN 的话,表明是启动语音助手按键的按下事件,注入 语音助手的标准 KeyEvent 即 KEYCODE_VOICE_ASSIST 的 DOWN 事件INJECT_VOICE_ASSIST_ACTION_UP 则是注入 KEYCODE_VOICE_ASSIST 的 UP 事件等

// CustomInputEventListener.java
public final class CustomInputEventListener {
    private final SampleCustomInputService mService;
    ...

    public @interface EventAction {
        /** Launches Map action. */
        int LAUNCH_MAPS_ACTION = 1001;
        ...
        /** Injects KEYCODE_VOICE_ASSIST (action down) key event */
        int INJECT_VOICE_ASSIST_ACTION_DOWN = 1009;

        /** Injects KEYCODE_VOICE_ASSIST (action up) key event */
        int INJECT_VOICE_ASSIST_ACTION_UP = 1010;
    }

    public CustomInputEventListener( ... ) {
        mContext = context;
        ...
    }

    public void handle(int targetDisplayType, CustomInputEvent event) {
        if (!isValidTargetDisplayType(targetDisplayType)) {
            return;
        }
        int targetDisplayId = getDisplayIdForDisplayType(targetDisplayType);
        @EventAction int action = event.getInputCode();

        switch (action) {
            case EventAction.LAUNCH_MAPS_ACTION:
                launchMap(targetDisplayId);
                break;
            ...
            case EventAction.INJECT_VOICE_ASSIST_ACTION_DOWN:
                injectKeyEvent(targetDisplayType,
                        newKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOICE_ASSIST));
                break;
            case EventAction.INJECT_VOICE_ASSIST_ACTION_UP:
                injectKeyEvent(targetDisplayType,
                        newKeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_VOICE_ASSIST));
                break;
            default: Log.e(TAG, "Ignoring event [" + action + "]");
        }
    }

    private int getDisplayIdForDisplayType(int targetDisplayType) {
        int displayId = mCarOccupantZoneManager.getDisplayIdForDriver(targetDisplayType);
        return displayId;
    }

    private static boolean isValidTargetDisplayType(int displayType) {
        if (displayType == CarOccupantZoneManager.DISPLAY_TYPE_MAIN) {
            return true;
        }
        return false;
    }

    private void launchMap(int targetDisplayId) {
        ActivityOptions options = ActivityOptions.makeBasic();
        options.setLaunchDisplayId(targetDisplayId);
        Intent mapsIntent = new Intent(Intent.ACTION_VIEW);
        mapsIntent.setClassName(mContext.getString(R.string.maps_app_package),
                mContext.getString(R.string.maps_activity_class));
        mapsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        mService.startActivity(mapsIntent, options.toBundle());
    }
    ...

    private KeyEvent newKeyEvent(int action, int keyCode) {
        long currentTime = SystemClock.uptimeMillis();
        return new KeyEvent(/* downTime= */ currentTime, /* eventTime= */ currentTime,
                action, keyCode, /* repeat= */ 0);
    }

    private void injectKeyEvent(int targetDisplayType, KeyEvent event) {
        mService.injectKeyEvent(event, targetDisplayType);
    }
}

KeyEvent 的注入还需要回到自定义 CustomInputService 中,之后是调用 CarInputManager 将 Event 进一步注入。

将在下个章节阐述 CarInputManager 的进一步处理。

// SampleCustomInputService.java
public class SampleCustomInputService extends Service implements
        CarInputManager.CarInputCaptureCallback {
    ...
    public void injectKeyEvent(KeyEvent event, int targetDisplayType) {
        if (mCarInputManager == null) {
            throw new IllegalStateException(
                    "Service was properly initialized, reference to CarInputManager is null");
        }
        mCarInputManager.injectKeyEvent(event, targetDisplayType);
    }
}

需要该 Service 生效的话,需要使用如下命令启动 Service,按照逻辑向系统注册事件监听。
adb shell am start-foreground-service com.android.car.custominput.sample/.SampleCustomInputService
接下来按压硬件的按键,或者像下面一样模拟按键的输入,比如下面模拟 1001 启动 Map 的按键按下:
adb shell cmd car_service inject-custom-input -d 0 f1

系统默认处理

以上述的 KEYCODE_VOICE_ASSIST 为例,看一下 CarInputManager 的进一步处理如何。

对应的在 CarInputService 中:

  1. 首先,injectKeyEvent() 将先检查注入方的相关权限:INJECT_EVENTS
  2. 接着,调用 onKeyEvent() 执行事件的后续处理
// packages/services/Car/service/src/com/android/car/CarInputService.java
public class CarInputService ... {
    ...
    @Override
    public void injectKeyEvent(KeyEvent event, @DisplayTypeEnum int targetDisplayType) {
        // Permission check
        if (PackageManager.PERMISSION_GRANTED != mContext.checkCallingOrSelfPermission(
                android.Manifest.permission.INJECT_EVENTS)) {
            throw new SecurityException("Injecting KeyEvent requires INJECT_EVENTS permission");
        }

        long token = Binder.clearCallingIdentity();
        try {
            // Redirect event to onKeyEvent
            onKeyEvent(event, targetDisplayType);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }
}

注入的事件类型为 KEYCODE_VOICE_ASSIST 的话,交给handleVoiceAssistKey() 处理。
当 action 尚为 DOWN 时机,交给 VoiceKeyTimer 的 keyDown() 开始计时
当 action 为 UP 时机:通过 Timer 的 keyUp() 获取是否达到长按(长按时长默认是 400ms,可以在 SettingsProvider 中改写)条件,并调用 dispatchProjectionKeyEvent() 发送相应的事件
短按处理 KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP
反之,发送 KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_UP
如果 dispatchProjectionKeyEvent() 没没有拦截处理,执行默认逻辑:launchDefaultVoiceAssistantHandler()

// packages/services/Car/service/src/com/android/car/CarInputService.java
public class CarInputService ... {
    ...
    @Override
    public void onKeyEvent(KeyEvent event, @DisplayTypeEnum int targetDisplayType) {
        switch (event.getKeyCode()) {
            case KeyEvent.KEYCODE_VOICE_ASSIST:
                handleVoiceAssistKey(event);
                return;
            ...
            default:
                break;
        }
        ...
    }

    private void handleVoiceAssistKey(KeyEvent event) {
        int action = event.getAction();
        if (action == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
            mVoiceKeyTimer.keyDown();
            dispatchProjectionKeyEvent(CarProjectionManager.KEY_EVENT_VOICE_SEARCH_KEY_DOWN);
        } else if (action == KeyEvent.ACTION_UP) {
            if (mVoiceKeyTimer.keyUp()) {
                dispatchProjectionKeyEvent(
                        CarProjectionManager.KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_UP);
                return;
            }

            if (dispatchProjectionKeyEvent(
                    CarProjectionManager.KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP)) {
                return;
            }

            launchDefaultVoiceAssistantHandler();
        }
    }

    private void launchDefaultVoiceAssistantHandler() {
        if (!AssistUtilsHelper.showPushToTalkSessionForActiveService(mContext, mShowCallback)) {
            Slogf.w(TAG, "Unable to retrieve assist component for current user");
        }
    }
}

CarProjectionManager 是允许 App 向系统注册/注销某些事件处理的机制。
dispatchProjectionKeyEvent() 则将上述的短按、长按事件发送给 App 通过 CarProjectionManager 向其注册的 ProjectionKeyEventHandler 处理。

// packages/services/Car/service/src/com/android/car/CarInputService.java
public class CarInputService ... {
    ...
    private boolean dispatchProjectionKeyEvent(@CarProjectionManager.KeyEventNum int event) {
        CarProjectionManager.ProjectionKeyEventHandler projectionKeyEventHandler;
        synchronized (mLock) {
            projectionKeyEventHandler = mProjectionKeyEventHandler;
            if (projectionKeyEventHandler == null || !mProjectionKeyEventsSubscribed.get(event)) {
                return false;
            }
        }

        projectionKeyEventHandler.onKeyEvent(event);
        return true;
    }
}

// packages/services/Car/service/src/com/android/car/CarProjectionService.java
class CarProjectionService ... {
    @Override
    public void onKeyEvent(@CarProjectionManager.KeyEventNum int keyEvent) {
        Slogf.d(TAG, "Dispatching key event: " + keyEvent);
        synchronized (mLock) {
            for (BinderInterfaceContainer.BinderInterface<ICarProjectionKeyEventHandler>
                    eventHandlerInterface : mKeyEventHandlers.getInterfaces()) {
                ProjectionKeyEventHandler eventHandler =
                        (ProjectionKeyEventHandler) eventHandlerInterface;

                if (eventHandler.canHandleEvent(keyEvent)) {
                    try {
                        // oneway
                        eventHandler.binderInterface.onKeyEvent(keyEvent);
                    } catch (RemoteException e) {
                        Slogf.e(TAG, "Cannot dispatch event to client", e);
                    }
                }
            }
        }
    }
    ...
}

假使没有 App 注册或者消费了 VOICE_SEARCH 的短按/长按事件,则调用默认的 launchDefaultVoiceAssistantHandler() 通过 Assist 相关的帮助类 AssistUtilsHelper 继续。

public final class AssistUtilsHelper {
    ...
    public static boolean showPushToTalkSessionForActiveService( ... ) {
        AssistUtils assistUtils = getAssistUtils(context);
        ...
        Bundle args = new Bundle();
        args.putBoolean(EXTRA_CAR_PUSH_TO_TALK, true);

        IVoiceInteractionSessionShowCallback callbackWrapper =
                new InternalVoiceInteractionSessionShowCallback(callback);

        return assistUtils.showSessionForActiveService(args, SHOW_SOURCE_PUSH_TO_TALK,
                callbackWrapper, /* activityToken= */ null);
    }
    ...
}

默认的语音助手的启动是通过 Android 标准的 VoiceInteraction 链路完成,所以后续的处理是通过 showSessionForActiveService() 交由专门管理 VoiceInteraction 的 VoiceInteractionManagerService 系统服务来完成。

public class AssistUtils {
    ...
    public boolean showSessionForActiveService(Bundle args, int sourceFlags,
            IVoiceInteractionSessionShowCallback showCallback, IBinder activityToken) {
        try {
            if (mVoiceInteractionManagerService != null) {
                return mVoiceInteractionManagerService.showSessionForActiveService(args,
                        sourceFlags, showCallback, activityToken);
            }
        } catch (RemoteException e) {
            Log.w(TAG, "Failed to call showSessionForActiveService", e);
        }
        return false;
    }
    ...
}

具体的是找到默认的数字助手 DigitalAssitant app 的 VoiceInteractionService 进行绑定和启动对应的 Session。

public class VoiceInteractionManagerService extends SystemService {
    class VoiceInteractionManagerServiceStub extends IVoiceInteractionManagerService.Stub {
        public boolean showSessionForActiveService( ... ) {
                ...
                final long caller = Binder.clearCallingIdentity();
                try {
                    ...
                    return mImpl.showSessionLocked(args,
                            sourceFlags
                                    | VoiceInteractionSession.SHOW_WITH_ASSIST
                                    | VoiceInteractionSession.SHOW_WITH_SCREENSHOT,
                            showCallback, activityToken);
                } finally {
                    Binder.restoreCallingIdentity(caller);
                }
            }
        }
        ...
    }
    ...
}   

对 VoiceInteraction 细节感兴趣的可以参考其他文章:
如何打造车载语音交互:Google Voice Interaction 给你答案(https://mp.weixin.qq.com/s/F5uw1oC3e7gAQUQeAAL0gA)

自定义按键来源

按键的信号输入来自于 ECU,其与 AAOS 的 Hal 按照定义监听 HW_CUSTOM_INPUT 输入事件的 property 变化,来自于上述提及的 types.hal 中定义的支持自定义输入事件 Code 发送到 Car Service 层。

Car Service App 的 VehicleHal 将在 onPropertyEvent() 中接收到 HAL service 的 property 发生变化。接着,订阅了 HW_CUSTOM_INPUT property 变化的 InputHalService 的 onHalEvents() 将被调用。

之后交由 CarInputService 处理,因其在 init() 时将自己作为 InputListener 的实现传递给了 InputHalService 持有。

处理自定义输入的 App 在调用 requestInputEventCapture() 时的 Callback 将被管理在 InputCaptureClientController 中的 SparseArray 里。

自然的 CarInputService 的 onCustomInputEvent() 需要将事件交给 InputCaptureClientController 来进一步分发。

public class CarInputService ... {
    ...
    @Override
    public void onCustomInputEvent(CustomInputEvent event) {
        if (!mCaptureController.onCustomInputEvent(event)) {
            return;
        }
    }
}

InputCaptureClientController 将从 SparseArray 中获取对应的 Callback 并回调 onCustomInputEvents()。

public class InputCaptureClientController {
    ...
    public boolean onCustomInputEvent(CustomInputEvent event) {
        int displayType = event.getTargetDisplayType();
        if (!SUPPORTED_DISPLAY_TYPES.contains(displayType)) {
            return false;
        }
        ICarInputCallback callback;
        synchronized (mLock) {
            callback = getClientForInputTypeLocked(displayType,
                    CarInputManager.INPUT_TYPE_CUSTOM_INPUT_EVENT);
            if (callback == null) {
                return false;
            }
        }
        dispatchCustomInputEvent(displayType, event, callback);
        return true;
    }

    private void dispatchCustomInputEvent(@DisplayTypeEnum int targetDisplayType,
            CustomInputEvent event,
            ICarInputCallback callback) {
        CarServiceUtils.runOnCommon(() -> {
            mCustomInputEventDispatchScratchList.clear();
            mCustomInputEventDispatchScratchList.add(event);
            try {
                callback.onCustomInputEvents(targetDisplayType,
                        mCustomInputEventDispatchScratchList);
            } ...
        });
    }
}

此后便抵达了 上个实战章节实现的 SampleCustomInputService 中的 onCustomInputEvents()。

模拟调试

我们知道自定义实体按键的输入并不属于 EventHub 范畴,那么传统的 getevent、dumpsys input 也就无法监听到该事件的输入,自然也就无法使用 adb 的 input 和 sendevent 命令来反向注入,正如实战章节提到的那样,我们可以使用 Car 专用的 adb 命令来达到目的。

当然如果需要区分按键的短按和长按事件,需要像上面的事例一样提供针对 DOWN 和 UP 的两种 Code,那么模拟的时候也要模拟按键之间的时长。

adb shell cmd car_service inject-custom-input ; sleep 0.2; adb shell cmd car_service inject-custom-input

另外要留意,虽然都归属于 Android platform,但有些标准 KeyEvent 的模拟可以被 AAOS 所处理,而有些却不支持呢?

比如使用如下的命令模拟发出音量 mute Keycode,系统能完成静音,但使用同样命令模式的音量的 +/-,系统则无反应。

adb shell input keyevent <key code number or name>
adb shell sendevent [device] [type] [code] [value]

这是因为部分 AAOS 的 OEM 实现里可能删除了部分标准 KeyEvent 的处理,而改部分的标准 Event 处理挪到了 Car Input 中统一处理了,所以需要使用上述的 car_service 对应的 inject-custom-input 才行。

让我们再从整体上看下自定义按键事件的分发和处理过程:

当不用区分某种事件的短按、长按逻辑,使用一种 Code 映射即可,由 CustomInputService 直接执行。比如监控方控上的“通话”和“结束通话”实体按键:
当没有来电时,按下方向盘上的“通话”按钮会发送 DIAL intent 并显示拨号器的拨号键盘页面
当有来电时,按下方向盘上的“通话”按钮会使 TelecomManager 接听来电
当有来电时,按下方向盘上的“结束通话”按钮会使 TelecomManager 挂断电话

而当需要区分长、短按的时候,需要配置两种 Code 和 DOWN 及 UP 进行对应,由 CustomInputService 或 转发送给 CarInputService 按照 DOWN 和 UP 的时间间隔决定触发短按还是长按逻辑。

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- niushuan.com 版权所有 赣ICP备2024042780号-2

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务