blob: 48c346a2fe22b01f36122333af24a93eec430da0 [file] [log] [blame]
/*
* 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.BinderThread;
import android.content.Context;
import android.graphics.Color;
import android.hardware.input.IKeyboardBacklightListener;
import android.hardware.input.IKeyboardBacklightState;
import android.hardware.input.InputManager;
import android.hardware.lights.Light;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UEventObserver;
import android.text.TextUtils;
import android.util.IndentingPrintWriter;
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.time.Duration;
import java.util.Arrays;
import java.util.Objects;
import java.util.OptionalInt;
/**
* A thread-safe component of {@link InputManagerService} responsible for managing the keyboard
* backlight for supported keyboards.
*/
final class KeyboardBacklightController implements
InputManagerService.KeyboardBacklightControllerInterface, 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_UPDATE_EXISTING_DEVICES = 1;
private static final int MSG_INCREMENT_KEYBOARD_BACKLIGHT = 2;
private static final int MSG_DECREMENT_KEYBOARD_BACKLIGHT = 3;
private static final int MSG_NOTIFY_USER_ACTIVITY = 4;
private static final int MSG_NOTIFY_USER_INACTIVITY = 5;
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 String UEVENT_KEYBOARD_BACKLIGHT_TAG = "kbd_backlight";
@VisibleForTesting
static final long USER_INACTIVITY_THRESHOLD_MILLIS = Duration.ofSeconds(30).toMillis();
@VisibleForTesting
static final int[] BRIGHTNESS_VALUE_FOR_LEVEL = new int[NUM_BRIGHTNESS_CHANGE_STEPS + 1];
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;
// 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
private boolean mIsBacklightOn = false;
// Maintains state if currently the device is interactive or not
private boolean mIsInteractive = true;
// List of currently registered keyboard backlight listeners
@GuardedBy("mKeyboardBacklightListenerRecords")
private final SparseArray<KeyboardBacklightListenerRecord> mKeyboardBacklightListenerRecords =
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_VALUE_FOR_LEVEL[i] = (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);
}
@Override
public void systemRunning() {
InputManager inputManager = Objects.requireNonNull(
mContext.getSystemService(InputManager.class));
inputManager.registerInputDeviceListener(this, mHandler);
Message msg = Message.obtain(mHandler, MSG_UPDATE_EXISTING_DEVICES,
inputManager.getInputDeviceIds());
mHandler.sendMessage(msg);
// Observe UEvents for "kbd_backlight" sysfs nodes.
// We want to observe creation of such LED nodes since they might be created after device
// FD created and InputDevice creation logic doesn't initialize LED nodes which leads to
// backlight not working.
UEventObserver observer = new UEventObserver() {
@Override
public void onUEvent(UEvent event) {
onKeyboardBacklightUEvent(event);
}
};
observer.startObserving(UEVENT_KEYBOARD_BACKLIGHT_TAG);
}
@Override
public void incrementKeyboardBacklight(int deviceId) {
Message msg = Message.obtain(mHandler, MSG_INCREMENT_KEYBOARD_BACKLIGHT, deviceId);
mHandler.sendMessage(msg);
}
@Override
public void decrementKeyboardBacklight(int deviceId) {
Message msg = Message.obtain(mHandler, MSG_DECREMENT_KEYBOARD_BACKLIGHT, deviceId);
mHandler.sendMessage(msg);
}
@Override
public void notifyUserActivity() {
Message msg = Message.obtain(mHandler, MSG_NOTIFY_USER_ACTIVITY);
mHandler.sendMessage(msg);
}
@Override
public void onInteractiveChanged(boolean isInteractive) {
Message msg = Message.obtain(mHandler, MSG_INTERACTIVE_STATE_CHANGED, isInteractive);
mHandler.sendMessage(msg);
}
private void updateKeyboardBacklight(int deviceId, Direction direction) {
InputDevice inputDevice = getInputDevice(deviceId);
KeyboardBacklightState state = mKeyboardBacklights.get(deviceId);
if (inputDevice == null || state == null) {
return;
}
Light keyboardBacklight = state.mLight;
// Follow preset levels of brightness defined in BRIGHTNESS_LEVELS
final int currBrightnessLevel = state.mBrightnessLevel;
final int newBrightnessLevel;
if (direction == Direction.DIRECTION_UP) {
newBrightnessLevel = Math.min(currBrightnessLevel + 1, NUM_BRIGHTNESS_CHANGE_STEPS);
} else {
newBrightnessLevel = Math.max(currBrightnessLevel - 1, 0);
}
updateBacklightState(deviceId, keyboardBacklight, newBrightnessLevel,
true /* isTriggeredByKeyPress */);
synchronized (mDataStore) {
try {
mDataStore.setKeyboardBacklightBrightness(inputDevice.getDescriptor(),
keyboardBacklight.getId(),
BRIGHTNESS_VALUE_FOR_LEVEL[newBrightnessLevel]);
} finally {
mDataStore.saveIfNeeded();
}
}
}
private void restoreBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight) {
OptionalInt brightness;
synchronized (mDataStore) {
brightness = mDataStore.getKeyboardBacklightBrightness(
inputDevice.getDescriptor(), keyboardBacklight.getId());
}
if (brightness.isPresent()) {
int brightnessValue = Math.max(0, Math.min(MAX_BRIGHTNESS, brightness.getAsInt()));
int index = Arrays.binarySearch(BRIGHTNESS_VALUE_FOR_LEVEL, brightnessValue);
if (index < 0) {
index = Math.min(NUM_BRIGHTNESS_CHANGE_STEPS, -(index + 1));
}
updateBacklightState(inputDevice.getId(), keyboardBacklight, index,
false /* isTriggeredByKeyPress */);
if (DEBUG) {
Slog.d(TAG, "Restoring brightness level " + brightness.getAsInt());
}
}
}
private void handleUserActivity() {
// Ignore user activity if device is not interactive. When device becomes interactive, we
// will send another user activity to turn backlight on.
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 */);
}
}
mHandler.removeMessages(MSG_NOTIFY_USER_INACTIVITY);
mHandler.sendEmptyMessageAtTime(MSG_NOTIFY_USER_INACTIVITY,
SystemClock.uptimeMillis() + USER_INACTIVITY_THRESHOLD_MILLIS);
}
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 */);
}
}
}
@VisibleForTesting
public void handleInteractiveStateChange(boolean isInteractive) {
// Interactive state changes should force the keyboard to turn on/off irrespective of
// whether time out occurred or not.
mIsInteractive = isInteractive;
if (isInteractive) {
handleUserActivity();
} else {
handleUserInactivity();
}
}
private boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_UPDATE_EXISTING_DEVICES:
for (int deviceId : (int[]) msg.obj) {
onInputDeviceAdded(deviceId);
}
return true;
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;
case MSG_NOTIFY_USER_ACTIVITY:
handleUserActivity();
return true;
case MSG_NOTIFY_USER_INACTIVITY:
handleUserInactivity();
return true;
case MSG_INTERACTIVE_STATE_CHANGED:
handleInteractiveStateChange((boolean) msg.obj);
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;
}
KeyboardBacklightState state = mKeyboardBacklights.get(deviceId);
if (state != null && state.mLight.getId() == keyboardBacklight.getId()) {
return;
}
// The keyboard backlight was added or changed.
mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(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;
}
/** Register the keyboard backlight listener for a process. */
@BinderThread
@Override
public void registerKeyboardBacklightListener(IKeyboardBacklightListener listener,
int pid) {
synchronized (mKeyboardBacklightListenerRecords) {
if (mKeyboardBacklightListenerRecords.get(pid) != null) {
throw new IllegalStateException("The calling process has already registered "
+ "a KeyboardBacklightListener.");
}
KeyboardBacklightListenerRecord record = new KeyboardBacklightListenerRecord(pid,
listener);
try {
listener.asBinder().linkToDeath(record, 0);
} catch (RemoteException ex) {
throw new RuntimeException(ex);
}
mKeyboardBacklightListenerRecords.put(pid, record);
}
}
/** Unregister the keyboard backlight listener for a process. */
@BinderThread
@Override
public void unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener,
int pid) {
synchronized (mKeyboardBacklightListenerRecords) {
KeyboardBacklightListenerRecord record = mKeyboardBacklightListenerRecords.get(pid);
if (record == null) {
throw new IllegalStateException("The calling process has no registered "
+ "KeyboardBacklightListener.");
}
if (record.mListener.asBinder() != listener.asBinder()) {
throw new IllegalStateException("The calling process has a different registered "
+ "KeyboardBacklightListener.");
}
record.mListener.asBinder().unlinkToDeath(record, 0);
mKeyboardBacklightListenerRecords.remove(pid);
}
}
private void updateBacklightState(int deviceId, Light light, 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;
synchronized (mKeyboardBacklightListenerRecords) {
for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) {
IKeyboardBacklightState callbackState = new IKeyboardBacklightState();
callbackState.brightnessLevel = brightnessLevel;
callbackState.maxBrightnessLevel = NUM_BRIGHTNESS_CHANGE_STEPS;
mKeyboardBacklightListenerRecords.valueAt(i).notifyKeyboardBacklightChanged(
deviceId, callbackState, isTriggeredByKeyPress);
}
}
}
private void onKeyboardBacklightListenerDied(int pid) {
synchronized (mKeyboardBacklightListenerRecords) {
mKeyboardBacklightListenerRecords.remove(pid);
}
}
@VisibleForTesting
public void onKeyboardBacklightUEvent(UEventObserver.UEvent event) {
if ("ADD".equalsIgnoreCase(event.get("ACTION")) && "LEDS".equalsIgnoreCase(
event.get("SUBSYSTEM"))) {
final String devPath = event.get("DEVPATH");
if (isValidBacklightNodePath(devPath)) {
mNative.sysfsNodeChanged("/sys" + devPath);
}
}
}
private static boolean isValidBacklightNodePath(String devPath) {
if (TextUtils.isEmpty(devPath)) {
return false;
}
int index = devPath.lastIndexOf('/');
if (index < 0) {
return false;
}
String backlightNode = devPath.substring(index + 1);
devPath = devPath.substring(0, index);
if (!devPath.endsWith("leds") || !backlightNode.contains("kbd_backlight")) {
return false;
}
index = devPath.lastIndexOf('/');
return index >= 0;
}
@Override
public void dump(PrintWriter pw) {
IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
ipw.println(
TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights, isBacklightOn = "
+ mIsBacklightOn);
ipw.increaseIndent();
for (int i = 0; i < mKeyboardBacklights.size(); i++) {
KeyboardBacklightState state = mKeyboardBacklights.valueAt(i);
ipw.println(i + ": " + state.toString());
}
ipw.decreaseIndent();
}
// A record of a registered Keyboard backlight listener from one process.
private class KeyboardBacklightListenerRecord implements IBinder.DeathRecipient {
public final int mPid;
public final IKeyboardBacklightListener mListener;
KeyboardBacklightListenerRecord(int pid, IKeyboardBacklightListener listener) {
mPid = pid;
mListener = listener;
}
@Override
public void binderDied() {
if (DEBUG) {
Slog.d(TAG, "Keyboard backlight listener for pid " + mPid + " died.");
}
onKeyboardBacklightListenerDied(mPid);
}
public void notifyKeyboardBacklightChanged(int deviceId, IKeyboardBacklightState state,
boolean isTriggeredByKeyPress) {
try {
mListener.onBrightnessChanged(deviceId, state, isTriggeredByKeyPress);
} catch (RemoteException ex) {
Slog.w(TAG, "Failed to notify process " + mPid
+ " that keyboard backlight changed, assuming it died.", ex);
binderDied();
}
}
}
private static class KeyboardBacklightState {
private final Light mLight;
private int mBrightnessLevel;
KeyboardBacklightState(Light light) {
mLight = light;
}
@Override
public String toString() {
return "KeyboardBacklightState{Light=" + mLight.getId()
+ ", BrightnessLevel=" + mBrightnessLevel
+ "}";
}
}
}