diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index 36099b0261c24..7eb5a1013f698 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -140,4 +140,16 @@ public abstract class InputManagerInternal { * canceled for all other channels. */ public abstract void pilferPointers(IBinder token); + + /** + * Increments keyboard backlight level if the device has an associated keyboard backlight + * {@see Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT} + */ + public abstract void incrementKeyboardBacklight(int deviceId); + + /** + * Decrements keyboard backlight level if the device has an associated keyboard backlight + * {@see Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT} + */ + public abstract void decrementKeyboardBacklight(int deviceId); } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 71feb952cb005..8223ba6c6f8d1 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -311,6 +311,9 @@ public class InputManagerService extends IInputManager.Stub // Manages battery state for input devices. private final BatteryController mBatteryController; + // Manages Keyboard backlight + private final KeyboardBacklightController mKeyboardBacklightController; + // Maximum number of milliseconds to wait for input event injection. private static final int INJECTION_TIMEOUT_MILLIS = 30 * 1000; @@ -421,6 +424,8 @@ public class InputManagerService extends IInputManager.Stub mHandler = new InputManagerHandler(injector.getLooper()); mNative = injector.getNativeService(this); mBatteryController = new BatteryController(mContext, mNative, injector.getLooper()); + mKeyboardBacklightController = new KeyboardBacklightController(mContext, mNative, + mDataStore, injector.getLooper()); mUseDevInputEventForAudioJack = mContext.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack); @@ -562,6 +567,7 @@ public class InputManagerService extends IInputManager.Stub } mBatteryController.systemRunning(); + mKeyboardBacklightController.systemRunning(); } private void reloadKeyboardLayouts() { @@ -2685,6 +2691,7 @@ public class InputManagerService extends IInputManager.Stub dumpSpyWindowGestureMonitors(pw, " " /*prefix*/); dumpDisplayInputPropertiesValues(pw, " " /*prefix*/); mBatteryController.dump(pw, " " /*prefix*/); + mKeyboardBacklightController.dump(pw, " " /*prefix*/); } private void dumpAssociations(PrintWriter pw, String prefix) { @@ -3762,6 +3769,16 @@ public class InputManagerService extends IInputManager.Stub public void pilferPointers(IBinder token) { mNative.pilferPointers(token); } + + @Override + public void incrementKeyboardBacklight(int deviceId) { + mKeyboardBacklightController.incrementKeyboardBacklight(deviceId); + } + + @Override + public void decrementKeyboardBacklight(int deviceId) { + mKeyboardBacklightController.decrementKeyboardBacklight(deviceId); + } } @Override diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java new file mode 100644 index 0000000000000..e33f28c229987 --- /dev/null +++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2022 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.annotation.ColorInt; +import android.content.Context; +import android.graphics.Color; +import android.hardware.input.InputManager; +import android.hardware.lights.Light; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.view.InputDevice; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +import java.io.PrintWriter; +import java.util.Objects; +import java.util.OptionalInt; +import java.util.TreeSet; + +/** + * A thread-safe component of {@link InputManagerService} responsible for managing the keyboard + * backlight for supported keyboards. + */ +final class KeyboardBacklightController implements InputManager.InputDeviceListener { + + private static final String TAG = "KbdBacklightController"; + + // To enable these logs, run: + // 'adb shell setprop log.tag.KbdBacklightController DEBUG' (requires restart) + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private enum Direction { + DIRECTION_UP, DIRECTION_DOWN + } + private static final int MSG_INCREMENT_KEYBOARD_BACKLIGHT = 1; + private static final int MSG_DECREMENT_KEYBOARD_BACKLIGHT = 2; + private static final int MAX_BRIGHTNESS = 255; + private static final int NUM_BRIGHTNESS_CHANGE_STEPS = 10; + @VisibleForTesting + static final TreeSet BRIGHTNESS_LEVELS = new TreeSet<>(); + + private final Context mContext; + private final NativeInputManagerService mNative; + // The PersistentDataStore should be locked before use. + @GuardedBy("mDataStore") + private final PersistentDataStore mDataStore; + private final Handler mHandler; + private final SparseArray mKeyboardBacklights = new SparseArray<>(); + + static { + // Fixed brightness levels to avoid issues when converting back and forth from the + // device brightness range to [0-255] + // Levels are: 0, 25, 51, ..., 255 + for (int i = 0; i <= NUM_BRIGHTNESS_CHANGE_STEPS; i++) { + BRIGHTNESS_LEVELS.add( + (int) Math.floor(((float) i * MAX_BRIGHTNESS) / NUM_BRIGHTNESS_CHANGE_STEPS)); + } + } + + KeyboardBacklightController(Context context, NativeInputManagerService nativeService, + PersistentDataStore dataStore, Looper looper) { + mContext = context; + mNative = nativeService; + mDataStore = dataStore; + mHandler = new Handler(looper, this::handleMessage); + } + + void systemRunning() { + InputManager inputManager = Objects.requireNonNull( + mContext.getSystemService(InputManager.class)); + inputManager.registerInputDeviceListener(this, mHandler); + // Circle through all the already added input devices + for (int deviceId : inputManager.getInputDeviceIds()) { + onInputDeviceAdded(deviceId); + } + } + + public void incrementKeyboardBacklight(int deviceId) { + Message msg = Message.obtain(mHandler, MSG_INCREMENT_KEYBOARD_BACKLIGHT, deviceId); + mHandler.sendMessage(msg); + } + + public void decrementKeyboardBacklight(int deviceId) { + Message msg = Message.obtain(mHandler, MSG_DECREMENT_KEYBOARD_BACKLIGHT, deviceId); + mHandler.sendMessage(msg); + } + + private void updateKeyboardBacklight(int deviceId, Direction direction) { + InputDevice inputDevice = getInputDevice(deviceId); + Light keyboardBacklight = mKeyboardBacklights.get(deviceId); + if (inputDevice == null || keyboardBacklight == null) { + return; + } + // Follow preset levels of brightness defined in BRIGHTNESS_LEVELS + int currBrightness = BRIGHTNESS_LEVELS.floor(Color.alpha( + mNative.getLightColor(deviceId, keyboardBacklight.getId()))); + int newBrightness; + if (direction == Direction.DIRECTION_UP) { + newBrightness = currBrightness != MAX_BRIGHTNESS ? BRIGHTNESS_LEVELS.higher( + currBrightness) : currBrightness; + } else { + newBrightness = currBrightness != 0 ? BRIGHTNESS_LEVELS.lower(currBrightness) + : currBrightness; + } + @ColorInt int newColor = Color.argb(newBrightness, 0, 0, 0); + mNative.setLightColor(deviceId, keyboardBacklight.getId(), newColor); + if (DEBUG) { + Slog.d(TAG, "Changing brightness from " + currBrightness + " to " + newBrightness); + } + + synchronized (mDataStore) { + try { + mDataStore.setKeyboardBacklightBrightness(inputDevice.getDescriptor(), + keyboardBacklight.getId(), + newBrightness); + } finally { + mDataStore.saveIfNeeded(); + } + } + } + + private void restoreBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight) { + OptionalInt brightness; + synchronized (mDataStore) { + brightness = mDataStore.getKeyboardBacklightBrightness( + inputDevice.getDescriptor(), keyboardBacklight.getId()); + } + if (!brightness.isEmpty()) { + mNative.setLightColor(inputDevice.getId(), keyboardBacklight.getId(), + Color.argb(brightness.getAsInt(), 0, 0, 0)); + if (DEBUG) { + Slog.d(TAG, "Restoring brightness level " + brightness.getAsInt()); + } + } + } + + private boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_INCREMENT_KEYBOARD_BACKLIGHT: + updateKeyboardBacklight((int) msg.obj, Direction.DIRECTION_UP); + return true; + case MSG_DECREMENT_KEYBOARD_BACKLIGHT: + updateKeyboardBacklight((int) msg.obj, Direction.DIRECTION_DOWN); + return true; + } + return false; + } + + @VisibleForTesting + @Override + public void onInputDeviceAdded(int deviceId) { + onInputDeviceChanged(deviceId); + } + + @VisibleForTesting + @Override + public void onInputDeviceRemoved(int deviceId) { + mKeyboardBacklights.remove(deviceId); + } + + @VisibleForTesting + @Override + public void onInputDeviceChanged(int deviceId) { + InputDevice inputDevice = getInputDevice(deviceId); + if (inputDevice == null) { + return; + } + final Light keyboardBacklight = getKeyboardBacklight(inputDevice); + if (keyboardBacklight == null) { + mKeyboardBacklights.remove(deviceId); + return; + } + final Light oldBacklight = mKeyboardBacklights.get(deviceId); + if (oldBacklight != null && oldBacklight.getId() == keyboardBacklight.getId()) { + return; + } + // The keyboard backlight was added or changed. + mKeyboardBacklights.put(deviceId, keyboardBacklight); + restoreBacklightBrightness(inputDevice, keyboardBacklight); + } + + private InputDevice getInputDevice(int deviceId) { + InputManager inputManager = mContext.getSystemService(InputManager.class); + return inputManager != null ? inputManager.getInputDevice(deviceId) : null; + } + + private Light getKeyboardBacklight(InputDevice inputDevice) { + // Assuming each keyboard can have only single Light node for Keyboard backlight control + // for simplicity. + for (Light light : inputDevice.getLightsManager().getLights()) { + if (light.getType() == Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT + && light.hasBrightnessControl()) { + return light; + } + } + return null; + } + + void dump(PrintWriter pw, String prefix) { + pw.println(prefix + TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights"); + for (int i = 0; i < mKeyboardBacklights.size(); i++) { + Light light = mKeyboardBacklights.get(i); + pw.println(prefix + " " + i + ": { id: " + light.getId() + ", name: " + light.getName() + + " }"); + } + } +} diff --git a/services/core/java/com/android/server/input/PersistentDataStore.java b/services/core/java/com/android/server/input/PersistentDataStore.java index 6cec2723b1411..7dce28c910729 100644 --- a/services/core/java/com/android/server/input/PersistentDataStore.java +++ b/services/core/java/com/android/server/input/PersistentDataStore.java @@ -16,40 +16,37 @@ package com.android.server.input; -import com.android.internal.util.ArrayUtils; -import com.android.internal.util.FastXmlSerializer; -import com.android.internal.util.XmlUtils; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - import android.annotation.Nullable; -import android.view.Surface; import android.hardware.input.TouchCalibration; import android.util.AtomicFile; import android.util.Slog; +import android.util.SparseIntArray; import android.util.TypedXmlPullParser; import android.util.TypedXmlSerializer; import android.util.Xml; +import android.view.Surface; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.XmlUtils; + +import libcore.io.IoUtils; + +import org.xmlpull.v1.XmlPullParserException; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.OptionalInt; import java.util.Set; -import libcore.io.IoUtils; - /** * Manages persistent state recorded by the input manager service as an XML file. * Caller must acquire lock on the data store before accessing it. @@ -69,7 +66,9 @@ final class PersistentDataStore { // Input device state by descriptor. private final HashMap mInputDevices = new HashMap(); - private final AtomicFile mAtomicFile; + + // The interface for methods which should be replaced by the test harness. + private Injector mInjector; // True if the data has been loaded. private boolean mLoaded; @@ -78,8 +77,12 @@ final class PersistentDataStore { private boolean mDirty; public PersistentDataStore() { - mAtomicFile = new AtomicFile(new File("/data/system/input-manager-state.xml"), - "input-state"); + this(new Injector()); + } + + @VisibleForTesting + PersistentDataStore(Injector injector) { + mInjector = injector; } public void saveIfNeeded() { @@ -90,7 +93,7 @@ final class PersistentDataStore { } public TouchCalibration getTouchCalibration(String inputDeviceDescriptor, int surfaceRotation) { - InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, false); + InputDeviceState state = getInputDeviceState(inputDeviceDescriptor); if (state == null) { return TouchCalibration.IDENTITY; } @@ -103,7 +106,7 @@ final class PersistentDataStore { } public boolean setTouchCalibration(String inputDeviceDescriptor, int surfaceRotation, TouchCalibration calibration) { - InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, true); + InputDeviceState state = getOrCreateInputDeviceState(inputDeviceDescriptor); if (state.setTouchCalibration(surfaceRotation, calibration)) { setDirty(); @@ -114,13 +117,13 @@ final class PersistentDataStore { } public String getCurrentKeyboardLayout(String inputDeviceDescriptor) { - InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, false); + InputDeviceState state = getInputDeviceState(inputDeviceDescriptor); return state != null ? state.getCurrentKeyboardLayout() : null; } public boolean setCurrentKeyboardLayout(String inputDeviceDescriptor, String keyboardLayoutDescriptor) { - InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, true); + InputDeviceState state = getOrCreateInputDeviceState(inputDeviceDescriptor); if (state.setCurrentKeyboardLayout(keyboardLayoutDescriptor)) { setDirty(); return true; @@ -129,7 +132,7 @@ final class PersistentDataStore { } public String[] getKeyboardLayouts(String inputDeviceDescriptor) { - InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, false); + InputDeviceState state = getInputDeviceState(inputDeviceDescriptor); if (state == null) { return (String[])ArrayUtils.emptyArray(String.class); } @@ -138,7 +141,7 @@ final class PersistentDataStore { public boolean addKeyboardLayout(String inputDeviceDescriptor, String keyboardLayoutDescriptor) { - InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, true); + InputDeviceState state = getOrCreateInputDeviceState(inputDeviceDescriptor); if (state.addKeyboardLayout(keyboardLayoutDescriptor)) { setDirty(); return true; @@ -148,7 +151,7 @@ final class PersistentDataStore { public boolean removeKeyboardLayout(String inputDeviceDescriptor, String keyboardLayoutDescriptor) { - InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, true); + InputDeviceState state = getOrCreateInputDeviceState(inputDeviceDescriptor); if (state.removeKeyboardLayout(keyboardLayoutDescriptor)) { setDirty(); return true; @@ -157,7 +160,7 @@ final class PersistentDataStore { } public boolean switchKeyboardLayout(String inputDeviceDescriptor, int direction) { - InputDeviceState state = getInputDeviceState(inputDeviceDescriptor, false); + InputDeviceState state = getInputDeviceState(inputDeviceDescriptor); if (state != null && state.switchKeyboardLayout(direction)) { setDirty(); return true; @@ -165,6 +168,24 @@ final class PersistentDataStore { return false; } + public boolean setKeyboardBacklightBrightness(String inputDeviceDescriptor, int lightId, + int brightness) { + InputDeviceState state = getOrCreateInputDeviceState(inputDeviceDescriptor); + if (state.setKeyboardBacklightBrightness(lightId, brightness)) { + setDirty(); + return true; + } + return false; + } + + public OptionalInt getKeyboardBacklightBrightness(String inputDeviceDescriptor, int lightId) { + InputDeviceState state = getInputDeviceState(inputDeviceDescriptor); + if (state == null) { + return OptionalInt.empty(); + } + return state.getKeyboardBacklightBrightness(lightId); + } + public boolean removeUninstalledKeyboardLayouts(Set availableKeyboardLayouts) { boolean changed = false; for (InputDeviceState state : mInputDevices.values()) { @@ -179,11 +200,15 @@ final class PersistentDataStore { return false; } - private InputDeviceState getInputDeviceState(String inputDeviceDescriptor, - boolean createIfAbsent) { + private InputDeviceState getInputDeviceState(String inputDeviceDescriptor) { + loadIfNeeded(); + return mInputDevices.get(inputDeviceDescriptor); + } + + private InputDeviceState getOrCreateInputDeviceState(String inputDeviceDescriptor) { loadIfNeeded(); InputDeviceState state = mInputDevices.get(inputDeviceDescriptor); - if (state == null && createIfAbsent) { + if (state == null) { state = new InputDeviceState(); mInputDevices.put(inputDeviceDescriptor, state); setDirty(); @@ -211,7 +236,7 @@ final class PersistentDataStore { final InputStream is; try { - is = mAtomicFile.openRead(); + is = mInjector.openRead(); } catch (FileNotFoundException ex) { return; } @@ -234,7 +259,7 @@ final class PersistentDataStore { private void save() { final FileOutputStream os; try { - os = mAtomicFile.startWrite(); + os = mInjector.startWrite(); boolean success = false; try { TypedXmlSerializer serializer = Xml.resolveSerializer(os); @@ -242,11 +267,7 @@ final class PersistentDataStore { serializer.flush(); success = true; } finally { - if (success) { - mAtomicFile.finishWrite(os); - } else { - mAtomicFile.failWrite(os); - } + mInjector.finishWrite(os, success); } } catch (IOException ex) { Slog.w(InputManagerService.TAG, "Failed to save input manager persistent store data.", ex); @@ -307,10 +328,12 @@ final class PersistentDataStore { private static final String[] CALIBRATION_NAME = { "x_scale", "x_ymix", "x_offset", "y_xmix", "y_scale", "y_offset" }; - private TouchCalibration[] mTouchCalibration = new TouchCalibration[4]; + private static final int INVALID_VALUE = -1; + private final TouchCalibration[] mTouchCalibration = new TouchCalibration[4]; @Nullable private String mCurrentKeyboardLayout; - private ArrayList mKeyboardLayouts = new ArrayList(); + private final ArrayList mKeyboardLayouts = new ArrayList(); + private final SparseIntArray mKeyboardBacklightBrightnessMap = new SparseIntArray(); public TouchCalibration getTouchCalibration(int surfaceRotation) { try { @@ -377,6 +400,19 @@ final class PersistentDataStore { return true; } + public boolean setKeyboardBacklightBrightness(int lightId, int brightness) { + if (mKeyboardBacklightBrightnessMap.get(lightId, INVALID_VALUE) == brightness) { + return false; + } + mKeyboardBacklightBrightnessMap.put(lightId, brightness); + return true; + } + + public OptionalInt getKeyboardBacklightBrightness(int lightId) { + int brightness = mKeyboardBacklightBrightnessMap.get(lightId, INVALID_VALUE); + return brightness == INVALID_VALUE ? OptionalInt.empty() : OptionalInt.of(brightness); + } + private void updateCurrentKeyboardLayoutIfRemoved( String removedKeyboardLayout, int removedIndex) { if (Objects.equals(mCurrentKeyboardLayout, removedKeyboardLayout)) { @@ -446,6 +482,10 @@ final class PersistentDataStore { } mCurrentKeyboardLayout = descriptor; } + } else if (parser.getName().equals("light-info")) { + int lightId = parser.getAttributeInt(null, "light-id"); + int lightBrightness = parser.getAttributeInt(null, "light-brightness"); + mKeyboardBacklightBrightnessMap.put(lightId, lightBrightness); } else if (parser.getName().equals("calibration")) { String format = parser.getAttributeValue(null, "format"); String rotation = parser.getAttributeValue(null, "rotation"); @@ -515,6 +555,15 @@ final class PersistentDataStore { serializer.endTag(null, "keyboard-layout"); } + for (int i = 0; i < mKeyboardBacklightBrightnessMap.size(); i++) { + int lightId = mKeyboardBacklightBrightnessMap.valueAt(i); + serializer.startTag(null, "light-info"); + serializer.attributeInt(null, "light-id", lightId); + serializer.attributeInt(null, "light-brightness", + mKeyboardBacklightBrightnessMap.get(lightId)); + serializer.endTag(null, "light-info"); + } + for (int i = 0; i < mTouchCalibration.length; i++) { if (mTouchCalibration[i] != null) { String rotation = surfaceRotationToString(i); @@ -559,4 +608,30 @@ final class PersistentDataStore { throw new IllegalArgumentException("Unsupported surface rotation string '" + s + "'"); } } + + @VisibleForTesting + static class Injector { + private final AtomicFile mAtomicFile; + + Injector() { + mAtomicFile = new AtomicFile(new File("/data/system/input-manager-state.xml"), + "input-state"); + } + + InputStream openRead() throws FileNotFoundException { + return mAtomicFile.openRead(); + } + + FileOutputStream startWrite() throws IOException { + return mAtomicFile.startWrite(); + } + + void finishWrite(FileOutputStream fos, boolean success) { + if (success) { + mAtomicFile.finishWrite(fos); + } else { + mAtomicFile.failWrite(fos); + } + } + } } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 07f5bcfb6e15a..41663648c98cf 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -2931,9 +2931,17 @@ public class PhoneWindowManager implements WindowManagerPolicy { } return key_consumed; case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN: + if (down) { + mInputManagerInternal.decrementKeyboardBacklight(event.getDeviceId()); + } + return key_consumed; case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP: + if (down) { + mInputManagerInternal.incrementKeyboardBacklight(event.getDeviceId()); + } + return key_consumed; case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE: - // TODO: Add logic to handle keyboard backlight controls (go/pk_backlight_control) + // TODO: Add logic return key_consumed; case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_DOWN: diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt new file mode 100644 index 0000000000000..44bdf5e496530 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2022 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.content.Context +import android.content.ContextWrapper +import android.graphics.Color +import android.hardware.input.IInputManager +import android.hardware.input.InputManager +import android.hardware.lights.Light +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import android.view.InputDevice +import androidx.test.core.app.ApplicationProvider +import com.android.server.input.KeyboardBacklightController.BRIGHTNESS_LEVELS +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.eq +import org.mockito.Mockito.spy +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnit + +private fun createKeyboard(deviceId: Int): InputDevice = + InputDevice.Builder() + .setId(deviceId) + .setName("Device $deviceId") + .setDescriptor("descriptor $deviceId") + .setSources(InputDevice.SOURCE_KEYBOARD) + .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC) + .setExternal(true) + .build() + +private fun createLight(lightId: Int, lightType: Int): Light = + Light( + lightId, + "Light $lightId", + 1, + lightType, + Light.LIGHT_CAPABILITY_BRIGHTNESS + ) +/** + * Tests for {@link KeyboardBacklightController}. + * + * Build/Install/Run: + * atest FrameworksServicesTests:KeyboardBacklightControllerTests + */ +@Presubmit +class KeyboardBacklightControllerTests { + companion object { + const val DEVICE_ID = 1 + const val LIGHT_ID = 2 + const val SECOND_LIGHT_ID = 3 + } + + @get:Rule + val rule = MockitoJUnit.rule()!! + + @Mock + private lateinit var iInputManager: IInputManager + @Mock + private lateinit var native: NativeInputManagerService + private lateinit var keyboardBacklightController: KeyboardBacklightController + private lateinit var context: Context + private lateinit var dataStore: PersistentDataStore + private lateinit var testLooper: TestLooper + private var lightColorMap: HashMap = HashMap() + + @Before + fun setup() { + context = spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + dataStore = PersistentDataStore(object : PersistentDataStore.Injector() { + override fun openRead(): InputStream? { + throw FileNotFoundException() + } + + override fun startWrite(): FileOutputStream? { + throw IOException() + } + + override fun finishWrite(fos: FileOutputStream?, success: Boolean) {} + }) + testLooper = TestLooper() + keyboardBacklightController = + KeyboardBacklightController(context, native, dataStore, testLooper.looper) + val inputManager = InputManager.resetInstance(iInputManager) + `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager) + `when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID)) + `when`(native.setLightColor(anyInt(), anyInt(), anyInt())).then { + val args = it.arguments + lightColorMap.put(args[1] as Int, args[2] as Int) + } + `when`(native.getLightColor(anyInt(), anyInt())).then { + val args = it.arguments + lightColorMap.getOrDefault(args[1] as Int, -1) + } + lightColorMap.clear() + } + + @After + fun tearDown() { + InputManager.clearInstance() + } + + @Test + fun testKeyboardBacklightIncrementDecrement() { + 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) + // Initially backlight is at min + lightColorMap[LIGHT_ID] = Color.argb(BRIGHTNESS_LEVELS.first(), 0, 0, 0) + + val brightnessLevelsArray = BRIGHTNESS_LEVELS.toTypedArray() + for (level in 1 until brightnessLevelsArray.size) { + keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID) + testLooper.dispatchNext() + assertEquals( + "Light value for level $level mismatched", + Color.argb(brightnessLevelsArray[level], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + assertEquals( + "Light value for level $level must be correctly stored in the datastore", + brightnessLevelsArray[level], + dataStore.getKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID + ).asInt + ) + } + + for (level in brightnessLevelsArray.size - 2 downTo 0) { + keyboardBacklightController.decrementKeyboardBacklight(DEVICE_ID) + testLooper.dispatchNext() + assertEquals( + "Light value for level $level mismatched", + Color.argb(brightnessLevelsArray[level], 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + assertEquals( + "Light value for level $level must be correctly stored in the datastore", + brightnessLevelsArray[level], + dataStore.getKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID + ).asInt + ) + } + } + + @Test + fun testKeyboardBacklightIncrementAboveMaxLevel() { + 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) + // Initially backlight is at max + lightColorMap[LIGHT_ID] = Color.argb(BRIGHTNESS_LEVELS.last(), 0, 0, 0) + + keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID) + testLooper.dispatchNext() + assertEquals( + "Light value for max level mismatched", + Color.argb(BRIGHTNESS_LEVELS.last(), 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + assertEquals( + "Light value for max level must be correctly stored in the datastore", + BRIGHTNESS_LEVELS.last(), + dataStore.getKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID + ).asInt + ) + } + + @Test + fun testKeyboardBacklightDecrementBelowMin() { + 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) + // Initially backlight is at min + lightColorMap[LIGHT_ID] = Color.argb(BRIGHTNESS_LEVELS.first(), 0, 0, 0) + + keyboardBacklightController.decrementKeyboardBacklight(DEVICE_ID) + testLooper.dispatchNext() + assertEquals( + "Light value for min level mismatched", + Color.argb(BRIGHTNESS_LEVELS.first(), 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + assertEquals( + "Light value for min level must be correctly stored in the datastore", + BRIGHTNESS_LEVELS.first(), + dataStore.getKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID + ).asInt + ) + } + + @Test + fun testKeyboardWithoutBacklight() { + val keyboardWithoutBacklight = createKeyboard(DEVICE_ID) + val keyboardInputLight = createLight(LIGHT_ID, Light.LIGHT_TYPE_INPUT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithoutBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardInputLight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID) + assertTrue("Non Keyboard backlights should not change", lightColorMap.isEmpty()) + } + + @Test + fun testKeyboardWithMultipleLight() { + 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) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn( + listOf( + keyboardBacklight, + keyboardInputLight + ) + ) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + + keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID) + testLooper.dispatchNext() + assertEquals("Only keyboard backlights should change", 1, lightColorMap.size) + assertNotNull("Keyboard backlight should change", lightColorMap[LIGHT_ID]) + assertNull("Input lights should not change", lightColorMap[SECOND_LIGHT_ID]) + } + + @Test + fun testRestoreBacklightOnInputDeviceAdded() { + 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)) + + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + BRIGHTNESS_LEVELS.last() + ) + lightColorMap.clear() + + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + assertEquals( + "Keyboard backlight level should be restored to the level saved in the data store", + Color.argb(BRIGHTNESS_LEVELS.last(), 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + } + + @Test + fun testRestoreBacklightOnInputDeviceChanged() { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + dataStore.setKeyboardBacklightBrightness( + keyboardWithBacklight.descriptor, + LIGHT_ID, + BRIGHTNESS_LEVELS.last() + ) + lightColorMap.clear() + + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + assertTrue( + "Keyboard backlight should not be changed until its added", + lightColorMap.isEmpty() + ) + + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceChanged(DEVICE_ID) + assertEquals( + "Keyboard backlight level should be restored to the level saved in the data store", + Color.argb(BRIGHTNESS_LEVELS.last(), 0, 0, 0), + lightColorMap[LIGHT_ID] + ) + } +}