從實體按鍵看 Android 車載的自定義事件機制

作者:TechMerger

在汽車數(shù)字化、智能化變革的進程中串慰,越來越多的車機設計或部分淹朋、或全部地舍棄了實體按鍵笙各,進而把車主操作的入口轉(zhuǎn)移到了車機 UI 以及語音助手。

但統(tǒng)一础芍、高效的零層級 UI 頗為困難杈抢,語音的準確率、覆蓋率亦不夠完善仑性,那么在當下的階段適當?shù)乇A舨糠謱嶓w按鍵是比較明智的選擇惶楼。

開發(fā)者都了解 Android 平臺可以監(jiān)聽按鍵、屏幕觸控诊杆、耳機插拔等硬件的事件來源歼捐,來獲取用戶輸入,進而封裝成 KeyEvent晨汹、MotionEvent 等各種事件類型豹储,并發(fā)送到 System 或 App 來進一步處理。

其原理都是利用 InputManagerService 系統(tǒng)服務讀取 EventHub 所對應的事件類型淘这,依照對應的 Mapper 轉(zhuǎn)換剥扣、Dispatcher 分發(fā)以及 Channel 傳送等步驟來完成的。

而本次探討的 Android 變體即 Automotive OS(簡稱 AAOS)作用在車載場景下慨灭,其需要更多朦乏、豐富的事件需求,比如來自方控氧骤、中控等呻疹。

可其和 Android 標準的 Event 來源不同,方控等設備并不處于同一個系統(tǒng)當中筹陵,屬于系統(tǒng)以外的 ECU 單元刽锤。那么如何高效、快捷地添加對這些系統(tǒng)以外的按鍵支持和處理朦佩,顯得非常必要并思。

這就要談到 AAOS 里特有的車載事件定制 CustomInputService

自定義按鍵的實戰(zhàn)

AAOS 默認支持的自定義事件 Code 位于文件 hardware/interfaces/automotive/vehicle/2.0/types.hal 中语稠,App 可以利用這些預設的事件 Code 進行監(jiān)聽和自定義處理邏輯宋彼。

當然弄砍,Car OEM 廠商可以使用任意有符號的 32 位數(shù)值來擴展支持自定義輸入 HW_CUSTOM_INPUTCustomInputType 枚舉范圍,以支持更多的按鍵 Code输涕,確保處理的范圍符合實際的車輛按鍵需求音婶。

 // hardware/interfaces/automotive/vehicle/2.0/types.hal
 /**
  * Input code values for HW_CUSTOM_INPUT.
  */
 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 的專用控件,比如:接電話莱坎、掛電話衣式、音量、語音檐什、微信按鈕碴卧、地圖按鈕、音樂控制等等乃正。

實戰(zhàn)的具體步驟來說住册,首先得聲明特定權限,才能監(jiān)聽 Car 的自定義輸入:

android.car.permission.CAR_MONITOR_INPUT

當然烫葬,如果涉及到向 Android 系統(tǒng)注入回標準 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>
  1. onBind() 時候調(diào)用 connectToCarService() 創(chuàng)建 Car 實例、獲取 CarInputManager搭综、CustomInputEventListener 實例,并向 CarInputManager 提供的 requestInputEventCapture() 進行注冊划栓,并傳遞 INPUT_TYPE_CUSTOM_INPUT_EVENT 作為輸入類型參數(shù)
  2. onDestroy() 里釋放對于該事件的監(jiān)聽
  3. 復寫 CarInputCaptureCallbackonCustomInputEvents() 方法兑巾,作為各事件的處理入口和時機,回調(diào)理將提供事件所屬的屏幕類型和事件類型忠荞,CustomInputEventListener 承載了具體的處理邏輯
 // 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. 首先調(diào)用 isValidTargetDisplayType() 驗證屏幕類型蒋歌,決定是否處理

  2. 通過 getInputCode() 從 CustomInputEvent 中提取 KEY CODE

  3. 按照預設的 Event 類型進行對應的處理,比如:

    • LAUNCH_MAPS_ACTION 的話委煤,封裝啟動 Map App 的方法 launchMap()堂油,注意需要根據(jù)起初的 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 中府框,之后是調(diào)用 CarInputManager 將 Event 進一步注入。

