/* * Copyright (C) 2020 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 java.util.Map.entry; import android.annotation.NonNull; import android.car.Car; import android.car.CarOccupantZoneManager; import android.car.input.CarInputManager; import android.car.input.CustomInputEvent; import android.car.input.ICarInputCallback; import android.car.input.RotaryEvent; import android.content.ComponentName; import android.content.Context; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Slog; import android.util.SparseArray; import android.view.KeyEvent; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; /** * Manages input capture request from clients */ public class InputCaptureClientController { private static final boolean DBG_STACK = false; private static final boolean DBG_DISPATCH = false; private static final boolean DBG_CALLS = false; private static final String TAG = CarLog.tagFor(InputCaptureClientController.class); /** * This table decides which input key goes into which input type. Not mapped here means it is * not supported for capturing. Rotary events are treated separately and this is only for * key events. */ private static final Map KEY_EVENT_TO_INPUT_TYPE = Map.ofEntries( entry(KeyEvent.KEYCODE_DPAD_CENTER, CarInputManager.INPUT_TYPE_DPAD_KEYS), entry(KeyEvent.KEYCODE_DPAD_DOWN, CarInputManager.INPUT_TYPE_DPAD_KEYS), entry(KeyEvent.KEYCODE_DPAD_UP, CarInputManager.INPUT_TYPE_DPAD_KEYS), entry(KeyEvent.KEYCODE_DPAD_LEFT, CarInputManager.INPUT_TYPE_DPAD_KEYS), entry(KeyEvent.KEYCODE_DPAD_RIGHT, CarInputManager.INPUT_TYPE_DPAD_KEYS), entry(KeyEvent.KEYCODE_DPAD_DOWN_LEFT, CarInputManager.INPUT_TYPE_DPAD_KEYS), entry(KeyEvent.KEYCODE_DPAD_DOWN_RIGHT, CarInputManager.INPUT_TYPE_DPAD_KEYS), entry(KeyEvent.KEYCODE_DPAD_UP_LEFT, CarInputManager.INPUT_TYPE_DPAD_KEYS), entry(KeyEvent.KEYCODE_DPAD_UP_RIGHT, CarInputManager.INPUT_TYPE_DPAD_KEYS), entry(KeyEvent.KEYCODE_NAVIGATE_IN, CarInputManager.INPUT_TYPE_NAVIGATE_KEYS), entry(KeyEvent.KEYCODE_NAVIGATE_OUT, CarInputManager.INPUT_TYPE_NAVIGATE_KEYS), entry(KeyEvent.KEYCODE_NAVIGATE_NEXT, CarInputManager.INPUT_TYPE_NAVIGATE_KEYS), entry(KeyEvent.KEYCODE_NAVIGATE_PREVIOUS, CarInputManager.INPUT_TYPE_NAVIGATE_KEYS), entry(KeyEvent.KEYCODE_BACK, CarInputManager.INPUT_TYPE_NAVIGATE_KEYS), entry(KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN, CarInputManager.INPUT_TYPE_SYSTEM_NAVIGATE_KEYS), entry(KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP, CarInputManager.INPUT_TYPE_SYSTEM_NAVIGATE_KEYS), entry(KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT, CarInputManager.INPUT_TYPE_SYSTEM_NAVIGATE_KEYS), entry(KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT, CarInputManager.INPUT_TYPE_SYSTEM_NAVIGATE_KEYS) ); private static final Set VALID_INPUT_TYPES = Set.of( CarInputManager.INPUT_TYPE_ALL_INPUTS, CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION, CarInputManager.INPUT_TYPE_DPAD_KEYS, CarInputManager.INPUT_TYPE_NAVIGATE_KEYS, CarInputManager.INPUT_TYPE_SYSTEM_NAVIGATE_KEYS, CarInputManager.INPUT_TYPE_CUSTOM_INPUT_EVENT ); private static final Set VALID_ROTARY_TYPES = Set.of( CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION ); // TODO(b/150818155) Need to migrate cluster code to use this to enable it. private static final List SUPPORTED_DISPLAY_TYPES = List.of( CarOccupantZoneManager.DISPLAY_TYPE_MAIN, CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER ); private static final int[] EMPTY_INPUT_TYPES = new int[0]; private final class ClientInfoForDisplay implements IBinder.DeathRecipient { private final int mUid; private final int mPid; private final ICarInputCallback mCallback; private final int mTargetDisplayType; private final int[] mInputTypes; private final int mFlags; private final ArrayList mGrantedTypes; private ClientInfoForDisplay(int uid, int pid, @NonNull ICarInputCallback callback, int targetDisplayType, int[] inputTypes, int flags) { mUid = uid; mPid = pid; mCallback = callback; mTargetDisplayType = targetDisplayType; mInputTypes = inputTypes; mFlags = flags; mGrantedTypes = new ArrayList<>(inputTypes.length); } private void linkToDeath() throws RemoteException { mCallback.asBinder().linkToDeath(this, 0); } private void unlinkToDeath() { mCallback.asBinder().unlinkToDeath(this, 0); } @Override public void binderDied() { onClientDeath(this); } @Override public String toString() { return new StringBuilder(128) .append("Client{") .append("uid:") .append(mUid) .append(",pid:") .append(mPid) .append(",callback:") .append(mCallback) .append(",inputTypes:") .append(mInputTypes) .append(",flags:") .append(Integer.toHexString(mFlags)) .append(",grantedTypes:") .append(mGrantedTypes) .append("}") .toString(); } } private static final class ClientsToDispatch { // The same client can be added multiple times. Keeping only the last addition is ok. private final ArrayMap mClientsToDispatch = new ArrayMap<>(); private final int mDisplayType; private ClientsToDispatch(int displayType) { mDisplayType = displayType; } private void add(ClientInfoForDisplay client) { int[] inputTypesToDispatch; if (client.mGrantedTypes.isEmpty()) { inputTypesToDispatch = EMPTY_INPUT_TYPES; } else { inputTypesToDispatch = ArrayUtils.convertToIntArray(client.mGrantedTypes); } mClientsToDispatch.put(client.mCallback, inputTypesToDispatch); } } private final Context mContext; private final Object mLock = new Object(); /** * key: display type, for quick discovery of client * LinkedList is for implementing stack. First entry is the top. */ @GuardedBy("mLock") private final SparseArray> mFullDisplayEventCapturers = new SparseArray<>(2); /** * key: display type -> inputType, for quick discovery of client * LinkedList is for implementing stack. First entry is the top. */ @GuardedBy("mLock") private final SparseArray>> mPerInputTypeCapturers = new SparseArray<>(2); @GuardedBy("mLock") /** key: display type -> client binder */ private final SparseArray> mAllClients = new SparseArray<>(1); @GuardedBy("mLock") /** Keeps events to dispatch together. FIFO, last one added to last */ private final LinkedList mClientDispatchQueue = new LinkedList<>(); /** Accessed from dispatch thread only */ private final ArrayList mKeyEventDispatchScratchList = new ArrayList<>(1); /** Accessed from dispatch thread only */ private final ArrayList mRotaryEventDispatchScratchList = new ArrayList<>(1); /** Accessed from dispatch thread only */ private final ArrayList mCustomInputEventDispatchScratchList = new ArrayList<>(1); @GuardedBy("mLock") private int mNumKeyEventsDispatched; @GuardedBy("mLock") private int mNumRotaryEventsDispatched; private final String mClusterHomePackage; public InputCaptureClientController(Context context) { mContext = context; mClusterHomePackage = ComponentName.unflattenFromString( mContext.getString(R.string.config_clusterHomeActivity)).getPackageName(); } /** * See * {@link CarInputManager#requestInputEventCapture(CarInputManager.CarInputCaptureCallback, * int, int[], int)}. */ public int requestInputEventCapture(ICarInputCallback callback, @DisplayTypeEnum int targetDisplayType, int[] inputTypes, int requestFlags) { ICarImpl.assertAnyPermission(mContext, Car.PERMISSION_CAR_MONITOR_INPUT, android.Manifest.permission.MONITOR_INPUT); Preconditions.checkArgument(SUPPORTED_DISPLAY_TYPES.contains(targetDisplayType), "Display not supported yet:" + targetDisplayType); boolean isRequestingAllEvents = (requestFlags & CarInputManager.CAPTURE_REQ_FLAGS_TAKE_ALL_EVENTS_FOR_DISPLAY) != 0; if (isRequestingAllEvents) { if (targetDisplayType != CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER) { ICarImpl.assertCallingFromSystemProcessOrSelf(); } else { // for DISPLAY_TYPE_INSTRUMENT_CLUSTER if (!ICarImpl.isCallingFromSystemProcessOrSelf()) { CarServiceUtils.assertPackageName(mContext, mClusterHomePackage); } } if (inputTypes.length != 1 || inputTypes[0] != CarInputManager.INPUT_TYPE_ALL_INPUTS) { throw new IllegalArgumentException("Input type should be INPUT_TYPE_ALL_INPUTS" + " for CAPTURE_REQ_FLAGS_TAKE_ALL_EVENTS_FOR_DISPLAY"); } } if (targetDisplayType != CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER && targetDisplayType != CarOccupantZoneManager.DISPLAY_TYPE_MAIN) { throw new IllegalArgumentException("Unrecognized display type:" + targetDisplayType); } if (inputTypes == null) { throw new IllegalArgumentException("inputTypes cannot be null"); } assertInputTypeValid(inputTypes); Arrays.sort(inputTypes); IBinder clientBinder = callback.asBinder(); boolean allowsDelayedGrant = (requestFlags & CarInputManager.CAPTURE_REQ_FLAGS_ALLOW_DELAYED_GRANT) != 0; int ret = CarInputManager.INPUT_CAPTURE_RESPONSE_SUCCEEDED; if (DBG_CALLS) { Slog.i(TAG, "requestInputEventCapture callback:" + callback + ", display:" + targetDisplayType + ", inputTypes:" + Arrays.toString(inputTypes) + ", flags:" + requestFlags); } ClientsToDispatch clientsToDispatch = new ClientsToDispatch(targetDisplayType); synchronized (mLock) { HashMap allClientsForDisplay = mAllClients.get( targetDisplayType); if (allClientsForDisplay == null) { allClientsForDisplay = new HashMap(); mAllClients.put(targetDisplayType, allClientsForDisplay); } ClientInfoForDisplay oldClientInfo = allClientsForDisplay.remove(clientBinder); LinkedList fullCapturersStack = mFullDisplayEventCapturers.get( targetDisplayType); if (fullCapturersStack == null) { fullCapturersStack = new LinkedList(); mFullDisplayEventCapturers.put(targetDisplayType, fullCapturersStack); } if (!isRequestingAllEvents && fullCapturersStack.size() > 0 && fullCapturersStack.getFirst() != oldClientInfo && !allowsDelayedGrant) { // full capturing active. return failed if not delayed granting. return CarInputManager.INPUT_CAPTURE_RESPONSE_FAILED; } // Now we need to register client anyway, so do death monitoring from here. ClientInfoForDisplay newClient = new ClientInfoForDisplay(Binder.getCallingUid(), Binder.getCallingPid(), callback, targetDisplayType, inputTypes, requestFlags); try { newClient.linkToDeath(); } catch (RemoteException e) { // client died Slog.i(TAG, "requestInputEventCapture, cannot linkToDeath to client, pid:" + Binder.getCallingUid()); return CarInputManager.INPUT_CAPTURE_RESPONSE_FAILED; } SparseArray> perInputStacks = mPerInputTypeCapturers.get(targetDisplayType); if (perInputStacks == null) { perInputStacks = new SparseArray>(); mPerInputTypeCapturers.put(targetDisplayType, perInputStacks); } if (isRequestingAllEvents) { if (!fullCapturersStack.isEmpty()) { ClientInfoForDisplay oldCapturer = fullCapturersStack.getFirst(); if (oldCapturer != oldClientInfo) { oldCapturer.mGrantedTypes.clear(); clientsToDispatch.add(oldCapturer); } fullCapturersStack.remove(oldClientInfo); } else { // All per input type top stack client should be notified. for (int i = 0; i < perInputStacks.size(); i++) { LinkedList perTypeStack = perInputStacks.valueAt(i); if (!perTypeStack.isEmpty()) { ClientInfoForDisplay topClient = perTypeStack.getFirst(); if (topClient != oldClientInfo) { topClient.mGrantedTypes.clear(); clientsToDispatch.add(topClient); } // Even if the client was on top, the one in back does not need // update. perTypeStack.remove(oldClientInfo); } } } fullCapturersStack.addFirst(newClient); } else { boolean hadFullCapture = false; boolean fullCaptureActive = false; if (fullCapturersStack.size() > 0) { if (fullCapturersStack.getFirst() == oldClientInfo) { fullCapturersStack.remove(oldClientInfo); // Now we need to check if there is other client in fullCapturersStack if (fullCapturersStack.size() > 0) { fullCaptureActive = true; ret = CarInputManager.INPUT_CAPTURE_RESPONSE_DELAYED; ClientInfoForDisplay topClient = fullCapturersStack.getFirst(); topClient.mGrantedTypes.clear(); topClient.mGrantedTypes.add(CarInputManager.INPUT_TYPE_ALL_INPUTS); clientsToDispatch.add(topClient); } else { hadFullCapture = true; } } else { // other client doing full capturing and it should have DELAYED_GRANT flag. fullCaptureActive = true; ret = CarInputManager.INPUT_CAPTURE_RESPONSE_DELAYED; } } for (int i = 0; i < perInputStacks.size(); i++) { LinkedList perInputStack = perInputStacks.valueAt(i); perInputStack.remove(oldClientInfo); } // Now go through per input stack for (int inputType : inputTypes) { LinkedList perInputStack = perInputStacks.get( inputType); if (perInputStack == null) { perInputStack = new LinkedList(); perInputStacks.put(inputType, perInputStack); } if (perInputStack.size() > 0) { ClientInfoForDisplay oldTopClient = perInputStack.getFirst(); if (oldTopClient.mGrantedTypes.remove(Integer.valueOf(inputType))) { clientsToDispatch.add(oldTopClient); } } if (!fullCaptureActive) { newClient.mGrantedTypes.add(inputType); } perInputStack.addFirst(newClient); } if (!fullCaptureActive && hadFullCapture) { for (int i = 0; i < perInputStacks.size(); i++) { int inputType = perInputStacks.keyAt(i); LinkedList perInputStack = perInputStacks.valueAt( i); if (perInputStack.size() > 0) { ClientInfoForDisplay topStackClient = perInputStack.getFirst(); if (topStackClient == newClient) { continue; } if (!topStackClient.mGrantedTypes.contains(inputType)) { topStackClient.mGrantedTypes.add(inputType); clientsToDispatch.add(topStackClient); } } } } } allClientsForDisplay.put(clientBinder, newClient); dispatchClientCallbackLocked(clientsToDispatch); } return ret; } /** * See {@link CarInputManager#releaseInputEventCapture(int)}. */ public void releaseInputEventCapture(ICarInputCallback callback, int targetDisplayType) { Objects.requireNonNull(callback); Preconditions.checkArgument(SUPPORTED_DISPLAY_TYPES.contains(targetDisplayType), "Display not supported yet:" + targetDisplayType); if (DBG_CALLS) { Slog.i(TAG, "releaseInputEventCapture callback:" + callback + ", display:" + targetDisplayType); } ClientsToDispatch clientsToDispatch = new ClientsToDispatch(targetDisplayType); synchronized (mLock) { HashMap allClientsForDisplay = mAllClients.get( targetDisplayType); ClientInfoForDisplay clientInfo = allClientsForDisplay.remove(callback.asBinder()); if (clientInfo == null) { Slog.w(TAG, "Cannot find client for releaseInputEventCapture:" + callback); return; } clientInfo.unlinkToDeath(); LinkedList fullCapturersStack = mFullDisplayEventCapturers.get( targetDisplayType); boolean fullCaptureActive = false; if (fullCapturersStack.size() > 0) { if (fullCapturersStack.getFirst() == clientInfo) { fullCapturersStack.remove(clientInfo); if (fullCapturersStack.size() > 0) { ClientInfoForDisplay newStopStackClient = fullCapturersStack.getFirst(); newStopStackClient.mGrantedTypes.clear(); newStopStackClient.mGrantedTypes.add(CarInputManager.INPUT_TYPE_ALL_INPUTS); clientsToDispatch.add(newStopStackClient); fullCaptureActive = true; } } else { // no notification as other client is in top of the stack fullCaptureActive = true; } fullCapturersStack.remove(clientInfo); } SparseArray> perInputStacks = mPerInputTypeCapturers.get(targetDisplayType); if (DBG_STACK) { Slog.i(TAG, "releaseInputEventCapture, fullCaptureActive:" + fullCaptureActive + ", perInputStacks:" + perInputStacks); } if (perInputStacks != null) { for (int i = 0; i < perInputStacks.size(); i++) { int inputType = perInputStacks.keyAt(i); LinkedList perInputStack = perInputStacks.valueAt(i); if (perInputStack.size() > 0) { if (perInputStack.getFirst() == clientInfo) { perInputStack.removeFirst(); if (perInputStack.size() > 0) { ClientInfoForDisplay newTopClient = perInputStack.getFirst(); if (!fullCaptureActive) { newTopClient.mGrantedTypes.add(inputType); clientsToDispatch.add(newTopClient); } } } else { // something else on top. if (!fullCaptureActive) { ClientInfoForDisplay topClient = perInputStack.getFirst(); if (!topClient.mGrantedTypes.contains(inputType)) { topClient.mGrantedTypes.add(inputType); clientsToDispatch.add(topClient); } } perInputStack.remove(clientInfo); } } } } dispatchClientCallbackLocked(clientsToDispatch); } } /** * Dispatches the given {@code KeyEvent} to a capturing client if there is one. * * @param displayType the display type defined in {@code CarInputManager} such as * {@link CarOccupantZoneManager#DISPLAY_TYPE_MAIN} * @param event the key event to handle * @return true if the event was consumed. */ public boolean onKeyEvent(@DisplayTypeEnum int displayType, KeyEvent event) { if (!SUPPORTED_DISPLAY_TYPES.contains(displayType)) { return false; } Integer inputType = KEY_EVENT_TO_INPUT_TYPE.get(event.getKeyCode()); if (inputType == null) { // not supported key inputType = CarInputManager.INPUT_TYPE_ALL_INPUTS; } ICarInputCallback callback; synchronized (mLock) { callback = getClientForInputTypeLocked(displayType, inputType); if (callback == null) { return false; } mNumKeyEventsDispatched++; } dispatchKeyEvent(displayType, event, callback); return true; } /** * Dispatches the given {@code RotaryEvent} to a capturing client if there is one. * * @param displayType the display type defined in {@code CarInputManager} such as * {@link CarOccupantZoneManager#DISPLAY_TYPE_MAIN} * @param event the Rotary event to handle * @return true if the event was consumed. */ public boolean onRotaryEvent(@DisplayTypeEnum int displayType, RotaryEvent event) { if (!SUPPORTED_DISPLAY_TYPES.contains(displayType)) { Slog.w(TAG, "onRotaryEvent for not supported display:" + displayType); return false; } int inputType = event.getInputType(); if (!VALID_ROTARY_TYPES.contains(inputType)) { Slog.w(TAG, "onRotaryEvent for not supported input type:" + inputType); return false; } ICarInputCallback callback; synchronized (mLock) { callback = getClientForInputTypeLocked(displayType, inputType); if (callback == null) { if (DBG_DISPATCH) { Slog.i(TAG, "onRotaryEvent no client for input type:" + inputType); } return false; } mNumRotaryEventsDispatched++; } dispatchRotaryEvent(displayType, event, callback); return true; } /** * Dispatches the given {@link CustomInputEvent} to a capturing client if there is one. * Nothing happens if no callback was registered for the incoming event. In this case this * method will return {@code false}. *

* In case of there are more than one client registered for this event, then only the first one * will be notified. * * @param event the {@link CustomInputEvent} to dispatch * @return {@code true} if the event was consumed. */ public boolean onCustomInputEvent(CustomInputEvent event) { int displayType = event.getTargetDisplayType(); if (!SUPPORTED_DISPLAY_TYPES.contains(displayType)) { Slog.w(TAG, "onCustomInputEvent for not supported display:" + displayType); return false; } ICarInputCallback callback; synchronized (mLock) { callback = getClientForInputTypeLocked(displayType, CarInputManager.INPUT_TYPE_CUSTOM_INPUT_EVENT); if (callback == null) { Slog.w(TAG, "No client for input: " + CarInputManager.INPUT_TYPE_CUSTOM_INPUT_EVENT + " and display: " + displayType); return false; } } dispatchCustomInputEvent(displayType, event, callback); return true; } ICarInputCallback getClientForInputTypeLocked(int targetDisplayType, int inputType) { LinkedList fullCapturersStack = mFullDisplayEventCapturers.get( targetDisplayType); if (fullCapturersStack != null && fullCapturersStack.size() > 0) { return fullCapturersStack.getFirst().mCallback; } SparseArray> perInputStacks = mPerInputTypeCapturers.get(targetDisplayType); if (perInputStacks == null) { return null; } LinkedList perInputStack = perInputStacks.get(inputType); if (perInputStack != null && perInputStack.size() > 0) { return perInputStack.getFirst().mCallback; } return null; } private void onClientDeath(ClientInfoForDisplay client) { releaseInputEventCapture(client.mCallback, client.mTargetDisplayType); } /** dump for debugging */ public void dump(PrintWriter writer) { writer.println("**InputCaptureClientController**"); synchronized (mLock) { for (int display : SUPPORTED_DISPLAY_TYPES) { writer.println("***Display:" + display); HashMap allClientsForDisplay = mAllClients.get( display); if (allClientsForDisplay != null) { writer.println("****All clients:"); for (ClientInfoForDisplay client : allClientsForDisplay.values()) { writer.println(client); } } LinkedList fullCapturersStack = mFullDisplayEventCapturers.get(display); if (fullCapturersStack != null) { writer.println("****Full capture stack"); for (ClientInfoForDisplay client : fullCapturersStack) { writer.println(client); } } SparseArray> perInputStacks = mPerInputTypeCapturers.get(display); if (perInputStacks != null) { for (int i = 0; i < perInputStacks.size(); i++) { int inputType = perInputStacks.keyAt(i); LinkedList perInputStack = perInputStacks.valueAt(i); if (perInputStack.size() > 0) { writer.println("**** Per Input stack, input type:" + inputType); for (ClientInfoForDisplay client : perInputStack) { writer.println(client); } } } } } writer.println("mNumKeyEventsDispatched:" + mNumKeyEventsDispatched + ",mNumRotaryEventsDispatched:" + mNumRotaryEventsDispatched); } } private void dispatchClientCallbackLocked(ClientsToDispatch clientsToDispatch) { if (clientsToDispatch.mClientsToDispatch.isEmpty()) { return; } if (DBG_DISPATCH) { Slog.i(TAG, "dispatchClientCallbackLocked, number of clients:" + clientsToDispatch.mClientsToDispatch.size()); } mClientDispatchQueue.add(clientsToDispatch); CarServiceUtils.runOnMain(() -> { ClientsToDispatch clients; synchronized (mLock) { if (mClientDispatchQueue.isEmpty()) { return; } clients = mClientDispatchQueue.pop(); } if (DBG_DISPATCH) { Slog.i(TAG, "dispatching to clients, num of clients:" + clients.mClientsToDispatch.size() + ", display:" + clients.mDisplayType); } for (int i = 0; i < clients.mClientsToDispatch.size(); i++) { ICarInputCallback callback = clients.mClientsToDispatch.keyAt(i); int[] inputTypes = clients.mClientsToDispatch.valueAt(i); Arrays.sort(inputTypes); if (DBG_DISPATCH) { Slog.i(TAG, "dispatching to client, callback:" + callback + ", inputTypes:" + Arrays.toString(inputTypes)); } try { callback.onCaptureStateChanged(clients.mDisplayType, inputTypes); } catch (RemoteException e) { // Ignore. Let death handler deal with it. } } }); } private void dispatchKeyEvent(int targetDisplayType, KeyEvent event, ICarInputCallback callback) { CarServiceUtils.runOnMain(() -> { mKeyEventDispatchScratchList.clear(); mKeyEventDispatchScratchList.add(event); try { callback.onKeyEvents(targetDisplayType, mKeyEventDispatchScratchList); } catch (RemoteException e) { if (DBG_DISPATCH) { Slog.e(TAG, "Failed to dispatch KeyEvent " + event, e); } } }); } private void dispatchRotaryEvent(int targetDisplayType, RotaryEvent event, ICarInputCallback callback) { if (DBG_DISPATCH) { Slog.i(TAG, "dispatchRotaryEvent:" + event); } // TODO(b/159623196): Use HandlerThread for dispatching rather than relying on the main // thread. Change here and other dispatch methods. CarServiceUtils.runOnMain(() -> { mRotaryEventDispatchScratchList.clear(); mRotaryEventDispatchScratchList.add(event); try { callback.onRotaryEvents(targetDisplayType, mRotaryEventDispatchScratchList); } catch (RemoteException e) { if (DBG_DISPATCH) { Slog.e(TAG, "Failed to dispatch RotaryEvent " + event, e); } } }); } private void dispatchCustomInputEvent(@DisplayTypeEnum int targetDisplayType, CustomInputEvent event, ICarInputCallback callback) { if (DBG_DISPATCH) { Slog.d(TAG, "dispatchCustomInputEvent:" + event); } CarServiceUtils.runOnMain(() -> { mCustomInputEventDispatchScratchList.clear(); mCustomInputEventDispatchScratchList.add(event); try { callback.onCustomInputEvents(targetDisplayType, mCustomInputEventDispatchScratchList); } catch (RemoteException e) { if (DBG_DISPATCH) { Slog.e(TAG, "Failed to dispatch CustomInputEvent " + event, e); } } }); } private static void assertInputTypeValid(int[] inputTypes) { for (int inputType : inputTypes) { if (!VALID_INPUT_TYPES.contains(inputType)) { throw new IllegalArgumentException("Invalid input type:" + inputType + ", inputTypes:" + Arrays.toString(inputTypes)); } } } }