Loading services/core/java/com/android/server/input/InputFeatureFlagProvider.java 0 → 100644 +67 −0 Original line number Diff line number Diff line /* * Copyright 2023 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.server.input; import android.os.SystemProperties; import java.util.Optional; /** * A component of {@link InputManagerService} responsible for managing the input sysprop flags * * @hide */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") public final class InputFeatureFlagProvider { // To disable Keyboard backlight control via Framework, run: // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart) private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED = SystemProperties.getBoolean( "persist.input.keyboard.backlight_control.enabled", true); // To disable Framework controlled keyboard backlight animation run: // adb shell setprop persist.input.keyboard_backlight_animation.enabled false (requires restart) private static final boolean KEYBOARD_BACKLIGHT_ANIMATION_ENABLED = SystemProperties.getBoolean( "persist.input.keyboard.keyboard_backlight_animation.enabled", false); private static Optional<Boolean> sKeyboardBacklightControlOverride = Optional.empty(); private static Optional<Boolean> sKeyboardBacklightAnimationOverride = Optional.empty(); public static boolean isKeyboardBacklightControlEnabled() { return sKeyboardBacklightControlOverride.orElse(KEYBOARD_BACKLIGHT_CONTROL_ENABLED); } public static boolean isKeyboardBacklightAnimationEnabled() { return sKeyboardBacklightAnimationOverride.orElse(KEYBOARD_BACKLIGHT_ANIMATION_ENABLED); } public static void setKeyboardBacklightControlEnabled(boolean enabled) { sKeyboardBacklightControlOverride = Optional.of(enabled); } public static void setKeyboardBacklightAnimationEnabled(boolean enabled) { sKeyboardBacklightAnimationOverride = Optional.of(enabled); } /** * Clears all input feature flag overrides. */ public static void clearOverrides() { sKeyboardBacklightControlOverride = Optional.empty(); sKeyboardBacklightAnimationOverride = Optional.empty(); } } services/core/java/com/android/server/input/InputManagerService.java +3 −10 Original line number Diff line number Diff line Loading @@ -72,7 +72,6 @@ import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; import android.os.SystemProperties; import android.os.UserHandle; import android.os.VibrationEffect; import android.os.vibrator.StepSegment; Loading Loading @@ -157,11 +156,6 @@ public class InputManagerService extends IInputManager.Stub private static final AdditionalDisplayInputProperties DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES = new AdditionalDisplayInputProperties(); // To disable Keyboard backlight control via Framework, run: // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart) private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED = SystemProperties.getBoolean( "persist.input.keyboard.backlight_control.enabled", true); private final NativeInputManagerService mNative; private final Context mContext; Loading Loading @@ -431,10 +425,9 @@ public class InputManagerService extends IInputManager.Stub mKeyboardLayoutManager = new KeyboardLayoutManager(mContext, mNative, mDataStore, injector.getLooper()); mBatteryController = new BatteryController(mContext, mNative, injector.getLooper()); mKeyboardBacklightController = KEYBOARD_BACKLIGHT_CONTROL_ENABLED ? new KeyboardBacklightController(mContext, mNative, mDataStore, injector.getLooper()) : new KeyboardBacklightControllerInterface() {}; mKeyboardBacklightController = InputFeatureFlagProvider.isKeyboardBacklightControlEnabled() ? new KeyboardBacklightController(mContext, mNative, mDataStore, injector.getLooper()) : new KeyboardBacklightControllerInterface() {}; mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper()); mUseDevInputEventForAudioJack = Loading services/core/java/com/android/server/input/KeyboardBacklightController.java +81 −36 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.server.input; import android.animation.ValueAnimator; import android.annotation.BinderThread; import android.content.Context; import android.graphics.Color; Loading Loading @@ -70,6 +71,8 @@ final class KeyboardBacklightController implements private static final int MSG_INTERACTIVE_STATE_CHANGED = 6; private static final int MAX_BRIGHTNESS = 255; private static final int NUM_BRIGHTNESS_CHANGE_STEPS = 10; private static final long TRANSITION_ANIMATION_DURATION_MILLIS = Duration.ofSeconds( 1).toMillis(); private static final String UEVENT_KEYBOARD_BACKLIGHT_TAG = "kbd_backlight"; Loading @@ -85,6 +88,7 @@ final class KeyboardBacklightController implements @GuardedBy("mDataStore") private final PersistentDataStore mDataStore; private final Handler mHandler; private final AnimatorFactory mAnimatorFactory; // Always access on handler thread or need to lock this for synchronization. private final SparseArray<KeyboardBacklightState> mKeyboardBacklights = new SparseArray<>(1); // Maintains state if all backlights should be on or turned off Loading @@ -109,10 +113,17 @@ final class KeyboardBacklightController implements KeyboardBacklightController(Context context, NativeInputManagerService nativeService, PersistentDataStore dataStore, Looper looper) { this(context, nativeService, dataStore, looper, ValueAnimator::ofInt); } @VisibleForTesting KeyboardBacklightController(Context context, NativeInputManagerService nativeService, PersistentDataStore dataStore, Looper looper, AnimatorFactory animatorFactory) { mContext = context; mNative = nativeService; mDataStore = dataStore; mHandler = new Handler(looper, this::handleMessage); mAnimatorFactory = animatorFactory; } @Override Loading Loading @@ -177,8 +188,7 @@ final class KeyboardBacklightController implements } else { newBrightnessLevel = Math.max(currBrightnessLevel - 1, 0); } updateBacklightState(deviceId, keyboardBacklight, newBrightnessLevel, true /* isTriggeredByKeyPress */); updateBacklightState(deviceId, newBrightnessLevel, true /* isTriggeredByKeyPress */); synchronized (mDataStore) { try { Loading @@ -203,8 +213,7 @@ final class KeyboardBacklightController implements if (index < 0) { index = Math.min(NUM_BRIGHTNESS_CHANGE_STEPS, -(index + 1)); } updateBacklightState(inputDevice.getId(), keyboardBacklight, index, false /* isTriggeredByKeyPress */); updateBacklightState(inputDevice.getId(), index, false /* isTriggeredByKeyPress */); if (DEBUG) { Slog.d(TAG, "Restoring brightness level " + brightness.getAsInt()); } Loading @@ -217,14 +226,10 @@ final class KeyboardBacklightController implements if (!mIsInteractive) { return; } if (!mIsBacklightOn) { mIsBacklightOn = true; for (int i = 0; i < mKeyboardBacklights.size(); i++) { int deviceId = mKeyboardBacklights.keyAt(i); KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel, false /* isTriggeredByKeyPress */); } state.onBacklightStateChanged(); } mHandler.removeMessages(MSG_NOTIFY_USER_INACTIVITY); mHandler.sendEmptyMessageAtTime(MSG_NOTIFY_USER_INACTIVITY, Loading @@ -232,14 +237,10 @@ final class KeyboardBacklightController implements } private void handleUserInactivity() { if (mIsBacklightOn) { mIsBacklightOn = false; for (int i = 0; i < mKeyboardBacklights.size(); i++) { int deviceId = mKeyboardBacklights.keyAt(i); KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel, false /* isTriggeredByKeyPress */); } state.onBacklightStateChanged(); } } Loading Loading @@ -310,7 +311,7 @@ final class KeyboardBacklightController implements return; } // The keyboard backlight was added or changed. mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(keyboardBacklight)); mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(deviceId, keyboardBacklight)); restoreBacklightBrightness(inputDevice, keyboardBacklight); } Loading Loading @@ -372,21 +373,14 @@ final class KeyboardBacklightController implements } } private void updateBacklightState(int deviceId, Light light, int brightnessLevel, private void updateBacklightState(int deviceId, int brightnessLevel, boolean isTriggeredByKeyPress) { KeyboardBacklightState state = mKeyboardBacklights.get(deviceId); if (state == null) { return; } mNative.setLightColor(deviceId, light.getId(), mIsBacklightOn ? Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[brightnessLevel], 0, 0, 0) : 0); if (DEBUG) { Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel + "(isBacklightOn = " + mIsBacklightOn + ")"); } state.mBrightnessLevel = brightnessLevel; state.setBrightnessLevel(brightnessLevel); synchronized (mKeyboardBacklightListenerRecords) { for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) { Loading @@ -397,6 +391,10 @@ final class KeyboardBacklightController implements deviceId, callbackState, isTriggeredByKeyPress); } } if (DEBUG) { Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel); } } private void onKeyboardBacklightListenerDied(int pid) { Loading Loading @@ -436,10 +434,7 @@ final class KeyboardBacklightController implements @Override public void dump(PrintWriter pw) { IndentingPrintWriter ipw = new IndentingPrintWriter(pw); ipw.println( TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights, isBacklightOn = " + mIsBacklightOn); ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights"); ipw.increaseIndent(); for (int i = 0; i < mKeyboardBacklights.size(); i++) { KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); Loading @@ -448,6 +443,10 @@ final class KeyboardBacklightController implements ipw.decreaseIndent(); } private static boolean isAnimationEnabled() { return InputFeatureFlagProvider.isKeyboardBacklightAnimationEnabled(); } // A record of a registered Keyboard backlight listener from one process. private class KeyboardBacklightListenerRecord implements IBinder.DeathRecipient { public final int mPid; Loading Loading @@ -478,14 +477,55 @@ final class KeyboardBacklightController implements } } private static class KeyboardBacklightState { private class KeyboardBacklightState { private final int mDeviceId; private final Light mLight; private int mBrightnessLevel; private ValueAnimator mAnimator; KeyboardBacklightState(Light light) { KeyboardBacklightState(int deviceId, Light light) { mDeviceId = deviceId; mLight = light; } private void onBacklightStateChanged() { setBacklightValue(mIsBacklightOn ? BRIGHTNESS_VALUE_FOR_LEVEL[mBrightnessLevel] : 0); } private void setBrightnessLevel(int brightnessLevel) { if (mIsBacklightOn) { setBacklightValue(BRIGHTNESS_VALUE_FOR_LEVEL[brightnessLevel]); } mBrightnessLevel = brightnessLevel; } private void cancelAnimation() { if (mAnimator != null && mAnimator.isRunning()) { mAnimator.cancel(); } } private void setBacklightValue(int toValue) { int fromValue = Color.alpha(mNative.getLightColor(mDeviceId, mLight.getId())); if (fromValue == toValue) { return; } if (isAnimationEnabled()) { startAnimation(fromValue, toValue); } else { mNative.setLightColor(mDeviceId, mLight.getId(), Color.argb(toValue, 0, 0, 0)); } } private void startAnimation(int fromValue, int toValue) { // Cancel any ongoing animation before starting a new one cancelAnimation(); mAnimator = mAnimatorFactory.makeIntAnimator(fromValue, toValue); mAnimator.addUpdateListener( (animation) -> mNative.setLightColor(mDeviceId, mLight.getId(), Color.argb((int) animation.getAnimatedValue(), 0, 0, 0))); mAnimator.setDuration(TRANSITION_ANIMATION_DURATION_MILLIS).start(); } @Override public String toString() { return "KeyboardBacklightState{Light=" + mLight.getId() Loading @@ -493,4 +533,9 @@ final class KeyboardBacklightController implements + "}"; } } @VisibleForTesting interface AnimatorFactory { ValueAnimator makeIntAnimator(int from, int to); } } services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt +265 −199 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.server.input import android.animation.ValueAnimator import android.content.Context import android.content.ContextWrapper import android.graphics.Color Loading @@ -29,6 +30,7 @@ import android.os.UEventObserver import android.os.test.TestLooper import android.platform.test.annotations.Presubmit import android.view.InputDevice import androidx.test.annotation.UiThreadTest import androidx.test.core.app.ApplicationProvider import com.android.server.input.KeyboardBacklightController.BRIGHTNESS_VALUE_FOR_LEVEL import com.android.server.input.KeyboardBacklightController.USER_INACTIVITY_THRESHOLD_MILLIS Loading Loading @@ -96,9 +98,11 @@ class KeyboardBacklightControllerTests { private lateinit var context: Context private lateinit var dataStore: PersistentDataStore private lateinit var testLooper: TestLooper private val totalLevels = BRIGHTNESS_VALUE_FOR_LEVEL.size private var lightColorMap: HashMap<Int, Int> = HashMap() private var lastBacklightState: KeyboardBacklightState? = null private var sysfsNodeChanges = 0 private var lastAnimationValues = IntArray(2) @Before fun setup() { Loading @@ -115,8 +119,8 @@ class KeyboardBacklightControllerTests { override fun finishWrite(fos: FileOutputStream?, success: Boolean) {} }) testLooper = TestLooper() keyboardBacklightController = KeyboardBacklightController(context, native, dataStore, testLooper.looper) keyboardBacklightController = KeyboardBacklightController(context, native, dataStore, testLooper.looper, FakeAnimatorFactory()) InputManagerGlobal.resetInstance(iInputManager) val inputManager = InputManager(context) `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager) Loading @@ -125,6 +129,10 @@ class KeyboardBacklightControllerTests { val args = it.arguments lightColorMap.put(args[1] as Int, args[2] as Int) } `when`(native.getLightColor(anyInt(), anyInt())).thenAnswer { val args = it.arguments lightColorMap.getOrDefault(args[1] as Int, 0) } lightColorMap.clear() `when`(native.sysfsNodeChanged(any())).then { sysfsNodeChanges++ Loading @@ -138,13 +146,14 @@ class KeyboardBacklightControllerTests { @Test fun testKeyboardBacklightIncrementDecrement() { BacklightAnimationFlag(false).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) { for (level in 1 until totalLevels) { incrementKeyboardBacklight(DEVICE_ID) assertEquals( "Light value for level $level mismatched", Loading Loading @@ -177,7 +186,7 @@ class KeyboardBacklightControllerTests { ).asInt ) for (level in BRIGHTNESS_VALUE_FOR_LEVEL.size - 2 downTo 0) { for (level in totalLevels - 2 downTo 0) { decrementKeyboardBacklight(DEVICE_ID) assertEquals( "Light value for level $level mismatched", Loading Loading @@ -210,9 +219,11 @@ class KeyboardBacklightControllerTests { ).asInt ) } } @Test fun testKeyboardWithoutBacklight() { BacklightAnimationFlag(false).use { val keyboardWithoutBacklight = createKeyboard(DEVICE_ID) val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight) Loading @@ -222,9 +233,11 @@ class KeyboardBacklightControllerTests { incrementKeyboardBacklight(DEVICE_ID) assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty()) } } @Test fun testKeyboardWithMultipleLight() { BacklightAnimationFlag(false).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT) Loading @@ -242,15 +255,17 @@ class KeyboardBacklightControllerTests { assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID]) assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID]) } } @Test fun testRestoreBacklightOnInputDeviceAdded() { BacklightAnimationFlag(false).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) { for (level in 1 until totalLevels) { dataStore.setKeyboardBacklightBrightness( keyboardWithBacklight.descriptor, LIGHT_ID, Loading @@ -261,17 +276,19 @@ class KeyboardBacklightControllerTests { keyboardBacklightController.notifyUserActivity() testLooper.dispatchNext() assertEquals( "Keyboard backlight level should be restored to the level saved in the data " + "store", "Keyboard backlight level should be restored to the level saved in the " + "data store", Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), lightColorMap[LIGHT_ID] ) keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID) } } } @Test fun testRestoreBacklightOnInputDeviceChanged() { BacklightAnimationFlag(false).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) Loading Loading @@ -299,9 +316,11 @@ class KeyboardBacklightControllerTests { lightColorMap[LIGHT_ID] ) } } @Test fun testKeyboardBacklight_registerUnregisterListener() { BacklightAnimationFlag(false).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) Loading @@ -327,8 +346,8 @@ class KeyboardBacklightControllerTests { lastBacklightState!!.brightnessLevel ) assertEquals( "Backlight state maxBrightnessLevel should be " + (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1), (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1), "Backlight state maxBrightnessLevel should be " + (totalLevels - 1), (totalLevels - 1), lastBacklightState!!.maxBrightnessLevel ) assertEquals( Loading @@ -345,9 +364,11 @@ class KeyboardBacklightControllerTests { assertNull("Listener should not receive any updates", lastBacklightState) } } @Test fun testKeyboardBacklight_userActivity() { BacklightAnimationFlag(false).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) Loading Loading @@ -375,9 +396,11 @@ class KeyboardBacklightControllerTests { lightColorMap[LIGHT_ID] ) } } @Test fun testKeyboardBacklight_displayOnOff() { BacklightAnimationFlag(false).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) Loading @@ -404,6 +427,7 @@ class KeyboardBacklightControllerTests { lightColorMap[LIGHT_ID] ) } } @Test fun testKeyboardBacklightSysfsNodeAdded_AfterInputDeviceAdded() { Loading Loading @@ -463,6 +487,30 @@ class KeyboardBacklightControllerTests { ) } @Test @UiThreadTest fun testKeyboardBacklightAnimation_onChangeLevels() { BacklightAnimationFlag(true).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) incrementKeyboardBacklight(DEVICE_ID) assertEquals( "Should start animation from level 0", BRIGHTNESS_VALUE_FOR_LEVEL[0], lastAnimationValues[0] ) assertEquals( "Should start animation to level 1", BRIGHTNESS_VALUE_FOR_LEVEL[1], lastAnimationValues[1] ) } } inner class KeyboardBacklightListener : IKeyboardBacklightListener.Stub() { override fun onBrightnessChanged( deviceId: Int, Loading Loading @@ -496,4 +544,22 @@ class KeyboardBacklightControllerTests { val maxBrightnessLevel: Int, val isTriggeredByKeyPress: Boolean ) private inner class BacklightAnimationFlag constructor(enabled: Boolean) : AutoCloseable { init { InputFeatureFlagProvider.setKeyboardBacklightAnimationEnabled(enabled) } override fun close() { InputFeatureFlagProvider.clearOverrides() } } private inner class FakeAnimatorFactory : KeyboardBacklightController.AnimatorFactory { override fun makeIntAnimator(from: Int, to: Int): ValueAnimator { lastAnimationValues[0] = from lastAnimationValues[1] = to return ValueAnimator.ofInt(from, to) } } } Loading
services/core/java/com/android/server/input/InputFeatureFlagProvider.java 0 → 100644 +67 −0 Original line number Diff line number Diff line /* * Copyright 2023 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.server.input; import android.os.SystemProperties; import java.util.Optional; /** * A component of {@link InputManagerService} responsible for managing the input sysprop flags * * @hide */ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") public final class InputFeatureFlagProvider { // To disable Keyboard backlight control via Framework, run: // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart) private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED = SystemProperties.getBoolean( "persist.input.keyboard.backlight_control.enabled", true); // To disable Framework controlled keyboard backlight animation run: // adb shell setprop persist.input.keyboard_backlight_animation.enabled false (requires restart) private static final boolean KEYBOARD_BACKLIGHT_ANIMATION_ENABLED = SystemProperties.getBoolean( "persist.input.keyboard.keyboard_backlight_animation.enabled", false); private static Optional<Boolean> sKeyboardBacklightControlOverride = Optional.empty(); private static Optional<Boolean> sKeyboardBacklightAnimationOverride = Optional.empty(); public static boolean isKeyboardBacklightControlEnabled() { return sKeyboardBacklightControlOverride.orElse(KEYBOARD_BACKLIGHT_CONTROL_ENABLED); } public static boolean isKeyboardBacklightAnimationEnabled() { return sKeyboardBacklightAnimationOverride.orElse(KEYBOARD_BACKLIGHT_ANIMATION_ENABLED); } public static void setKeyboardBacklightControlEnabled(boolean enabled) { sKeyboardBacklightControlOverride = Optional.of(enabled); } public static void setKeyboardBacklightAnimationEnabled(boolean enabled) { sKeyboardBacklightAnimationOverride = Optional.of(enabled); } /** * Clears all input feature flag overrides. */ public static void clearOverrides() { sKeyboardBacklightControlOverride = Optional.empty(); sKeyboardBacklightAnimationOverride = Optional.empty(); } }
services/core/java/com/android/server/input/InputManagerService.java +3 −10 Original line number Diff line number Diff line Loading @@ -72,7 +72,6 @@ import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; import android.os.SystemProperties; import android.os.UserHandle; import android.os.VibrationEffect; import android.os.vibrator.StepSegment; Loading Loading @@ -157,11 +156,6 @@ public class InputManagerService extends IInputManager.Stub private static final AdditionalDisplayInputProperties DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES = new AdditionalDisplayInputProperties(); // To disable Keyboard backlight control via Framework, run: // 'adb shell setprop persist.input.keyboard_backlight_control.enabled false' (requires restart) private static final boolean KEYBOARD_BACKLIGHT_CONTROL_ENABLED = SystemProperties.getBoolean( "persist.input.keyboard.backlight_control.enabled", true); private final NativeInputManagerService mNative; private final Context mContext; Loading Loading @@ -431,10 +425,9 @@ public class InputManagerService extends IInputManager.Stub mKeyboardLayoutManager = new KeyboardLayoutManager(mContext, mNative, mDataStore, injector.getLooper()); mBatteryController = new BatteryController(mContext, mNative, injector.getLooper()); mKeyboardBacklightController = KEYBOARD_BACKLIGHT_CONTROL_ENABLED ? new KeyboardBacklightController(mContext, mNative, mDataStore, injector.getLooper()) : new KeyboardBacklightControllerInterface() {}; mKeyboardBacklightController = InputFeatureFlagProvider.isKeyboardBacklightControlEnabled() ? new KeyboardBacklightController(mContext, mNative, mDataStore, injector.getLooper()) : new KeyboardBacklightControllerInterface() {}; mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper()); mUseDevInputEventForAudioJack = Loading
services/core/java/com/android/server/input/KeyboardBacklightController.java +81 −36 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.server.input; import android.animation.ValueAnimator; import android.annotation.BinderThread; import android.content.Context; import android.graphics.Color; Loading Loading @@ -70,6 +71,8 @@ final class KeyboardBacklightController implements private static final int MSG_INTERACTIVE_STATE_CHANGED = 6; private static final int MAX_BRIGHTNESS = 255; private static final int NUM_BRIGHTNESS_CHANGE_STEPS = 10; private static final long TRANSITION_ANIMATION_DURATION_MILLIS = Duration.ofSeconds( 1).toMillis(); private static final String UEVENT_KEYBOARD_BACKLIGHT_TAG = "kbd_backlight"; Loading @@ -85,6 +88,7 @@ final class KeyboardBacklightController implements @GuardedBy("mDataStore") private final PersistentDataStore mDataStore; private final Handler mHandler; private final AnimatorFactory mAnimatorFactory; // Always access on handler thread or need to lock this for synchronization. private final SparseArray<KeyboardBacklightState> mKeyboardBacklights = new SparseArray<>(1); // Maintains state if all backlights should be on or turned off Loading @@ -109,10 +113,17 @@ final class KeyboardBacklightController implements KeyboardBacklightController(Context context, NativeInputManagerService nativeService, PersistentDataStore dataStore, Looper looper) { this(context, nativeService, dataStore, looper, ValueAnimator::ofInt); } @VisibleForTesting KeyboardBacklightController(Context context, NativeInputManagerService nativeService, PersistentDataStore dataStore, Looper looper, AnimatorFactory animatorFactory) { mContext = context; mNative = nativeService; mDataStore = dataStore; mHandler = new Handler(looper, this::handleMessage); mAnimatorFactory = animatorFactory; } @Override Loading Loading @@ -177,8 +188,7 @@ final class KeyboardBacklightController implements } else { newBrightnessLevel = Math.max(currBrightnessLevel - 1, 0); } updateBacklightState(deviceId, keyboardBacklight, newBrightnessLevel, true /* isTriggeredByKeyPress */); updateBacklightState(deviceId, newBrightnessLevel, true /* isTriggeredByKeyPress */); synchronized (mDataStore) { try { Loading @@ -203,8 +213,7 @@ final class KeyboardBacklightController implements if (index < 0) { index = Math.min(NUM_BRIGHTNESS_CHANGE_STEPS, -(index + 1)); } updateBacklightState(inputDevice.getId(), keyboardBacklight, index, false /* isTriggeredByKeyPress */); updateBacklightState(inputDevice.getId(), index, false /* isTriggeredByKeyPress */); if (DEBUG) { Slog.d(TAG, "Restoring brightness level " + brightness.getAsInt()); } Loading @@ -217,14 +226,10 @@ final class KeyboardBacklightController implements if (!mIsInteractive) { return; } if (!mIsBacklightOn) { mIsBacklightOn = true; for (int i = 0; i < mKeyboardBacklights.size(); i++) { int deviceId = mKeyboardBacklights.keyAt(i); KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel, false /* isTriggeredByKeyPress */); } state.onBacklightStateChanged(); } mHandler.removeMessages(MSG_NOTIFY_USER_INACTIVITY); mHandler.sendEmptyMessageAtTime(MSG_NOTIFY_USER_INACTIVITY, Loading @@ -232,14 +237,10 @@ final class KeyboardBacklightController implements } private void handleUserInactivity() { if (mIsBacklightOn) { mIsBacklightOn = false; for (int i = 0; i < mKeyboardBacklights.size(); i++) { int deviceId = mKeyboardBacklights.keyAt(i); KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); updateBacklightState(deviceId, state.mLight, state.mBrightnessLevel, false /* isTriggeredByKeyPress */); } state.onBacklightStateChanged(); } } Loading Loading @@ -310,7 +311,7 @@ final class KeyboardBacklightController implements return; } // The keyboard backlight was added or changed. mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(keyboardBacklight)); mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(deviceId, keyboardBacklight)); restoreBacklightBrightness(inputDevice, keyboardBacklight); } Loading Loading @@ -372,21 +373,14 @@ final class KeyboardBacklightController implements } } private void updateBacklightState(int deviceId, Light light, int brightnessLevel, private void updateBacklightState(int deviceId, int brightnessLevel, boolean isTriggeredByKeyPress) { KeyboardBacklightState state = mKeyboardBacklights.get(deviceId); if (state == null) { return; } mNative.setLightColor(deviceId, light.getId(), mIsBacklightOn ? Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[brightnessLevel], 0, 0, 0) : 0); if (DEBUG) { Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel + "(isBacklightOn = " + mIsBacklightOn + ")"); } state.mBrightnessLevel = brightnessLevel; state.setBrightnessLevel(brightnessLevel); synchronized (mKeyboardBacklightListenerRecords) { for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) { Loading @@ -397,6 +391,10 @@ final class KeyboardBacklightController implements deviceId, callbackState, isTriggeredByKeyPress); } } if (DEBUG) { Slog.d(TAG, "Changing state from " + state.mBrightnessLevel + " to " + brightnessLevel); } } private void onKeyboardBacklightListenerDied(int pid) { Loading Loading @@ -436,10 +434,7 @@ final class KeyboardBacklightController implements @Override public void dump(PrintWriter pw) { IndentingPrintWriter ipw = new IndentingPrintWriter(pw); ipw.println( TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights, isBacklightOn = " + mIsBacklightOn); ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights"); ipw.increaseIndent(); for (int i = 0; i < mKeyboardBacklights.size(); i++) { KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); Loading @@ -448,6 +443,10 @@ final class KeyboardBacklightController implements ipw.decreaseIndent(); } private static boolean isAnimationEnabled() { return InputFeatureFlagProvider.isKeyboardBacklightAnimationEnabled(); } // A record of a registered Keyboard backlight listener from one process. private class KeyboardBacklightListenerRecord implements IBinder.DeathRecipient { public final int mPid; Loading Loading @@ -478,14 +477,55 @@ final class KeyboardBacklightController implements } } private static class KeyboardBacklightState { private class KeyboardBacklightState { private final int mDeviceId; private final Light mLight; private int mBrightnessLevel; private ValueAnimator mAnimator; KeyboardBacklightState(Light light) { KeyboardBacklightState(int deviceId, Light light) { mDeviceId = deviceId; mLight = light; } private void onBacklightStateChanged() { setBacklightValue(mIsBacklightOn ? BRIGHTNESS_VALUE_FOR_LEVEL[mBrightnessLevel] : 0); } private void setBrightnessLevel(int brightnessLevel) { if (mIsBacklightOn) { setBacklightValue(BRIGHTNESS_VALUE_FOR_LEVEL[brightnessLevel]); } mBrightnessLevel = brightnessLevel; } private void cancelAnimation() { if (mAnimator != null && mAnimator.isRunning()) { mAnimator.cancel(); } } private void setBacklightValue(int toValue) { int fromValue = Color.alpha(mNative.getLightColor(mDeviceId, mLight.getId())); if (fromValue == toValue) { return; } if (isAnimationEnabled()) { startAnimation(fromValue, toValue); } else { mNative.setLightColor(mDeviceId, mLight.getId(), Color.argb(toValue, 0, 0, 0)); } } private void startAnimation(int fromValue, int toValue) { // Cancel any ongoing animation before starting a new one cancelAnimation(); mAnimator = mAnimatorFactory.makeIntAnimator(fromValue, toValue); mAnimator.addUpdateListener( (animation) -> mNative.setLightColor(mDeviceId, mLight.getId(), Color.argb((int) animation.getAnimatedValue(), 0, 0, 0))); mAnimator.setDuration(TRANSITION_ANIMATION_DURATION_MILLIS).start(); } @Override public String toString() { return "KeyboardBacklightState{Light=" + mLight.getId() Loading @@ -493,4 +533,9 @@ final class KeyboardBacklightController implements + "}"; } } @VisibleForTesting interface AnimatorFactory { ValueAnimator makeIntAnimator(int from, int to); } }
services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt +265 −199 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.server.input import android.animation.ValueAnimator import android.content.Context import android.content.ContextWrapper import android.graphics.Color Loading @@ -29,6 +30,7 @@ import android.os.UEventObserver import android.os.test.TestLooper import android.platform.test.annotations.Presubmit import android.view.InputDevice import androidx.test.annotation.UiThreadTest import androidx.test.core.app.ApplicationProvider import com.android.server.input.KeyboardBacklightController.BRIGHTNESS_VALUE_FOR_LEVEL import com.android.server.input.KeyboardBacklightController.USER_INACTIVITY_THRESHOLD_MILLIS Loading Loading @@ -96,9 +98,11 @@ class KeyboardBacklightControllerTests { private lateinit var context: Context private lateinit var dataStore: PersistentDataStore private lateinit var testLooper: TestLooper private val totalLevels = BRIGHTNESS_VALUE_FOR_LEVEL.size private var lightColorMap: HashMap<Int, Int> = HashMap() private var lastBacklightState: KeyboardBacklightState? = null private var sysfsNodeChanges = 0 private var lastAnimationValues = IntArray(2) @Before fun setup() { Loading @@ -115,8 +119,8 @@ class KeyboardBacklightControllerTests { override fun finishWrite(fos: FileOutputStream?, success: Boolean) {} }) testLooper = TestLooper() keyboardBacklightController = KeyboardBacklightController(context, native, dataStore, testLooper.looper) keyboardBacklightController = KeyboardBacklightController(context, native, dataStore, testLooper.looper, FakeAnimatorFactory()) InputManagerGlobal.resetInstance(iInputManager) val inputManager = InputManager(context) `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager) Loading @@ -125,6 +129,10 @@ class KeyboardBacklightControllerTests { val args = it.arguments lightColorMap.put(args[1] as Int, args[2] as Int) } `when`(native.getLightColor(anyInt(), anyInt())).thenAnswer { val args = it.arguments lightColorMap.getOrDefault(args[1] as Int, 0) } lightColorMap.clear() `when`(native.sysfsNodeChanged(any())).then { sysfsNodeChanges++ Loading @@ -138,13 +146,14 @@ class KeyboardBacklightControllerTests { @Test fun testKeyboardBacklightIncrementDecrement() { BacklightAnimationFlag(false).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) { for (level in 1 until totalLevels) { incrementKeyboardBacklight(DEVICE_ID) assertEquals( "Light value for level $level mismatched", Loading Loading @@ -177,7 +186,7 @@ class KeyboardBacklightControllerTests { ).asInt ) for (level in BRIGHTNESS_VALUE_FOR_LEVEL.size - 2 downTo 0) { for (level in totalLevels - 2 downTo 0) { decrementKeyboardBacklight(DEVICE_ID) assertEquals( "Light value for level $level mismatched", Loading Loading @@ -210,9 +219,11 @@ class KeyboardBacklightControllerTests { ).asInt ) } } @Test fun testKeyboardWithoutBacklight() { BacklightAnimationFlag(false).use { val keyboardWithoutBacklight = createKeyboard(DEVICE_ID) val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight) Loading @@ -222,9 +233,11 @@ class KeyboardBacklightControllerTests { incrementKeyboardBacklight(DEVICE_ID) assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty()) } } @Test fun testKeyboardWithMultipleLight() { BacklightAnimationFlag(false).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) val keyboardInputLight = createLight(SECOND_LIGHT_ID, Light.LIGHT_TYPE_INPUT) Loading @@ -242,15 +255,17 @@ class KeyboardBacklightControllerTests { assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID]) assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID]) } } @Test fun testRestoreBacklightOnInputDeviceAdded() { BacklightAnimationFlag(false).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) { for (level in 1 until totalLevels) { dataStore.setKeyboardBacklightBrightness( keyboardWithBacklight.descriptor, LIGHT_ID, Loading @@ -261,17 +276,19 @@ class KeyboardBacklightControllerTests { keyboardBacklightController.notifyUserActivity() testLooper.dispatchNext() assertEquals( "Keyboard backlight level should be restored to the level saved in the data " + "store", "Keyboard backlight level should be restored to the level saved in the " + "data store", Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0), lightColorMap[LIGHT_ID] ) keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID) } } } @Test fun testRestoreBacklightOnInputDeviceChanged() { BacklightAnimationFlag(false).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) Loading Loading @@ -299,9 +316,11 @@ class KeyboardBacklightControllerTests { lightColorMap[LIGHT_ID] ) } } @Test fun testKeyboardBacklight_registerUnregisterListener() { BacklightAnimationFlag(false).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) Loading @@ -327,8 +346,8 @@ class KeyboardBacklightControllerTests { lastBacklightState!!.brightnessLevel ) assertEquals( "Backlight state maxBrightnessLevel should be " + (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1), (BRIGHTNESS_VALUE_FOR_LEVEL.size - 1), "Backlight state maxBrightnessLevel should be " + (totalLevels - 1), (totalLevels - 1), lastBacklightState!!.maxBrightnessLevel ) assertEquals( Loading @@ -345,9 +364,11 @@ class KeyboardBacklightControllerTests { assertNull("Listener should not receive any updates", lastBacklightState) } } @Test fun testKeyboardBacklight_userActivity() { BacklightAnimationFlag(false).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) Loading Loading @@ -375,9 +396,11 @@ class KeyboardBacklightControllerTests { lightColorMap[LIGHT_ID] ) } } @Test fun testKeyboardBacklight_displayOnOff() { BacklightAnimationFlag(false).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) Loading @@ -404,6 +427,7 @@ class KeyboardBacklightControllerTests { lightColorMap[LIGHT_ID] ) } } @Test fun testKeyboardBacklightSysfsNodeAdded_AfterInputDeviceAdded() { Loading Loading @@ -463,6 +487,30 @@ class KeyboardBacklightControllerTests { ) } @Test @UiThreadTest fun testKeyboardBacklightAnimation_onChangeLevels() { BacklightAnimationFlag(true).use { val keyboardWithBacklight = createKeyboard(DEVICE_ID) val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) incrementKeyboardBacklight(DEVICE_ID) assertEquals( "Should start animation from level 0", BRIGHTNESS_VALUE_FOR_LEVEL[0], lastAnimationValues[0] ) assertEquals( "Should start animation to level 1", BRIGHTNESS_VALUE_FOR_LEVEL[1], lastAnimationValues[1] ) } } inner class KeyboardBacklightListener : IKeyboardBacklightListener.Stub() { override fun onBrightnessChanged( deviceId: Int, Loading Loading @@ -496,4 +544,22 @@ class KeyboardBacklightControllerTests { val maxBrightnessLevel: Int, val isTriggeredByKeyPress: Boolean ) private inner class BacklightAnimationFlag constructor(enabled: Boolean) : AutoCloseable { init { InputFeatureFlagProvider.setKeyboardBacklightAnimationEnabled(enabled) } override fun close() { InputFeatureFlagProvider.clearOverrides() } } private inner class FakeAnimatorFactory : KeyboardBacklightController.AnimatorFactory { override fun makeIntAnimator(from: Int, to: Int): ValueAnimator { lastAnimationValues[0] = from lastAnimationValues[1] = to return ValueAnimator.ofInt(from, to) } } }