| /* |
| * 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.hardware.input.InputManager.INJECT_INPUT_EVENT_MODE_ASYNC; |
| import static android.service.voice.VoiceInteractionSession.SHOW_SOURCE_PUSH_TO_TALK; |
| |
| import android.annotation.Nullable; |
| import android.annotation.UserIdInt; |
| import android.app.ActivityManager; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothHeadsetClient; |
| import android.bluetooth.BluetoothProfile; |
| import android.car.CarOccupantZoneManager; |
| import android.car.CarProjectionManager; |
| 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; |
| 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.Bundle; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.UserHandle; |
| import android.provider.CallLog.Calls; |
| import android.provider.Settings; |
| import android.telecom.TelecomManager; |
| import android.text.TextUtils; |
| import android.util.IndentingPrintWriter; |
| import android.view.InputDevice; |
| import android.view.KeyEvent; |
| import android.view.ViewConfiguration; |
| |
| import com.android.car.hal.InputHalService; |
| import com.android.car.internal.common.UserHelperLite; |
| import com.android.car.user.CarUserService; |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.app.AssistUtils; |
| import com.android.internal.app.IVoiceInteractionSessionShowCallback; |
| import com.android.internal.os.BackgroundThread; |
| import com.android.server.utils.Slogf; |
| |
| import java.util.ArrayList; |
| 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 { |
| |
| private static final String TAG = CarLog.TAG_INPUT; |
| |
| /** An interface to receive {@link KeyEvent}s as they occur. */ |
| public interface KeyEventListener { |
| /** Called when a key event occurs. */ |
| void onKeyEvent(KeyEvent 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 IVoiceInteractionSessionShowCallback mShowCallback = |
| new IVoiceInteractionSessionShowCallback.Stub() { |
| @Override |
| public void onFailed() { |
| Slogf.w(TAG, "Failed to show VoiceInteractionSession"); |
| } |
| |
| @Override |
| public void onShown() { |
| Slogf.d(TAG, "IVoiceInteractionSessionShowCallback onShown()"); |
| } |
| }; |
| |
| @VisibleForTesting |
| static final String EXTRA_CAR_PUSH_TO_TALK = |
| "com.android.car.input.EXTRA_CAR_PUSH_TO_TALK"; |
| |
| private final Context mContext; |
| private final InputHalService mInputHalService; |
| private final CarUserService mUserService; |
| private final CarOccupantZoneService mCarOccupantZoneService; |
| private final TelecomManager mTelecomManager; |
| private final AssistUtils mAssistUtils; |
| |
| // The default handler for main-display input events. By default, injects the events into |
| // the input queue via InputManager, but can be overridden for testing. |
| private final KeyEventListener mMainDisplayHandler; |
| // The supplier for the last-called number. By default, gets the number from the call log. |
| // May be overridden for testing. |
| private final Supplier<String> 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; |
| |
| private final InputCaptureClientController mCaptureController; |
| |
| private final BluetoothAdapter mBluetoothAdapter; |
| |
| // BluetoothHeadsetClient set through mBluetoothProfileServiceListener, and used by |
| // launchBluetoothVoiceRecognition(). |
| @GuardedBy("mLock") |
| private BluetoothHeadsetClient mBluetoothHeadsetClient; |
| |
| private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener = |
| new BluetoothProfile.ServiceListener() { |
| @Override |
| public void onServiceConnected(int profile, BluetoothProfile proxy) { |
| if (profile == BluetoothProfile.HEADSET_CLIENT) { |
| Slogf.d(TAG, "Bluetooth proxy connected for HEADSET_CLIENT profile"); |
| synchronized (mLock) { |
| mBluetoothHeadsetClient = (BluetoothHeadsetClient) proxy; |
| } |
| } |
| } |
| |
| @Override |
| public void onServiceDisconnected(int profile) { |
| if (profile == BluetoothProfile.HEADSET_CLIENT) { |
| Slogf.d(TAG, "Bluetooth proxy disconnected for HEADSET_CLIENT profile"); |
| synchronized (mLock) { |
| mBluetoothHeadsetClient = null; |
| } |
| } |
| } |
| }; |
| |
| private final CarUserManager.UserLifecycleListener mUserLifecycleListener = event -> { |
| Slogf.d(TAG, "CarInputService.onEvent(%s)", event); |
| if (CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING == event.getEventType()) { |
| updateRotaryServiceSettings(event.getUserId()); |
| } |
| }; |
| |
| private static int getViewLongPressDelay(ContentResolver cr) { |
| return Settings.Secure.getIntForUser( |
| cr, |
| Settings.Secure.LONG_PRESS_TIMEOUT, |
| ViewConfiguration.getLongPressTimeout(), |
| UserHandle.USER_CURRENT); |
| } |
| |
| public CarInputService(Context context, InputHalService inputHalService, |
| CarUserService userService, CarOccupantZoneService occupantZoneService) { |
| this(context, inputHalService, userService, occupantZoneService, |
| new Handler(Looper.getMainLooper()), |
| context.getSystemService(TelecomManager.class), new AssistUtils(context), |
| event -> |
| context.getSystemService(InputManager.class) |
| .injectInputEvent(event, INJECT_INPUT_EVENT_MODE_ASYNC), |
| () -> Calls.getLastOutgoingCall(context), |
| () -> getViewLongPressDelay(context.getContentResolver()), |
| () -> context.getResources().getBoolean(R.bool.config_callButtonEndsOngoingCall), |
| new InputCaptureClientController(context), |
| BluetoothAdapter.getDefaultAdapter()); |
| } |
| |
| @VisibleForTesting |
| CarInputService(Context context, InputHalService inputHalService, CarUserService userService, |
| CarOccupantZoneService occupantZoneService, Handler handler, |
| TelecomManager telecomManager, AssistUtils assistUtils, |
| KeyEventListener mainDisplayHandler, |
| Supplier<String> lastCalledNumberSupplier, IntSupplier longPressDelaySupplier, |
| BooleanSupplier shouldCallButtonEndOngoingCallSupplier, |
| InputCaptureClientController captureController, BluetoothAdapter bluetoothAdapter) { |
| mContext = context; |
| mCaptureController = captureController; |
| mInputHalService = inputHalService; |
| mUserService = userService; |
| mCarOccupantZoneService = occupantZoneService; |
| mTelecomManager = telecomManager; |
| mAssistUtils = assistUtils; |
| mMainDisplayHandler = mainDisplayHandler; |
| mLastCalledNumberSupplier = lastCalledNumberSupplier; |
| mLongPressDelaySupplier = longPressDelaySupplier; |
| |
| mVoiceKeyTimer = |
| new KeyPressTimer( |
| handler, longPressDelaySupplier, this::handleVoiceAssistLongPress); |
| mCallKeyTimer = |
| new KeyPressTimer(handler, longPressDelaySupplier, this::handleCallLongPress); |
| |
| mRotaryServiceComponentName = mContext.getString(R.string.rotaryService); |
| mShouldCallButtonEndOngoingCallSupplier = shouldCallButtonEndOngoingCallSupplier; |
| mBluetoothAdapter = bluetoothAdapter; |
| } |
| |
| /** |
| * 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); |
| } |
| } |
| } |
| |
| /** |
| * 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); |
| if (mBluetoothAdapter != null) { |
| BackgroundThread.getHandler().post(() -> { |
| mBluetoothAdapter.getProfileProxy(mContext, |
| mBluetoothProfileServiceListener, BluetoothProfile.HEADSET_CLIENT); |
| }); |
| } |
| if (!TextUtils.isEmpty(mRotaryServiceComponentName)) { |
| mUserService.addUserLifecycleListener(mUserLifecycleListener); |
| } |
| } |
| |
| @Override |
| public void release() { |
| synchronized (mLock) { |
| mProjectionKeyEventHandler = null; |
| mProjectionKeyEventsSubscribed.clear(); |
| mInstrumentClusterKeyListener = null; |
| if (mBluetoothHeadsetClient != null) { |
| mBluetoothAdapter.closeProfileProxy( |
| BluetoothProfile.HEADSET_CLIENT, mBluetoothHeadsetClient); |
| mBluetoothHeadsetClient = null; |
| } |
| } |
| if (!TextUtils.isEmpty(mRotaryServiceComponentName)) { |
| mUserService.removeUserLifecycleListener(mUserLifecycleListener); |
| } |
| } |
| |
| @Override |
| public void onKeyEvent(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: |
| handleVoiceAssistKey(event); |
| 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; |
| } |
| mMainDisplayHandler.onKeyEvent(event); |
| } |
| |
| 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. |
| event.setDisplayId(newDisplayId); |
| } |
| |
| @Override |
| public void onRotaryEvent(RotaryEvent event, @DisplayTypeEnum int targetDisplay) { |
| if (!mCaptureController.onRotaryEvent(targetDisplay, event)) { |
| List<KeyEvent> 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<KeyEvent> 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<KeyEvent> 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. |
| * <p> |
| * The event's display id will be overridden 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); |
| } |
| } |
| |
| 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()) { |
| // 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; |
| } |
| |
| 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 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, null, UserHandle.CURRENT_OR_SELF); |
| } |
| |
| 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, null)) |
| .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| mContext.startActivityAsUser(callLastNumberIntent, null, UserHandle.CURRENT_OR_SELF); |
| } |
| } |
| |
| 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() { |
| synchronized (mLock) { |
| if (mBluetoothHeadsetClient == null || !isBluetoothVoiceRecognitionEnabled()) { |
| return false; |
| } |
| // getConnectedDevices() does not make any guarantees about the order of the returned |
| // list. As of 2019-02-26, this code is only triggered through a long-press of the |
| // voice recognition key, so handling of multiple connected devices that support voice |
| // recognition is not expected to be a primary use case. |
| List<BluetoothDevice> devices = mBluetoothHeadsetClient.getConnectedDevices(); |
| if (devices != null) { |
| for (BluetoothDevice device : devices) { |
| Bundle bundle = mBluetoothHeadsetClient.getCurrentAgFeatures(device); |
| if (bundle == null || !bundle.getBoolean( |
| BluetoothHeadsetClient.EXTRA_AG_FEATURE_VOICE_RECOGNITION)) { |
| continue; |
| } |
| if (mBluetoothHeadsetClient.startVoiceRecognition(device)) { |
| Slogf.d(TAG, "started voice recognition on BT device at (%s)", |
| device.getAddress()); |
| return true; |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| private void launchDefaultVoiceAssistantHandler() { |
| Slogf.i(TAG, "voice key, invoke AssistUtils"); |
| |
| if (mAssistUtils.getAssistComponentForUser(ActivityManager.getCurrentUser()) == null) { |
| Slogf.w(TAG, "Unable to retrieve assist component for current user"); |
| return; |
| } |
| |
| final Bundle args = new Bundle(); |
| args.putBoolean(EXTRA_CAR_PUSH_TO_TALK, true); |
| |
| mAssistUtils.showSessionForActiveService(args, |
| SHOW_SOURCE_PUSH_TO_TALK, mShowCallback, null /*activityToken*/); |
| } |
| |
| /** |
| * @return false if the KeyEvent isn't consumed because there is no |
| * InstrumentClusterKeyListener. |
| */ |
| private boolean handleInstrumentClusterKey(KeyEvent event) { |
| KeyEventListener listener = null; |
| synchronized (mLock) { |
| listener = mInstrumentClusterKeyListener; |
| } |
| if (listener == null) { |
| return false; |
| } |
| listener.onKeyEvent(event); |
| return true; |
| } |
| |
| @Override |
| 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); |
| } |
| |
| private void updateRotaryServiceSettings(@UserIdInt int userId) { |
| if (UserHelperLite.isHeadlessSystemUser(userId)) { |
| return; |
| } |
| ContentResolver contentResolver = mContext.getContentResolver(); |
| Settings.Secure.putStringForUser(contentResolver, |
| Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, |
| mRotaryServiceComponentName, |
| userId); |
| Settings.Secure.putStringForUser(contentResolver, |
| Settings.Secure.ACCESSIBILITY_ENABLED, |
| "1", |
| userId); |
| } |
| } |