1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.input; 18 19 import android.animation.ValueAnimator; 20 import android.annotation.BinderThread; 21 import android.annotation.Nullable; 22 import android.content.Context; 23 import android.graphics.Color; 24 import android.hardware.input.IKeyboardBacklightListener; 25 import android.hardware.input.IKeyboardBacklightState; 26 import android.hardware.input.InputManager; 27 import android.hardware.lights.Light; 28 import android.os.Handler; 29 import android.os.IBinder; 30 import android.os.Looper; 31 import android.os.Message; 32 import android.os.RemoteException; 33 import android.os.SystemClock; 34 import android.os.UEventObserver; 35 import android.text.TextUtils; 36 import android.util.IndentingPrintWriter; 37 import android.util.Log; 38 import android.util.Slog; 39 import android.util.SparseArray; 40 import android.view.InputDevice; 41 42 import com.android.internal.annotations.GuardedBy; 43 import com.android.internal.annotations.VisibleForTesting; 44 45 import java.io.PrintWriter; 46 import java.time.Duration; 47 import java.util.Arrays; 48 import java.util.Objects; 49 import java.util.OptionalInt; 50 import java.util.TreeSet; 51 52 /** 53 * A thread-safe component of {@link InputManagerService} responsible for managing the keyboard 54 * backlight for supported keyboards. 55 */ 56 final class KeyboardBacklightController implements 57 InputManagerService.KeyboardBacklightControllerInterface, InputManager.InputDeviceListener { 58 59 private static final String TAG = "KbdBacklightController"; 60 61 // To enable these logs, run: 62 // 'adb shell setprop log.tag.KbdBacklightController DEBUG' (requires restart) 63 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 64 65 private enum Direction { 66 DIRECTION_UP, DIRECTION_DOWN 67 } 68 private static final int MSG_UPDATE_EXISTING_DEVICES = 1; 69 private static final int MSG_INCREMENT_KEYBOARD_BACKLIGHT = 2; 70 private static final int MSG_DECREMENT_KEYBOARD_BACKLIGHT = 3; 71 private static final int MSG_NOTIFY_USER_ACTIVITY = 4; 72 private static final int MSG_NOTIFY_USER_INACTIVITY = 5; 73 private static final int MSG_INTERACTIVE_STATE_CHANGED = 6; 74 private static final int MAX_BRIGHTNESS = 255; 75 private static final int DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS = 10; 76 @VisibleForTesting 77 static final int MAX_BRIGHTNESS_CHANGE_STEPS = 10; 78 private static final long TRANSITION_ANIMATION_DURATION_MILLIS = 79 Duration.ofSeconds(1).toMillis(); 80 81 private static final String UEVENT_KEYBOARD_BACKLIGHT_TAG = "kbd_backlight"; 82 83 @VisibleForTesting 84 static final long USER_INACTIVITY_THRESHOLD_MILLIS = Duration.ofSeconds(30).toMillis(); 85 86 @VisibleForTesting 87 static final int[] DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL = 88 new int[DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS + 1]; 89 90 private final Context mContext; 91 private final NativeInputManagerService mNative; 92 // The PersistentDataStore should be locked before use. 93 @GuardedBy("mDataStore") 94 private final PersistentDataStore mDataStore; 95 private final Handler mHandler; 96 private final AnimatorFactory mAnimatorFactory; 97 private final UEventManager mUEventManager; 98 // Always access on handler thread or need to lock this for synchronization. 99 private final SparseArray<KeyboardBacklightState> mKeyboardBacklights = new SparseArray<>(1); 100 // Maintains state if all backlights should be on or turned off 101 private boolean mIsBacklightOn = false; 102 // Maintains state if currently the device is interactive or not 103 private boolean mIsInteractive = true; 104 105 // List of currently registered keyboard backlight listeners 106 @GuardedBy("mKeyboardBacklightListenerRecords") 107 private final SparseArray<KeyboardBacklightListenerRecord> mKeyboardBacklightListenerRecords = 108 new SparseArray<>(); 109 110 private final AmbientKeyboardBacklightController mAmbientController; 111 @Nullable 112 private AmbientKeyboardBacklightController.AmbientKeyboardBacklightListener mAmbientListener; 113 114 private int mAmbientBacklightValue = 0; 115 116 static { 117 // Fixed brightness levels to avoid issues when converting back and forth from the 118 // device brightness range to [0-255] 119 // Levels are: 0, 51, ..., 255 120 for (int i = 0; i <= DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS; i++) { 121 DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL[i] = (int) Math.floor( 122 ((float) i * MAX_BRIGHTNESS) / DEFAULT_NUM_BRIGHTNESS_CHANGE_STEPS); 123 } 124 } 125 KeyboardBacklightController(Context context, NativeInputManagerService nativeService, PersistentDataStore dataStore, Looper looper, UEventManager uEventManager)126 KeyboardBacklightController(Context context, NativeInputManagerService nativeService, 127 PersistentDataStore dataStore, Looper looper, UEventManager uEventManager) { 128 this(context, nativeService, dataStore, looper, ValueAnimator::ofInt, uEventManager); 129 } 130 131 @VisibleForTesting KeyboardBacklightController(Context context, NativeInputManagerService nativeService, PersistentDataStore dataStore, Looper looper, AnimatorFactory animatorFactory, UEventManager uEventManager)132 KeyboardBacklightController(Context context, NativeInputManagerService nativeService, 133 PersistentDataStore dataStore, Looper looper, AnimatorFactory animatorFactory, 134 UEventManager uEventManager) { 135 mContext = context; 136 mNative = nativeService; 137 mDataStore = dataStore; 138 mHandler = new Handler(looper, this::handleMessage); 139 mAnimatorFactory = animatorFactory; 140 mAmbientController = new AmbientKeyboardBacklightController(context, looper); 141 mUEventManager = uEventManager; 142 } 143 144 @Override systemRunning()145 public void systemRunning() { 146 InputManager inputManager = Objects.requireNonNull( 147 mContext.getSystemService(InputManager.class)); 148 inputManager.registerInputDeviceListener(this, mHandler); 149 150 Message msg = Message.obtain(mHandler, MSG_UPDATE_EXISTING_DEVICES, 151 inputManager.getInputDeviceIds()); 152 mHandler.sendMessage(msg); 153 154 // Observe UEvents for "kbd_backlight" sysfs nodes. 155 // We want to observe creation of such LED nodes since they might be created after device 156 // FD created and InputDevice creation logic doesn't initialize LED nodes which leads to 157 // backlight not working. 158 mUEventManager.addListener(new UEventManager.UEventListener() { 159 @Override 160 public void onUEvent(UEventObserver.UEvent event) { 161 onKeyboardBacklightUEvent(event); 162 } 163 }, UEVENT_KEYBOARD_BACKLIGHT_TAG); 164 165 if (InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled()) { 166 // Start ambient backlight controller 167 mAmbientController.systemRunning(); 168 } 169 } 170 171 @Override incrementKeyboardBacklight(int deviceId)172 public void incrementKeyboardBacklight(int deviceId) { 173 Message msg = Message.obtain(mHandler, MSG_INCREMENT_KEYBOARD_BACKLIGHT, deviceId); 174 mHandler.sendMessage(msg); 175 } 176 177 @Override decrementKeyboardBacklight(int deviceId)178 public void decrementKeyboardBacklight(int deviceId) { 179 Message msg = Message.obtain(mHandler, MSG_DECREMENT_KEYBOARD_BACKLIGHT, deviceId); 180 mHandler.sendMessage(msg); 181 } 182 183 @Override notifyUserActivity()184 public void notifyUserActivity() { 185 Message msg = Message.obtain(mHandler, MSG_NOTIFY_USER_ACTIVITY); 186 mHandler.sendMessage(msg); 187 } 188 189 @Override onInteractiveChanged(boolean isInteractive)190 public void onInteractiveChanged(boolean isInteractive) { 191 Message msg = Message.obtain(mHandler, MSG_INTERACTIVE_STATE_CHANGED, isInteractive); 192 mHandler.sendMessage(msg); 193 } 194 updateKeyboardBacklight(int deviceId, Direction direction)195 private void updateKeyboardBacklight(int deviceId, Direction direction) { 196 InputDevice inputDevice = getInputDevice(deviceId); 197 KeyboardBacklightState state = mKeyboardBacklights.get(deviceId); 198 if (inputDevice == null || state == null) { 199 return; 200 } 201 // Follow preset levels of brightness defined in BRIGHTNESS_LEVELS 202 final int currBrightnessLevel; 203 if (state.mUseAmbientController) { 204 int index = Arrays.binarySearch(state.mBrightnessValueForLevel, mAmbientBacklightValue); 205 // Set current level to the lower bound of the ambient value in the brightness array. 206 if (index < 0) { 207 int lowerBound = Math.max(0, -(index + 1) - 1); 208 currBrightnessLevel = 209 direction == Direction.DIRECTION_UP ? lowerBound : lowerBound + 1; 210 } else { 211 currBrightnessLevel = index; 212 } 213 } else { 214 currBrightnessLevel = state.mBrightnessLevel; 215 } 216 final int newBrightnessLevel; 217 if (direction == Direction.DIRECTION_UP) { 218 newBrightnessLevel = Math.min(currBrightnessLevel + 1, 219 state.getNumBrightnessChangeSteps()); 220 } else { 221 newBrightnessLevel = Math.max(currBrightnessLevel - 1, 0); 222 } 223 224 state.setBrightnessLevel(newBrightnessLevel); 225 226 // Might need to stop listening to ALS since user has manually selected backlight 227 // level through keyboard up/down button 228 updateAmbientLightListener(); 229 230 maybeBackupBacklightBrightness(inputDevice, state.mLight, 231 state.mBrightnessValueForLevel[newBrightnessLevel]); 232 233 if (DEBUG) { 234 Slog.d(TAG, 235 "Changing state from " + state.mBrightnessLevel + " to " + newBrightnessLevel); 236 } 237 238 synchronized (mKeyboardBacklightListenerRecords) { 239 for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) { 240 IKeyboardBacklightState callbackState = new IKeyboardBacklightState(); 241 callbackState.brightnessLevel = newBrightnessLevel; 242 callbackState.maxBrightnessLevel = state.getNumBrightnessChangeSteps(); 243 mKeyboardBacklightListenerRecords.valueAt(i).notifyKeyboardBacklightChanged( 244 deviceId, callbackState, true); 245 } 246 } 247 } 248 maybeBackupBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight, int brightnessValue)249 private void maybeBackupBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight, 250 int brightnessValue) { 251 // Don't back up or restore when ALS based keyboard backlight is enabled 252 if (InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled()) { 253 return; 254 } 255 synchronized (mDataStore) { 256 try { 257 mDataStore.setKeyboardBacklightBrightness(inputDevice.getDescriptor(), 258 keyboardBacklight.getId(), 259 brightnessValue); 260 } finally { 261 mDataStore.saveIfNeeded(); 262 } 263 } 264 } 265 maybeRestoreBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight)266 private void maybeRestoreBacklightBrightness(InputDevice inputDevice, Light keyboardBacklight) { 267 // Don't back up or restore when ALS based keyboard backlight is enabled 268 if (InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled()) { 269 return; 270 } 271 KeyboardBacklightState state = mKeyboardBacklights.get(inputDevice.getId()); 272 OptionalInt brightness; 273 synchronized (mDataStore) { 274 brightness = mDataStore.getKeyboardBacklightBrightness( 275 inputDevice.getDescriptor(), keyboardBacklight.getId()); 276 } 277 if (state != null && brightness.isPresent()) { 278 int brightnessValue = Math.max(0, Math.min(MAX_BRIGHTNESS, brightness.getAsInt())); 279 int newLevel = Arrays.binarySearch(state.mBrightnessValueForLevel, brightnessValue); 280 if (newLevel < 0) { 281 newLevel = Math.min(state.getNumBrightnessChangeSteps(), -(newLevel + 1)); 282 } 283 state.setBrightnessLevel(newLevel); 284 if (DEBUG) { 285 Slog.d(TAG, "Restoring brightness level " + brightness.getAsInt()); 286 } 287 } 288 } 289 handleUserActivity()290 private void handleUserActivity() { 291 // Ignore user activity if device is not interactive. When device becomes interactive, we 292 // will send another user activity to turn backlight on. 293 if (!mIsInteractive) { 294 return; 295 } 296 mIsBacklightOn = true; 297 for (int i = 0; i < mKeyboardBacklights.size(); i++) { 298 KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); 299 state.onBacklightStateChanged(); 300 } 301 mHandler.removeMessages(MSG_NOTIFY_USER_INACTIVITY); 302 mHandler.sendEmptyMessageAtTime(MSG_NOTIFY_USER_INACTIVITY, 303 SystemClock.uptimeMillis() + USER_INACTIVITY_THRESHOLD_MILLIS); 304 } 305 handleUserInactivity()306 private void handleUserInactivity() { 307 mIsBacklightOn = false; 308 for (int i = 0; i < mKeyboardBacklights.size(); i++) { 309 KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); 310 state.onBacklightStateChanged(); 311 } 312 } 313 314 @VisibleForTesting handleInteractiveStateChange(boolean isInteractive)315 public void handleInteractiveStateChange(boolean isInteractive) { 316 // Interactive state changes should force the keyboard to turn on/off irrespective of 317 // whether time out occurred or not. 318 mIsInteractive = isInteractive; 319 if (isInteractive) { 320 handleUserActivity(); 321 } else { 322 handleUserInactivity(); 323 } 324 updateAmbientLightListener(); 325 } 326 327 @VisibleForTesting handleAmbientLightValueChanged(int brightnessValue)328 public void handleAmbientLightValueChanged(int brightnessValue) { 329 mAmbientBacklightValue = brightnessValue; 330 for (int i = 0; i < mKeyboardBacklights.size(); i++) { 331 KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); 332 state.onAmbientBacklightValueChanged(); 333 } 334 } 335 handleMessage(Message msg)336 private boolean handleMessage(Message msg) { 337 switch (msg.what) { 338 case MSG_UPDATE_EXISTING_DEVICES: 339 for (int deviceId : (int[]) msg.obj) { 340 onInputDeviceAdded(deviceId); 341 } 342 return true; 343 case MSG_INCREMENT_KEYBOARD_BACKLIGHT: 344 updateKeyboardBacklight((int) msg.obj, Direction.DIRECTION_UP); 345 return true; 346 case MSG_DECREMENT_KEYBOARD_BACKLIGHT: 347 updateKeyboardBacklight((int) msg.obj, Direction.DIRECTION_DOWN); 348 return true; 349 case MSG_NOTIFY_USER_ACTIVITY: 350 handleUserActivity(); 351 return true; 352 case MSG_NOTIFY_USER_INACTIVITY: 353 handleUserInactivity(); 354 return true; 355 case MSG_INTERACTIVE_STATE_CHANGED: 356 handleInteractiveStateChange((boolean) msg.obj); 357 return true; 358 } 359 return false; 360 } 361 362 @VisibleForTesting 363 @Override onInputDeviceAdded(int deviceId)364 public void onInputDeviceAdded(int deviceId) { 365 onInputDeviceChanged(deviceId); 366 updateAmbientLightListener(); 367 } 368 369 @VisibleForTesting 370 @Override onInputDeviceRemoved(int deviceId)371 public void onInputDeviceRemoved(int deviceId) { 372 mKeyboardBacklights.remove(deviceId); 373 updateAmbientLightListener(); 374 } 375 376 @VisibleForTesting 377 @Override onInputDeviceChanged(int deviceId)378 public void onInputDeviceChanged(int deviceId) { 379 InputDevice inputDevice = getInputDevice(deviceId); 380 if (inputDevice == null) { 381 return; 382 } 383 final Light keyboardBacklight = getKeyboardBacklight(inputDevice); 384 if (keyboardBacklight == null) { 385 mKeyboardBacklights.remove(deviceId); 386 return; 387 } 388 KeyboardBacklightState state = mKeyboardBacklights.get(deviceId); 389 if (state != null && state.mLight.getId() == keyboardBacklight.getId()) { 390 return; 391 } 392 // The keyboard backlight was added or changed. 393 mKeyboardBacklights.put(deviceId, new KeyboardBacklightState(deviceId, keyboardBacklight)); 394 maybeRestoreBacklightBrightness(inputDevice, keyboardBacklight); 395 } 396 getInputDevice(int deviceId)397 private InputDevice getInputDevice(int deviceId) { 398 InputManager inputManager = mContext.getSystemService(InputManager.class); 399 return inputManager != null ? inputManager.getInputDevice(deviceId) : null; 400 } 401 getKeyboardBacklight(InputDevice inputDevice)402 private Light getKeyboardBacklight(InputDevice inputDevice) { 403 // Assuming each keyboard can have only single Light node for Keyboard backlight control 404 // for simplicity. 405 for (Light light : inputDevice.getLightsManager().getLights()) { 406 if (light.getType() == Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT 407 && light.hasBrightnessControl()) { 408 return light; 409 } 410 } 411 return null; 412 } 413 414 /** Register the keyboard backlight listener for a process. */ 415 @BinderThread 416 @Override registerKeyboardBacklightListener(IKeyboardBacklightListener listener, int pid)417 public void registerKeyboardBacklightListener(IKeyboardBacklightListener listener, 418 int pid) { 419 synchronized (mKeyboardBacklightListenerRecords) { 420 if (mKeyboardBacklightListenerRecords.get(pid) != null) { 421 throw new IllegalStateException("The calling process has already registered " 422 + "a KeyboardBacklightListener."); 423 } 424 KeyboardBacklightListenerRecord record = new KeyboardBacklightListenerRecord(pid, 425 listener); 426 try { 427 listener.asBinder().linkToDeath(record, 0); 428 } catch (RemoteException ex) { 429 throw new RuntimeException(ex); 430 } 431 mKeyboardBacklightListenerRecords.put(pid, record); 432 } 433 } 434 435 /** Unregister the keyboard backlight listener for a process. */ 436 @BinderThread 437 @Override unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener, int pid)438 public void unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener, 439 int pid) { 440 synchronized (mKeyboardBacklightListenerRecords) { 441 KeyboardBacklightListenerRecord record = mKeyboardBacklightListenerRecords.get(pid); 442 if (record == null) { 443 throw new IllegalStateException("The calling process has no registered " 444 + "KeyboardBacklightListener."); 445 } 446 if (record.mListener.asBinder() != listener.asBinder()) { 447 throw new IllegalStateException("The calling process has a different registered " 448 + "KeyboardBacklightListener."); 449 } 450 record.mListener.asBinder().unlinkToDeath(record, 0); 451 mKeyboardBacklightListenerRecords.remove(pid); 452 } 453 } 454 onKeyboardBacklightListenerDied(int pid)455 private void onKeyboardBacklightListenerDied(int pid) { 456 synchronized (mKeyboardBacklightListenerRecords) { 457 mKeyboardBacklightListenerRecords.remove(pid); 458 } 459 } 460 461 @VisibleForTesting onKeyboardBacklightUEvent(UEventObserver.UEvent event)462 public void onKeyboardBacklightUEvent(UEventObserver.UEvent event) { 463 if ("ADD".equalsIgnoreCase(event.get("ACTION")) && "LEDS".equalsIgnoreCase( 464 event.get("SUBSYSTEM"))) { 465 final String devPath = event.get("DEVPATH"); 466 if (isValidBacklightNodePath(devPath)) { 467 mNative.sysfsNodeChanged("/sys" + devPath); 468 } 469 } 470 } 471 updateAmbientLightListener()472 private void updateAmbientLightListener() { 473 if (!InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled()) { 474 return; 475 } 476 boolean needToListenAmbientLightSensor = false; 477 for (int i = 0; i < mKeyboardBacklights.size(); i++) { 478 needToListenAmbientLightSensor |= mKeyboardBacklights.valueAt(i).mUseAmbientController; 479 } 480 needToListenAmbientLightSensor &= mIsInteractive; 481 if (needToListenAmbientLightSensor && mAmbientListener == null) { 482 mAmbientListener = this::handleAmbientLightValueChanged; 483 mAmbientController.registerAmbientBacklightListener(mAmbientListener); 484 } 485 if (!needToListenAmbientLightSensor && mAmbientListener != null) { 486 mAmbientController.unregisterAmbientBacklightListener(mAmbientListener); 487 mAmbientListener = null; 488 } 489 } 490 isValidBacklightNodePath(String devPath)491 private static boolean isValidBacklightNodePath(String devPath) { 492 if (TextUtils.isEmpty(devPath)) { 493 return false; 494 } 495 int index = devPath.lastIndexOf('/'); 496 if (index < 0) { 497 return false; 498 } 499 String backlightNode = devPath.substring(index + 1); 500 devPath = devPath.substring(0, index); 501 if (!devPath.endsWith("leds") || !backlightNode.contains("kbd_backlight")) { 502 return false; 503 } 504 index = devPath.lastIndexOf('/'); 505 return index >= 0; 506 } 507 508 @Override dump(PrintWriter pw)509 public void dump(PrintWriter pw) { 510 IndentingPrintWriter ipw = new IndentingPrintWriter(pw); 511 ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights"); 512 ipw.increaseIndent(); 513 for (int i = 0; i < mKeyboardBacklights.size(); i++) { 514 KeyboardBacklightState state = mKeyboardBacklights.valueAt(i); 515 ipw.println(i + ": " + state.toString()); 516 } 517 ipw.decreaseIndent(); 518 } 519 520 // A record of a registered Keyboard backlight listener from one process. 521 private class KeyboardBacklightListenerRecord implements IBinder.DeathRecipient { 522 public final int mPid; 523 public final IKeyboardBacklightListener mListener; 524 KeyboardBacklightListenerRecord(int pid, IKeyboardBacklightListener listener)525 KeyboardBacklightListenerRecord(int pid, IKeyboardBacklightListener listener) { 526 mPid = pid; 527 mListener = listener; 528 } 529 530 @Override binderDied()531 public void binderDied() { 532 if (DEBUG) { 533 Slog.d(TAG, "Keyboard backlight listener for pid " + mPid + " died."); 534 } 535 onKeyboardBacklightListenerDied(mPid); 536 } 537 notifyKeyboardBacklightChanged(int deviceId, IKeyboardBacklightState state, boolean isTriggeredByKeyPress)538 public void notifyKeyboardBacklightChanged(int deviceId, IKeyboardBacklightState state, 539 boolean isTriggeredByKeyPress) { 540 try { 541 mListener.onBrightnessChanged(deviceId, state, isTriggeredByKeyPress); 542 } catch (RemoteException ex) { 543 Slog.w(TAG, "Failed to notify process " + mPid 544 + " that keyboard backlight changed, assuming it died.", ex); 545 binderDied(); 546 } 547 } 548 } 549 550 private class KeyboardBacklightState { 551 private final int mDeviceId; 552 private final Light mLight; 553 private int mBrightnessLevel; 554 private ValueAnimator mAnimator; 555 private final int[] mBrightnessValueForLevel; 556 private boolean mUseAmbientController = 557 InputFeatureFlagProvider.isAmbientKeyboardBacklightControlEnabled(); 558 KeyboardBacklightState(int deviceId, Light light)559 KeyboardBacklightState(int deviceId, Light light) { 560 mDeviceId = deviceId; 561 mLight = light; 562 mBrightnessValueForLevel = setupBrightnessLevels(); 563 } 564 setupBrightnessLevels()565 private int[] setupBrightnessLevels() { 566 if (!InputFeatureFlagProvider.isKeyboardBacklightCustomLevelsEnabled()) { 567 return DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL; 568 } 569 int[] customLevels = mLight.getPreferredBrightnessLevels(); 570 if (customLevels == null || customLevels.length == 0) { 571 return DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL; 572 } 573 TreeSet<Integer> brightnessLevels = new TreeSet<>(); 574 brightnessLevels.add(0); 575 for (int level : customLevels) { 576 if (level > 0 && level < MAX_BRIGHTNESS) { 577 brightnessLevels.add(level); 578 } 579 } 580 brightnessLevels.add(MAX_BRIGHTNESS); 581 int brightnessChangeSteps = brightnessLevels.size() - 1; 582 if (brightnessChangeSteps > MAX_BRIGHTNESS_CHANGE_STEPS) { 583 return DEFAULT_BRIGHTNESS_VALUE_FOR_LEVEL; 584 } 585 int[] result = new int[brightnessLevels.size()]; 586 int index = 0; 587 for (int val : brightnessLevels) { 588 result[index++] = val; 589 } 590 return result; 591 } 592 getNumBrightnessChangeSteps()593 private int getNumBrightnessChangeSteps() { 594 return mBrightnessValueForLevel.length - 1; 595 } 596 onBacklightStateChanged()597 private void onBacklightStateChanged() { 598 int toValue = mUseAmbientController ? mAmbientBacklightValue 599 : mBrightnessValueForLevel[mBrightnessLevel]; 600 setBacklightValue(mIsBacklightOn ? toValue : 0); 601 } setBrightnessLevel(int brightnessLevel)602 private void setBrightnessLevel(int brightnessLevel) { 603 // Once we manually set level, disregard ambient light controller 604 mUseAmbientController = false; 605 if (mIsBacklightOn) { 606 setBacklightValue(mBrightnessValueForLevel[brightnessLevel]); 607 } 608 mBrightnessLevel = brightnessLevel; 609 } 610 onAmbientBacklightValueChanged()611 private void onAmbientBacklightValueChanged() { 612 if (mIsBacklightOn && mUseAmbientController) { 613 setBacklightValue(mAmbientBacklightValue); 614 } 615 } 616 cancelAnimation()617 private void cancelAnimation() { 618 if (mAnimator != null && mAnimator.isRunning()) { 619 mAnimator.cancel(); 620 } 621 } 622 setBacklightValue(int toValue)623 private void setBacklightValue(int toValue) { 624 int fromValue = Color.alpha(mNative.getLightColor(mDeviceId, mLight.getId())); 625 if (fromValue == toValue) { 626 return; 627 } 628 if (InputFeatureFlagProvider.isKeyboardBacklightAnimationEnabled()) { 629 startAnimation(fromValue, toValue); 630 } else { 631 mNative.setLightColor(mDeviceId, mLight.getId(), Color.argb(toValue, 0, 0, 0)); 632 } 633 } 634 startAnimation(int fromValue, int toValue)635 private void startAnimation(int fromValue, int toValue) { 636 // Cancel any ongoing animation before starting a new one 637 cancelAnimation(); 638 mAnimator = mAnimatorFactory.makeIntAnimator(fromValue, toValue); 639 mAnimator.addUpdateListener( 640 (animation) -> mNative.setLightColor(mDeviceId, mLight.getId(), 641 Color.argb((int) animation.getAnimatedValue(), 0, 0, 0))); 642 mAnimator.setDuration(TRANSITION_ANIMATION_DURATION_MILLIS).start(); 643 } 644 645 @Override toString()646 public String toString() { 647 return "KeyboardBacklightState{Light=" + mLight.getId() 648 + ", BrightnessLevel=" + mBrightnessLevel 649 + "}"; 650 } 651 } 652 653 @VisibleForTesting 654 interface AnimatorFactory { makeIntAnimator(int from, int to)655 ValueAnimator makeIntAnimator(int from, int to); 656 } 657 } 658