blob: 5870b32e0bf28ab1cee0853ac3aad090ff1da6d8 [file] [log] [blame]
/*
* 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 static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
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.ExcludeFromCodeCoverageGeneratedReport;
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
@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);
}
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);
}
}