Merge "Add keyboard backlight controller"

This commit is contained in:
Vaibhav Devmurari
2022-10-03 14:53:42 +00:00
committed by Android (Google) Code Review
6 changed files with 690 additions and 38 deletions

View File

@@ -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);
}

View File

@@ -312,6 +312,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;
@@ -422,6 +425,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);
@@ -563,6 +568,7 @@ public class InputManagerService extends IInputManager.Stub
}
mBatteryController.systemRunning();
mKeyboardBacklightController.systemRunning();
}
private void reloadKeyboardLayouts() {
@@ -2680,6 +2686,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) {
@@ -3757,6 +3764,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

View File

@@ -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<Integer> 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<Light> 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()
+ " }");
}
}
}

View File

@@ -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<String, InputDeviceState> mInputDevices =
new HashMap<String, InputDeviceState>();
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<String> 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<String> mKeyboardLayouts = new ArrayList<String>();
private final ArrayList<String> mKeyboardLayouts = new ArrayList<String>();
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);
}
}
}
}

View File

@@ -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:

View File

@@ -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<Int, Int> = 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]
)
}
}