將在下個章節(jié)闡述 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,按照邏輯向系統(tǒng)注冊事件監(jiān)聽兴使。

 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

其他幾個和上述邏輯相應的事件模擬命令:

 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

系統(tǒng)的默認處理

以上述的 KEYCODE_VOICE_ASSIST 為例发魄,看一下 CarInputManager 的進一步處理如何盹牧。

對應的在 CarInputService 中:

  1. 首先,injectKeyEvent() 將先檢查注入方的相關權限:INJECT_EVENTS
  2. 接著,調(diào)用 onKeyEvent() 執(zhí)行事件的后續(xù)處理
 // 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 中改寫)條件啄清,并調(diào)用 dispatchProjectionKeyEvent() 發(fā)送相應的事件:

    • 短按處理 KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP
    • 反之,發(fā)送 KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_UP
    • 如果 dispatchProjectionKeyEvent() 沒沒有攔截處理俺孙,執(zhí)行默認邏輯: 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 向系統(tǒng)注冊/注銷某些事件處理的機制辣卒。

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

dispatchProjectionKeyEvent() 則將上述的短按、長按事件發(fā)送給 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 的短按/長按事件荣茫,則調(diào)用默認的 launchDefaultVoiceAssistantHandler() 通過 Assist 相關的幫助類 AssistUtilsHelper 繼續(xù)。

 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 鏈路完成场靴,所以后續(xù)的處理是通過 showSessionForActiveService() 交由專門管理 VoiceInteraction 的 VoiceInteractionManagerService 系統(tǒng)服務來完成啡莉。

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

具體的是找到默認的數(shù)字助手 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 細節(jié)感興趣的可以參考其他文章:

自定義按鍵的來源

按鍵的信號輸入來自于 ECU旨剥,其與 AAOS 的 Hal 按照定義監(jiān)聽 HW_CUSTOM_INPUT 輸入事件的 property 變化咧欣,來自于上述提及的 types.hal 中定義的支持自定義輸入事件 Code 發(fā)送到 Car Service 層。

Car Service App 的 VehicleHal 將在 onPropertyEvent() 中接收到 HAL service 的 property 發(fā)生變化轨帜。接著魄咕,訂閱了 HW_CUSTOM_INPUT property 變化的 InputHalService 的 onHalEvents() 將被調(diào)用。

之后交由 CarInputService 處理蚌父,因其在 init() 時將自己作為 InputListener 的實現(xiàn)傳遞給了 InputHalService 持有哮兰。

處理自定義輸入的 App 在調(diào)用 requestInputEventCapture() 時的 Callback 將被管理在 InputCaptureClientController 中的 SparseArray 里。

自然的 CarInputService 的 onCustomInputEvent() 需要將事件交給 InputCaptureClientController 來進一步分發(fā)苟弛。

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

InputCaptureClientController 將從 SparseArray 中獲取對應的 Callback 并回調(diào) 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);
             } ...
         });
     }
 }

此后便抵達了 上個實戰(zhàn)章節(jié)實現(xiàn)的 SampleCustomInputService 中的 onCustomInputEvents()。

模擬調(diào)試

在漫長的 HMI 實驗臺架膏秫、實車準備就緒之前右遭,往往需要開發(fā)者提前驗證鏈路的可行性,這時候就如何模擬這些自定義事件的注入就顯得非常需要荔睹。

我們知道自定義實體按鍵的輸入并不屬于 EventHub 范疇狸演,那么傳統(tǒng)的 geteventdumpsys input 也就無法監(jiān)聽到該事件的輸入僻他,自然也就無法使用 adb 的 inputsendevent 命令來反向注入宵距,正如實戰(zhàn)章節(jié)提到的那樣,我們可以使用 Car 專用的 adb 命令來達到目的吨拗。

 adb shell cmd car_service inject-custom-input <custom key code>
 # or
 adb shell cmd car_service inject-key <key code>

前者模擬的是自定義事件的注入满哪,后者則是針對 Android 標準事件婿斥。

當然如果需要區(qū)分按鍵的短按和長按事件,需要像上面的事例一樣提供針對 DOWN 和 UP 的兩種 Code哨鸭,那么模擬的時候也要模擬按鍵之間的時長民宿。

 adb shell cmd car_service inject-custom-input <custom key code for down>; sleep 0.2; adb shell cmd car_service inject-custom-input <custome key code for up>

