作者: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_INPUT
的 CustomInputType
枚舉范圍,以支持更多的按鍵 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>
- onBind() 時候調(diào)用 connectToCarService() 創(chuàng)建 Car 實例、獲取
CarInputManager
搭综、CustomInputEventListener
實例,并向 CarInputManager 提供的requestInputEventCapture()
進行注冊划栓,并傳遞 INPUT_TYPE_CUSTOM_INPUT_EVENT 作為輸入類型參數(shù) - onDestroy() 里釋放對于該事件的監(jiān)聽
- 復寫
CarInputCaptureCallback
的onCustomInputEvents()
方法兑巾,作為各事件的處理入口和時機,回調(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():
首先調(diào)用 isValidTargetDisplayType() 驗證屏幕類型蒋歌,決定是否處理
通過 getInputCode() 從
CustomInputEvent
中提取 KEY CODE-
按照預設的 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
中:
- 首先,injectKeyEvent() 將先檢查注入方的相關權限:
INJECT_EVENTS
- 接著,調(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 時機,交給
VoiceKeyTimer
的keyDown()
開始計時-
當 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)的 getevent
、dumpsys input
也就無法監(jiān)聽到該事件的輸入僻他,自然也就無法使用 adb 的 input
和 sendevent
命令來反向注入宵距,正如實戰(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í)意取消了實體鍵礁苗,那真是本末倒置了。