/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car; import static android.car.CarOccupantZoneManager.DisplayTypeEnum; import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING; import static com.android.car.BuiltinPackageDependency.CAR_ACCESSIBILITY_SERVICE_CLASS; import static com.android.car.CarServiceUtils.getCommonHandlerThread; import static com.android.car.CarServiceUtils.getContentResolverForUser; import static com.android.car.CarServiceUtils.isEventOfType; import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; import static com.android.internal.util.Preconditions.checkArgument; import static java.util.Objects.requireNonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.car.CarOccupantZoneManager; import android.car.CarOccupantZoneManager.OccupantZoneInfo; import android.car.CarProjectionManager; import android.car.VehicleAreaSeat; import android.car.builtin.input.InputManagerHelper; import android.car.builtin.util.AssistUtilsHelper; import android.car.builtin.util.AssistUtilsHelper.VoiceInteractionSessionShowCallbackHelper; import android.car.builtin.util.Slogf; import android.car.builtin.view.InputEventHelper; import android.car.builtin.view.KeyEventHelper; import android.car.input.CarInputManager; import android.car.input.CustomInputEvent; import android.car.input.ICarInput; import android.car.input.ICarInputCallback; import android.car.input.RotaryEvent; import android.car.user.CarUserManager.UserLifecycleListener; import android.car.user.UserLifecycleEventFilter; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.hardware.input.InputManager; import android.net.Uri; import android.os.Binder; import android.os.Handler; import android.os.UserHandle; import android.provider.CallLog.Calls; import android.provider.Settings; import android.telecom.TelecomManager; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.proto.ProtoOutputStream; import android.view.Display; import android.view.InputDevice; import android.view.InputEvent; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewConfiguration; import com.android.car.bluetooth.CarBluetoothService; import com.android.car.hal.InputHalService; import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; import com.android.car.internal.common.UserHelperLite; import com.android.car.internal.util.IndentingPrintWriter; import com.android.car.power.CarPowerManagementService; import com.android.car.systeminterface.SystemInterface; import com.android.car.user.CarUserService; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.Collections; import java.util.List; import java.util.function.BooleanSupplier; import java.util.function.IntSupplier; import java.util.function.Supplier; /** * CarInputService monitors and handles input event through vehicle HAL. */ public class CarInputService extends ICarInput.Stub implements CarServiceBase, InputHalService.InputListener { public static final String ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR = ":"; private static final int MAX_RETRIES_FOR_ENABLING_ACCESSIBILITY_SERVICES = 5; @VisibleForTesting static final String TAG = CarLog.TAG_INPUT; private static final boolean DBG = Slogf.isLoggable(TAG, Log.DEBUG); @VisibleForTesting static final String LONG_PRESS_TIMEOUT = "long_press_timeout"; /** An interface to receive {@link KeyEvent}s as they occur. */ public interface KeyEventListener { /** Called when a key event occurs. */ // TODO(b/247170915): This method is no needed anymore, please remove and use // onKeyEvent(KeyEvent event, intDisplayType, int seat) default void onKeyEvent(KeyEvent event) { } /** * Called when a key event occurs with seat. * * @param event the key event that occurred * @param displayType target display the event is associated with should be one of * {@link CarOccupantZoneManager#DISPLAY_TYPE_MAIN}, * {@link CarOccupantZoneManager#DISPLAY_TYPE_INSTRUMENT_CLUSTER}, * {@link CarOccupantZoneManager#DISPLAY_TYPE_HUD}, * {@link CarOccupantZoneManager#DISPLAY_TYPE_INPUT}, * {@link CarOccupantZoneManager#DISPLAY_TYPE_AUXILIARY}, * @param seat the area id this event is occurring from */ default void onKeyEvent(KeyEvent event, @DisplayTypeEnum int displayType, @VehicleAreaSeat.Enum int seat) { // No op } } /** An interface to receive {@link MotionEvent}s as they occur. */ public interface MotionEventListener { /** Called when a motion event occurs. */ void onMotionEvent(MotionEvent event); } private final class KeyPressTimer { private final Runnable mLongPressRunnable; private final Runnable mCallback = this::onTimerExpired; private final IntSupplier mLongPressDelaySupplier; @GuardedBy("CarInputService.this.mLock") private final Handler mHandler; @GuardedBy("CarInputService.this.mLock") private boolean mDown; @GuardedBy("CarInputService.this.mLock") private boolean mLongPress = false; KeyPressTimer( Handler handler, IntSupplier longPressDelaySupplier, Runnable longPressRunnable) { mHandler = handler; mLongPressRunnable = longPressRunnable; mLongPressDelaySupplier = longPressDelaySupplier; } /** Marks that a key was pressed, and starts the long-press timer. */ void keyDown() { synchronized (mLock) { mDown = true; mLongPress = false; mHandler.removeCallbacks(mCallback); mHandler.postDelayed(mCallback, mLongPressDelaySupplier.getAsInt()); } } /** * Marks that a key was released, and stops the long-press timer. *

* Returns true if the press was a long-press. */ boolean keyUp() { synchronized (mLock) { mHandler.removeCallbacks(mCallback); mDown = false; return mLongPress; } } private void onTimerExpired() { synchronized (mLock) { // If the timer expires after key-up, don't retroactively make the press long. if (!mDown) { return; } mLongPress = true; } mLongPressRunnable.run(); } } private final VoiceInteractionSessionShowCallbackHelper mShowCallback; static final VoiceInteractionSessionShowCallbackHelper sDefaultShowCallback = new VoiceInteractionSessionShowCallbackHelper() { @Override public void onFailed() { Slogf.w(TAG, "Failed to show VoiceInteractionSession"); } @Override public void onShown() { Slogf.d(TAG, "VoiceInteractionSessionShowCallbackHelper onShown()"); } }; private final Context mContext; private final InputHalService mInputHalService; private final CarUserService mUserService; private final CarOccupantZoneService mCarOccupantZoneService; private final CarBluetoothService mCarBluetoothService; private final CarPowerManagementService mCarPowerService; private final TelecomManager mTelecomManager; private final SystemInterface mSystemInterface; // The default handler for main-display key events. By default, injects the events into // the input queue via InputManager, but can be overridden for testing. private final KeyEventListener mDefaultKeyHandler; // The default handler for main-display motion events. By default, injects the events into // the input queue via InputManager, but can be overridden for testing. private final MotionEventListener mDefaultMotionHandler; // The supplier for the last-called number. By default, gets the number from the call log. // May be overridden for testing. private final Supplier mLastCalledNumberSupplier; // The supplier for the system long-press delay, in milliseconds. By default, gets the value // from Settings.Secure for the current user, falling back to the system-wide default // long-press delay defined in ViewConfiguration. May be overridden for testing. private final IntSupplier mLongPressDelaySupplier; // ComponentName of the RotaryService. private final String mRotaryServiceComponentName; private final BooleanSupplier mShouldCallButtonEndOngoingCallSupplier; private final Object mLock = new Object(); @GuardedBy("mLock") private CarProjectionManager.ProjectionKeyEventHandler mProjectionKeyEventHandler; @GuardedBy("mLock") private final BitSet mProjectionKeyEventsSubscribed = new BitSet(); private final KeyPressTimer mVoiceKeyTimer; private final KeyPressTimer mCallKeyTimer; @GuardedBy("mLock") private KeyEventListener mInstrumentClusterKeyListener; @GuardedBy("mLock") private final SparseArray mListeners = new SparseArray<>(); private final InputCaptureClientController mCaptureController; private int mDriverSeat = VehicleAreaSeat.SEAT_UNKNOWN; private boolean mHasDriver; // key: seat, value: power key handled by ACTION_DOWN. // {@code true} if the screen was turned on with the power key ACTION_DOWN. In this case, // we need to block the power key's ACTION_UP to prevent the device from going back to sleep. // When ACTION_UP, it is released with {@code false}. private SparseBooleanArray mPowerKeyHandled = new SparseBooleanArray(); // The default handler for special keys. The behavior of the keys is implemented in this // service. It can be overridden by {@link #registerKeyEventListener}. private final KeyEventListener mDefaultSpecialKeyHandler = new KeyEventListener() { @Override public void onKeyEvent(KeyEvent event, @DisplayTypeEnum int displayType, @VehicleAreaSeat.Enum int seat) { switch (event.getKeyCode()) { case KeyEvent.KEYCODE_HOME: handleHomeKey(event, displayType, seat); break; case KeyEvent.KEYCODE_POWER: handlePowerKey(event, displayType, seat); break; default: Slogf.e(TAG, "Key event %s is not supported by special key handler", KeyEvent.keyCodeToString(event.getKeyCode())); break; } } }; private final UserLifecycleListener mUserLifecycleListener = event -> { if (!isEventOfType(TAG, event, USER_LIFECYCLE_EVENT_TYPE_SWITCHING)) { return; } Slogf.d(TAG, "CarInputService.onEvent(%s)", event); updateCarAccessibilityServicesSettings(event.getUserId()); }; private static int getViewLongPressDelay(Context context) { return Settings.Secure.getInt(getContentResolverForUser(context, UserHandle.CURRENT.getIdentifier()), LONG_PRESS_TIMEOUT, ViewConfiguration.getLongPressTimeout()); } public CarInputService(Context context, InputHalService inputHalService, CarUserService userService, CarOccupantZoneService occupantZoneService, CarBluetoothService bluetoothService, CarPowerManagementService carPowerService, SystemInterface systemInterface) { this(context, inputHalService, userService, occupantZoneService, bluetoothService, carPowerService, systemInterface, new Handler(getCommonHandlerThread().getLooper()), context.getSystemService(TelecomManager.class), new KeyEventListener() { @Override public void onKeyEvent(KeyEvent event, @DisplayTypeEnum int displayType, @VehicleAreaSeat.Enum int seat) { InputManagerHelper.injectInputEvent( context.getSystemService(InputManager.class), event); } }, /* defaultMotionHandler= */ event -> InputManagerHelper.injectInputEvent( context.getSystemService(InputManager.class), event), /* lastCalledNumberSupplier= */ () -> Calls.getLastOutgoingCall(context), /* longPressDelaySupplier= */ () -> getViewLongPressDelay(context), /* shouldCallButtonEndOngoingCallSupplier= */ () -> context.getResources() .getBoolean(R.bool.config_callButtonEndsOngoingCall), new InputCaptureClientController(context), sDefaultShowCallback); } @VisibleForTesting CarInputService(Context context, InputHalService inputHalService, CarUserService userService, CarOccupantZoneService occupantZoneService, CarBluetoothService bluetoothService, CarPowerManagementService carPowerService, SystemInterface systemInterface, Handler handler, TelecomManager telecomManager, KeyEventListener defaultKeyHandler, MotionEventListener defaultMotionHandler, Supplier lastCalledNumberSupplier, IntSupplier longPressDelaySupplier, BooleanSupplier shouldCallButtonEndOngoingCallSupplier, InputCaptureClientController captureController, VoiceInteractionSessionShowCallbackHelper showCallback) { super(); mContext = context; mCaptureController = captureController; mInputHalService = inputHalService; mUserService = userService; mCarOccupantZoneService = occupantZoneService; mCarBluetoothService = bluetoothService; mCarPowerService = carPowerService; mSystemInterface = systemInterface; mTelecomManager = telecomManager; mDefaultKeyHandler = defaultKeyHandler; mDefaultMotionHandler = defaultMotionHandler; mLastCalledNumberSupplier = lastCalledNumberSupplier; mLongPressDelaySupplier = longPressDelaySupplier; mShowCallback = showCallback; mVoiceKeyTimer = new KeyPressTimer( handler, longPressDelaySupplier, this::handleVoiceAssistLongPress); mCallKeyTimer = new KeyPressTimer(handler, longPressDelaySupplier, this::handleCallLongPress); mRotaryServiceComponentName = mContext.getString(R.string.rotaryService); mShouldCallButtonEndOngoingCallSupplier = shouldCallButtonEndOngoingCallSupplier; registerKeyEventListener(mDefaultSpecialKeyHandler, Arrays.asList(KeyEvent.KEYCODE_HOME, KeyEvent.KEYCODE_POWER)); } /** * Set projection key event listener. If null, unregister listener. */ public void setProjectionKeyEventHandler( @Nullable CarProjectionManager.ProjectionKeyEventHandler listener, @Nullable BitSet events) { synchronized (mLock) { mProjectionKeyEventHandler = listener; mProjectionKeyEventsSubscribed.clear(); if (events != null) { mProjectionKeyEventsSubscribed.or(events); } } } /** * This method registers a keyEventListener to listen on key events that it is interested in. * * @param listener the listener to be registered * @param keyCodesOfInterest the events of interest that the listener is interested in * @throws IllegalArgumentException when an event is already registered to another listener */ public void registerKeyEventListener(KeyEventListener listener, List keyCodesOfInterest) { requireNonNull(listener, "Key event listener can not be null"); requireNonNull(keyCodesOfInterest, "Key events of interest can not be null"); checkArgument(!keyCodesOfInterest.isEmpty(), "Key events of interest can not be empty"); synchronized (mLock) { // Check for invalid key codes for (int i = 0; i < keyCodesOfInterest.size(); i++) { if (mListeners.contains(keyCodesOfInterest.get(i)) && mListeners.get(keyCodesOfInterest.get(i)) != mDefaultSpecialKeyHandler) { throw new IllegalArgumentException("Event " + KeyEvent.keyCodeToString(keyCodesOfInterest.get(i)) + " already registered to another listener"); } } for (int i = 0; i < keyCodesOfInterest.size(); i++) { mListeners.put(keyCodesOfInterest.get(i), listener); } } } /** * Unregisters the key event listener for all the keys it currently listen to * * @param listener the listener to be unregistered */ public boolean unregisterKeyEventListener(KeyEventListener listener) { requireNonNull(listener, "Key event listener can not be null"); synchronized (mLock) { var keysToRemove = new ArrayList(); for (int c = 0; c < mListeners.size(); c++) { if (!mListeners.valueAt(c).equals(listener)) { continue; } keysToRemove.add(mListeners.keyAt(c)); } if (keysToRemove.isEmpty()) { Slogf.w(TAG, "Failed to unregister listener ({%s} was not registered)", listener); return false; } for (int c = 0; c < keysToRemove.size(); c++) { mListeners.delete(keysToRemove.get(c)); } } return true; } /** * Sets the instrument cluster key event listener. */ public void setInstrumentClusterKeyListener(KeyEventListener listener) { synchronized (mLock) { mInstrumentClusterKeyListener = listener; } } @Override public void init() { if (!mInputHalService.isKeyInputSupported()) { Slogf.w(TAG, "Hal does not support key input."); return; } Slogf.d(TAG, "Hal supports key input."); mInputHalService.setInputListener(this); UserLifecycleEventFilter userSwitchingEventFilter = new UserLifecycleEventFilter.Builder() .addEventType(USER_LIFECYCLE_EVENT_TYPE_SWITCHING).build(); mUserService.addUserLifecycleListener(userSwitchingEventFilter, mUserLifecycleListener); mDriverSeat = mCarOccupantZoneService.getDriverSeat(); mHasDriver = (mDriverSeat != VehicleAreaSeat.SEAT_UNKNOWN); } @Override public void release() { synchronized (mLock) { mProjectionKeyEventHandler = null; mProjectionKeyEventsSubscribed.clear(); mInstrumentClusterKeyListener = null; mListeners.clear(); } mUserService.removeUserLifecycleListener(mUserLifecycleListener); } @Override public void onKeyEvent(KeyEvent event, @DisplayTypeEnum int targetDisplayType) { onKeyEvent(event, targetDisplayType, mDriverSeat); } /** * Called for key event * * @throws IllegalArgumentException if the passed seat is an unknown seat and the driver seat is * not an unknown seat */ @Override public void onKeyEvent(KeyEvent event, @DisplayTypeEnum int targetDisplayType, @VehicleAreaSeat.Enum int seat) { if (mHasDriver && seat == VehicleAreaSeat.SEAT_UNKNOWN) { // To support {@link #onKeyEvent(KeyEvent, int)}, we need to check whether the driver // exists or not. // For example, for a passenger-only system, the driver seat might be SEAT_UNKNOWN. // In this case, no exception should be occurred. throw new IllegalArgumentException("Unknown seat"); } // Update user activity information to car power management service. notifyUserActivity(event, targetDisplayType, seat); // Driver key events are handled the same as HW_KEY_INPUT. if (seat == mDriverSeat) { dispatchKeyEventForDriver(event, targetDisplayType); return; } // Notifies the listeners of the key event. notifyKeyEventListener(event, targetDisplayType, seat); } private void dispatchKeyEventForDriver(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: // TODO: b/288107028 - Pass target display type to handleVoiceAssistKey() // when passenger displays support voice assist keys handleVoiceAssistKey(event, targetDisplayType); 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; } mDefaultKeyHandler.onKeyEvent(event, targetDisplayType, mDriverSeat); } /** * Called for motion event */ @Override public void onMotionEvent(MotionEvent event, @DisplayTypeEnum int targetDisplayType, @VehicleAreaSeat.Enum int seat) { if (seat == VehicleAreaSeat.SEAT_UNKNOWN) { throw new IllegalArgumentException("Unknown seat"); } notifyUserActivity(event, targetDisplayType, seat); assignDisplayIdForSeat(event, targetDisplayType, seat); mDefaultMotionHandler.onMotionEvent(event); } private void notifyKeyEventListener(KeyEvent event, int targetDisplay, int seat) { KeyEventListener keyEventListener; synchronized (mLock) { keyEventListener = mListeners.get(event.getKeyCode()); } if (keyEventListener == null) { if (DBG) { Slogf.d(TAG, "Key event listener not found for event %s", KeyEvent.keyCodeToString(event.getKeyCode())); } // If there is no listener for the key event, it is injected into the core system. keyEventListener = mDefaultKeyHandler; } assignDisplayIdForSeat(event, targetDisplay, seat); keyEventListener.onKeyEvent(event, targetDisplay, seat); } private void assignDisplayId(KeyEvent event, @DisplayTypeEnum int targetDisplayType) { // Setting display id for driver user id (currently MAIN and CLUSTER display types are // linked to driver user only) int newDisplayId = mCarOccupantZoneService.getDisplayIdForDriver(targetDisplayType); // Display id is overridden even if already set. KeyEventHelper.setDisplayId(event, newDisplayId); } private void assignDisplayIdForSeat(InputEvent event, @DisplayTypeEnum int targetDisplayType, @VehicleAreaSeat.Enum int seat) { int newDisplayId = getDisplayIdForSeat(targetDisplayType, seat); InputEventHelper.setDisplayId(event, newDisplayId); } private int getDisplayIdForSeat(@DisplayTypeEnum int targetDisplayType, @VehicleAreaSeat.Enum int seat) { int zoneId = mCarOccupantZoneService.getOccupantZoneIdForSeat(seat); return mCarOccupantZoneService.getDisplayForOccupant(zoneId, targetDisplayType); } /** * Notifies the car power manager that user activity happened. */ private void notifyUserActivity(InputEvent event, @DisplayTypeEnum int targetDisplayType, @VehicleAreaSeat.Enum int seat) { int displayId = getDisplayIdForSeat(targetDisplayType, seat); if (displayId == Display.INVALID_DISPLAY) { return; } mCarPowerService.notifyUserActivity(displayId, event.getEventTime()); } @Override public void onRotaryEvent(RotaryEvent event, @DisplayTypeEnum int targetDisplay) { if (!mCaptureController.onRotaryEvent(targetDisplay, event)) { List keyEvents = rotaryEventToKeyEvents(event); for (KeyEvent keyEvent : keyEvents) { onKeyEvent(keyEvent, targetDisplay); } } } @Override public void onCustomInputEvent(CustomInputEvent event) { if (!mCaptureController.onCustomInputEvent(event)) { Slogf.w(TAG, "Failed to propagate (%s)", event); return; } Slogf.d(TAG, "Succeed injecting (%s)", event); } private static List 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; default: Slogf.e(TAG, "Unknown rotary input type: %d", event.getInputType()); return Collections.EMPTY_LIST; } ArrayList 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; } private static KeyEvent createKeyEvent(boolean down, long downTime, long eventTime, int keyCode) { return new KeyEvent( downTime, eventTime, /* action= */ down ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP, keyCode, /* repeat= */ 0, /* metaState= */ 0, /* deviceId= */ 0, /* scancode= */ 0, /* flags= */ 0, InputDevice.SOURCE_CLASS_BUTTON); } @Override public int requestInputEventCapture(ICarInputCallback callback, @DisplayTypeEnum int targetDisplayType, int[] inputTypes, int requestFlags) { return mCaptureController.requestInputEventCapture(callback, targetDisplayType, inputTypes, requestFlags); } @Override public void releaseInputEventCapture(ICarInputCallback callback, @DisplayTypeEnum int targetDisplayType) { mCaptureController.releaseInputEventCapture(callback, targetDisplayType); } /** * Injects the {@link KeyEvent} passed as parameter against Car Input API. *

* The event's display id will be overwritten accordingly to the display type (it will be * retrieved from {@link CarOccupantZoneService}). * * @param event the event to inject * @param targetDisplayType the display type associated with the event * @throws SecurityException when caller doesn't have INJECT_EVENTS permission granted */ @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); } } /** * Injects the {@link KeyEvent} passed as parameter against Car Input API. *

* The event's display id will be overwritten accordingly to the display type (it will be * retrieved from {@link CarOccupantZoneService}). * * @param event the event to inject * @param targetDisplayType the display type associated with the event * @param seat the seat associated with the event * @throws SecurityException when caller doesn't have INJECT_EVENTS permission granted */ public void injectKeyEventForSeat(KeyEvent event, @DisplayTypeEnum int targetDisplayType, @VehicleAreaSeat.Enum int seat) { // 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, seat); } finally { Binder.restoreCallingIdentity(token); } } /** * Injects the {@link MotionEvent} passed as parameter against Car Input API. *

* The event's display id will be overwritten accordingly to the display type (it will be * retrieved from {@link CarOccupantZoneService}). * * @param event the event to inject * @param targetDisplayType the display type associated with the event * @param seat the seat associated with the event * @throws SecurityException when caller doesn't have INJECT_EVENTS permission granted */ public void injectMotionEventForSeat(MotionEvent event, @DisplayTypeEnum int targetDisplayType, @VehicleAreaSeat.Enum int seat) { // Permission check if (PackageManager.PERMISSION_GRANTED != mContext.checkCallingOrSelfPermission( android.Manifest.permission.INJECT_EVENTS)) { throw new SecurityException("Injecting MotionEvent requires INJECT_EVENTS permission"); } long token = Binder.clearCallingIdentity(); try { // Redirect event to onMotionEvent onMotionEvent(event, targetDisplayType, seat); } finally { Binder.restoreCallingIdentity(token); } } private void handleVoiceAssistKey(KeyEvent event, @DisplayTypeEnum int targetDisplayType) { 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()) { // Long press already handled by handleVoiceAssistLongPress(), nothing more to do. // Hand it off to projection, if it's interested, otherwise we're done. dispatchProjectionKeyEvent( CarProjectionManager.KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_UP); return; } if (dispatchProjectionKeyEvent( CarProjectionManager.KEY_EVENT_VOICE_SEARCH_SHORT_PRESS_KEY_UP)) { return; } // TODO: b/288107028 - Pass the actual target display type to onKeyEvent // when passenger displays support voice assist keys if (mCaptureController.onKeyEvent(targetDisplayType, event)) { return; } launchDefaultVoiceAssistantHandler(); } } private void handleVoiceAssistLongPress() { // If projection wants this event, let it take it. if (dispatchProjectionKeyEvent( CarProjectionManager.KEY_EVENT_VOICE_SEARCH_LONG_PRESS_KEY_DOWN)) { return; } // Otherwise, try to launch voice recognition on a BT device. if (launchBluetoothVoiceRecognition()) { return; } // Finally, fallback to the default voice assist handling. launchDefaultVoiceAssistantHandler(); } private void handleCallKey(KeyEvent event) { int action = event.getAction(); if (action == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { mCallKeyTimer.keyDown(); dispatchProjectionKeyEvent(CarProjectionManager.KEY_EVENT_CALL_KEY_DOWN); } else if (action == KeyEvent.ACTION_UP) { if (mCallKeyTimer.keyUp()) { // Long press already handled by handleCallLongPress(), nothing more to do. // Hand it off to projection, if it's interested, otherwise we're done. dispatchProjectionKeyEvent(CarProjectionManager.KEY_EVENT_CALL_LONG_PRESS_KEY_UP); return; } if (acceptCallIfRinging()) { // Ringing call answered, nothing more to do. return; } if (mShouldCallButtonEndOngoingCallSupplier.getAsBoolean() && endCall()) { // On-going call ended, nothing more to do. return; } if (dispatchProjectionKeyEvent( CarProjectionManager.KEY_EVENT_CALL_SHORT_PRESS_KEY_UP)) { return; } launchDialerHandler(); } } private void handleCallLongPress() { // Long-press answers call if ringing, same as short-press. if (acceptCallIfRinging()) { return; } if (mShouldCallButtonEndOngoingCallSupplier.getAsBoolean() && endCall()) { return; } if (dispatchProjectionKeyEvent(CarProjectionManager.KEY_EVENT_CALL_LONG_PRESS_KEY_DOWN)) { return; } dialLastCallHandler(); } private void handlePowerKey(KeyEvent event, @DisplayTypeEnum int targetDisplayType, @VehicleAreaSeat.Enum int seat) { if (DBG) { Slogf.d(TAG, "called handlePowerKey: DisplayType=%d, VehicleAreaSeat=%d", targetDisplayType, seat); } int displayId = getDisplayIdForSeat(targetDisplayType, seat); if (displayId == Display.INVALID_DISPLAY) { Slogf.e(TAG, "Failed to set display power state : Invalid display type=%d, seat=%d", targetDisplayType, seat); return; } boolean isOn = mSystemInterface.isDisplayEnabled(displayId); if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { if (!isOn) { mCarPowerService.setDisplayPowerState(displayId, /* enable= */ true); setPowerKeyHandled(seat, /* handled= */ true); } } else if (event.getAction() == KeyEvent.ACTION_UP) { if (isOn && !isPowerKeyHandled(seat)) { mCarPowerService.setDisplayPowerState(displayId, /* enable= */ false); } setPowerKeyHandled(seat, /* handled= */ false); } } private boolean isPowerKeyHandled(@VehicleAreaSeat.Enum int seat) { return mPowerKeyHandled.get(seat); } private void setPowerKeyHandled(@VehicleAreaSeat.Enum int seat, boolean handled) { mPowerKeyHandled.put(seat, handled); } private void handleHomeKey(KeyEvent event, @DisplayTypeEnum int targetDisplayType, @VehicleAreaSeat.Enum int seat) { if (DBG) { Slogf.d(TAG, "called handleHomeKey: DisplayType=%d, VehicleAreaSeat=%d", targetDisplayType, seat); } if (event.getAction() == KeyEvent.ACTION_UP) { int zoneId = mCarOccupantZoneService.getOccupantZoneIdForSeat(seat); if (zoneId == OccupantZoneInfo.INVALID_ZONE_ID) { Slogf.w(TAG, "Failed to get occupant zone id : Invalid seat=%d", seat); return; } int userId = mCarOccupantZoneService.getUserForOccupant(zoneId); int displayId = mCarOccupantZoneService.getDisplayForOccupant(zoneId, targetDisplayType); CarServiceUtils.startHomeForUserAndDisplay(mContext, userId, displayId); } } private boolean dispatchProjectionKeyEvent(@CarProjectionManager.KeyEventNum int event) { CarProjectionManager.ProjectionKeyEventHandler projectionKeyEventHandler; synchronized (mLock) { projectionKeyEventHandler = mProjectionKeyEventHandler; if (projectionKeyEventHandler == null || !mProjectionKeyEventsSubscribed.get(event)) { // No event handler, or event handler doesn't want this event - we're done. return false; } } projectionKeyEventHandler.onKeyEvent(event); return true; } private void launchDialerHandler() { Slogf.i(TAG, "call key, launch dialer intent"); Intent dialerIntent = new Intent(Intent.ACTION_DIAL); mContext.startActivityAsUser(dialerIntent, UserHandle.CURRENT); } private void dialLastCallHandler() { Slogf.i(TAG, "call key, dialing last call"); String lastNumber = mLastCalledNumberSupplier.get(); if (!TextUtils.isEmpty(lastNumber)) { Intent callLastNumberIntent = new Intent(Intent.ACTION_CALL) .setData(Uri.fromParts("tel", lastNumber, /* fragment= */ null)) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivityAsUser(callLastNumberIntent, UserHandle.CURRENT); } } private boolean acceptCallIfRinging() { if (mTelecomManager != null && mTelecomManager.isRinging()) { Slogf.i(TAG, "call key while ringing. Answer the call!"); mTelecomManager.acceptRingingCall(); return true; } return false; } private boolean endCall() { if (mTelecomManager != null && mTelecomManager.isInCall()) { Slogf.i(TAG, "End the call!"); mTelecomManager.endCall(); return true; } return false; } private boolean isBluetoothVoiceRecognitionEnabled() { Resources res = mContext.getResources(); return res.getBoolean(R.bool.enableLongPressBluetoothVoiceRecognition); } private boolean launchBluetoothVoiceRecognition() { if (isBluetoothVoiceRecognitionEnabled()) { Slogf.d(TAG, "Attempting to start Bluetooth Voice Recognition."); return mCarBluetoothService.startBluetoothVoiceRecognition(); } Slogf.d(TAG, "Unable to start Bluetooth Voice Recognition, it is not enabled."); return false; } private void launchDefaultVoiceAssistantHandler() { Slogf.d(TAG, "voice key, invoke AssistUtilsHelper"); if (!AssistUtilsHelper.showPushToTalkSessionForActiveService(mContext, mShowCallback)) { Slogf.w(TAG, "Unable to retrieve assist component for current user"); } } /** * @return false if the KeyEvent isn't consumed because there is no * InstrumentClusterKeyListener. */ private boolean handleInstrumentClusterKey(KeyEvent event) { KeyEventListener listener; synchronized (mLock) { listener = mInstrumentClusterKeyListener; } if (listener == null) { return false; } listener.onKeyEvent(event); return true; } private List getAccessibilityServicesToBeEnabled() { String carSafetyAccessibilityServiceComponentName = BuiltinPackageDependency .getComponentName(CAR_ACCESSIBILITY_SERVICE_CLASS); ArrayList accessibilityServicesToBeEnabled = new ArrayList<>(); accessibilityServicesToBeEnabled.add(carSafetyAccessibilityServiceComponentName); if (!TextUtils.isEmpty(mRotaryServiceComponentName)) { accessibilityServicesToBeEnabled.add(mRotaryServiceComponentName); } return accessibilityServicesToBeEnabled; } private static List createServiceListFromSettingsString( String accessibilityServicesString) { return TextUtils.isEmpty(accessibilityServicesString) ? new ArrayList<>() : Arrays.asList(accessibilityServicesString.split( ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR)); } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) public void dump(IndentingPrintWriter writer) { writer.println("*Input Service*"); writer.println("Long-press delay: " + mLongPressDelaySupplier.getAsInt() + "ms"); writer.println("Call button ends ongoing call: " + mShouldCallButtonEndOngoingCallSupplier.getAsBoolean()); mCaptureController.dump(writer); } @Override @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) public void dumpProto(ProtoOutputStream proto) { // No op } private void updateCarAccessibilityServicesSettings(@UserIdInt int userId) { if (UserHelperLite.isHeadlessSystemUser(userId)) { return; } List accessibilityServicesToBeEnabled = getAccessibilityServicesToBeEnabled(); ContentResolver contentResolverForUser = getContentResolverForUser(mContext, userId); List alreadyEnabledServices = createServiceListFromSettingsString( Settings.Secure.getString(contentResolverForUser, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)); int retry = 0; while (!alreadyEnabledServices.containsAll(accessibilityServicesToBeEnabled) && retry <= MAX_RETRIES_FOR_ENABLING_ACCESSIBILITY_SERVICES) { ArrayList enabledServicesList = new ArrayList<>(alreadyEnabledServices); int numAccessibilityServicesToBeEnabled = accessibilityServicesToBeEnabled.size(); for (int i = 0; i < numAccessibilityServicesToBeEnabled; i++) { String serviceToBeEnabled = accessibilityServicesToBeEnabled.get(i); if (!enabledServicesList.contains(serviceToBeEnabled)) { enabledServicesList.add(serviceToBeEnabled); } } Settings.Secure.putString(contentResolverForUser, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, String.join(ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR, enabledServicesList)); // Read again to account for any race condition with other parts of the code that might // be enabling other accessibility services. alreadyEnabledServices = createServiceListFromSettingsString( Settings.Secure.getString(contentResolverForUser, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)); retry++; } if (!alreadyEnabledServices.containsAll(accessibilityServicesToBeEnabled)) { Slogf.e(TAG, "Failed to enable accessibility services"); } Settings.Secure.putString(contentResolverForUser, Settings.Secure.ACCESSIBILITY_ENABLED, "1"); } }