另外要留意,雖然都歸屬于 Android platform像鸡,但有些標準 KeyEvent 的模擬可以被 AAOS 所處理活鹰,而有些卻不支持呢?

比如使用如下的命令模擬發(fā)出音量 mute Keycode只估,系統(tǒng)能完成靜音志群,但使用同樣命令模式的音量的 +/-,系統(tǒng)則無反應蛔钙。

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

這是因為部分 AAOS 的 OEM 實現(xiàn)里可能刪除了部分標準 KeyEvent 的處理锌云,而改部分的標準 Event 處理挪到了 Car Input 中統(tǒng)一處理了,所以需要使用上述的 car_service 對應的 inject-custom-input 才行吁脱。

結語

讓我們再從整體上看下自定義按鍵事件的分發(fā)和處理過程:

如果自定義的按鍵數(shù)量不多桑涎,可以使用 AAOS 預置的 F1~F10。反之兼贡,可以采用任意有符號的 32 位數(shù)值來擴展自定義輸入的范圍攻冷。

當不用區(qū)分某種事件的短按、長按邏輯遍希,使用一種 Code 映射即可讲衫,由 CustomInputService 直接執(zhí)行。比如監(jiān)控方控上的“通話”和“結束通話”實體按鍵:

  • 當沒有來電時孵班,按下方向盤上的“通話”按鈕會發(fā)送 DIAL intent 并顯示撥號器的撥號鍵盤頁面
  • 當有來電時,按下方向盤上的“通話”按鈕會使 TelecomManager 接聽來電
  • 當有來電時招驴,按下方向盤上的“結束通話”按鈕會使 TelecomManager 掛斷電話

而當需要區(qū)分長篙程、短按的時候,需要配置兩種 Code 和 DOWN 及 UP 進行對應别厘,由 CustomInputService 或 轉(zhuǎn)發(fā)送給 CarInputService 按照 DOWN 和 UP 的時間間隔決定觸發(fā)短按還是長按邏輯虱饿。

從遙遠的未來來講,實體按鍵的交互方式肯定會消亡触趴,取而代之的是手勢氮发、語音、眼睛等更直接冗懦、豐富的方式爽冕。

但正如前言講的,在現(xiàn)階段適當?shù)乇A舾哳l的實體按鍵披蕉,和車機的數(shù)字化颈畸、智能化之間并不沖突乌奇,車機的智能化不等于粗暴地拋棄實體按鍵等傳統(tǒng)設計。

而且需要當心的一點是:如果車機交互做得不夠好眯娱,還執(zhí)意取消了實體鍵礁苗,那真是本末倒置了

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末徙缴,一起剝皮案震驚了整個濱河市试伙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌于样,老刑警劉巖疏叨,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異百宇,居然都是意外死亡考廉,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門携御,熙熙樓的掌柜王于貴愁眉苦臉地迎上來昌粤,“玉大人,你說我怎么就攤上這事啄刹′套” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵誓军,是天一觀的道長袱讹。 經(jīng)常有香客問我,道長昵时,這世上最難降的妖魔是什么捷雕? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮壹甥,結果婚禮上救巷,老公的妹妹穿的比我還像新娘。我一直安慰自己句柠,他們只是感情好浦译,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著溯职,像睡著了一般精盅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上谜酒,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天叹俏,我揣著相機與錄音,去河邊找鬼甚带。 笑死她肯,一個胖子當著我的面吹牛佳头,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播晴氨,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼康嘉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了籽前?” 一聲冷哼從身側(cè)響起亭珍,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎枝哄,沒想到半個月后肄梨,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡挠锥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年众羡,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蓖租。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡粱侣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蓖宦,到底是詐尸還是另有隱情齐婴,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布稠茂,位于F島的核電站柠偶,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏睬关。R本人自食惡果不足惜诱担,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望电爹。 院中可真熱鬧该肴,春花似錦、人聲如沸藐不。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽雏蛮。三九已至,卻和暖如春阱州,著一層夾襖步出監(jiān)牢的瞬間挑秉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工苔货, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留犀概,地道東北人立哑。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像姻灶,于是被迫代替她去往敵國和親铛绰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容