从实体按键看 Android 车载的自定义事件机制_android 4

其他几个和上述逻辑相应的事件模拟命令:

adb shell cmd car_service inject-custom-input f2 // accept incoming calls
adb shell cmd car_service inject-custom-input f3 // reject incoming calls
adb shell cmd car_service inject-custom-input f4 // To increase media volume
adb shell cmd car_service inject-custom-input f5 // To decrease media volume
adb shell cmd car_service inject-custom-input f6 // To increase alarm volume
adb shell cmd car_service inject-custom-input f7 // To decrease alarm volume
adb shell cmd car_service inject-custom-input f8 // To simulate pressing BACK HOME button

系统的默认处理

以上述的 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 时机,交给 VoiceKeyTimerkeyDown() 开始计时
  • 当 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 向系统注册/注销某些事件处理的机制。

CarProjectionManager allows applications implementing projection to register/unregister itself with projection manager, listen for voice notification.

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
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 细节感兴趣的可以参考其他文章:

自定义按键的来源

按键的信号输入来自于 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()。

模拟调试

在漫长的 HMI 实验台架、实车准备就绪之前,往往需要开发者提前验证链路的可行性,这时候就如何模拟这些自定义事件的注入就显得非常需要。

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

adb shell cmd car_service inject-custom-input

or

adb shell cmd car_service inject-key

前者模拟的是自定义事件的注入,后者则是针对 Android 标准事件。

当然如果需要区分按键的短按和长按事件,需要像上面的事例一样提供针对 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
adb shell sendevent [device] [type] [code] [value]

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

结语

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

如果自定义的按键数量不多,可以使用 AAOS 预置的 F1~F10。反之,可以采用任意有符号的 32 位数值来扩展自定义输入的范围。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数HarmonyOS鸿蒙开发工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年HarmonyOS鸿蒙开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上HarmonyOS鸿蒙开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注鸿蒙获取)
img

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

073745)]
[外链图片转存中…(img-MetGl48F-1712742073746)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上HarmonyOS鸿蒙开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注鸿蒙获取)
[外链图片转存中…(img-xjcxv20b-1712742073746)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值