1 /* 2 * Copyright 2023 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.annotation.MainThread; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.hardware.Sensor; 24 import android.hardware.SensorEvent; 25 import android.hardware.SensorEventListener; 26 import android.hardware.SensorManager; 27 import android.hardware.display.DisplayManager; 28 import android.hardware.display.DisplayManagerInternal; 29 import android.os.Handler; 30 import android.os.Looper; 31 import android.os.Message; 32 import android.util.Log; 33 import android.util.Slog; 34 import android.util.TypedValue; 35 import android.view.Display; 36 import android.view.DisplayInfo; 37 38 import com.android.internal.annotations.GuardedBy; 39 import com.android.internal.annotations.VisibleForTesting; 40 import com.android.server.LocalServices; 41 import com.android.server.display.utils.SensorUtils; 42 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.List; 46 import java.util.Objects; 47 48 /** 49 * A thread-safe component of {@link InputManagerService} responsible for managing the keyboard 50 * backlight based on ambient light sensor. 51 */ 52 final class AmbientKeyboardBacklightController implements DisplayManager.DisplayListener, 53 SensorEventListener { 54 55 private static final String TAG = "KbdBacklightController"; 56 57 // To enable these logs, run: 58 // 'adb shell setprop log.tag.KbdBacklightController DEBUG' (requires restart) 59 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 60 61 // Number of light sensor responses required to overcome temporal hysteresis. 62 @VisibleForTesting 63 public static final int HYSTERESIS_THRESHOLD = 2; 64 65 private static final int MSG_BRIGHTNESS_CALLBACK = 0; 66 private static final int MSG_SETUP_DISPLAY_AND_SENSOR = 1; 67 68 private static final Object sAmbientControllerLock = new Object(); 69 70 private final Context mContext; 71 private final Handler mHandler; 72 73 @Nullable 74 @GuardedBy("sAmbientControllerLock") 75 private Sensor mLightSensor; 76 @GuardedBy("sAmbientControllerLock") 77 private String mCurrentDefaultDisplayUniqueId; 78 79 // List of currently registered ambient backlight listeners 80 @GuardedBy("sAmbientControllerLock") 81 private final List<AmbientKeyboardBacklightListener> mAmbientKeyboardBacklightListeners = 82 new ArrayList<>(); 83 84 private BrightnessStep[] mBrightnessSteps; 85 private int mCurrentBrightnessStepIndex; 86 private HysteresisState mHysteresisState; 87 private int mHysteresisCount = 0; 88 private float mSmoothingConstant; 89 private int mSmoothedLux; 90 private int mSmoothedLuxAtLastAdjustment; 91 92 private enum HysteresisState { 93 // The most-recent mSmoothedLux matched mSmoothedLuxAtLastAdjustment. 94 STABLE, 95 // The most-recent mSmoothedLux was less than mSmoothedLuxAtLastAdjustment. 96 DECREASING, 97 // The most-recent mSmoothedLux was greater than mSmoothedLuxAtLastAdjustment. 98 INCREASING, 99 // The brightness should be adjusted immediately after the next sensor reading. 100 IMMEDIATE, 101 } 102 AmbientKeyboardBacklightController(Context context, Looper looper)103 AmbientKeyboardBacklightController(Context context, Looper looper) { 104 mContext = context; 105 mHandler = new Handler(looper, this::handleMessage); 106 initConfiguration(); 107 } 108 systemRunning()109 public void systemRunning() { 110 mHandler.sendEmptyMessage(MSG_SETUP_DISPLAY_AND_SENSOR); 111 DisplayManager displayManager = Objects.requireNonNull( 112 mContext.getSystemService(DisplayManager.class)); 113 displayManager.registerDisplayListener(this, mHandler); 114 } 115 registerAmbientBacklightListener(AmbientKeyboardBacklightListener listener)116 public void registerAmbientBacklightListener(AmbientKeyboardBacklightListener listener) { 117 synchronized (sAmbientControllerLock) { 118 if (mAmbientKeyboardBacklightListeners.contains(listener)) { 119 throw new IllegalStateException( 120 "AmbientKeyboardBacklightListener was already registered, listener = " 121 + listener); 122 } 123 if (mAmbientKeyboardBacklightListeners.isEmpty()) { 124 // Add sensor listener when we add the first ambient backlight listener. 125 addSensorListener(mLightSensor); 126 } 127 mAmbientKeyboardBacklightListeners.add(listener); 128 } 129 } 130 unregisterAmbientBacklightListener(AmbientKeyboardBacklightListener listener)131 public void unregisterAmbientBacklightListener(AmbientKeyboardBacklightListener listener) { 132 synchronized (sAmbientControllerLock) { 133 if (!mAmbientKeyboardBacklightListeners.contains(listener)) { 134 throw new IllegalStateException( 135 "AmbientKeyboardBacklightListener was never registered, listener = " 136 + listener); 137 } 138 mAmbientKeyboardBacklightListeners.remove(listener); 139 if (mAmbientKeyboardBacklightListeners.isEmpty()) { 140 removeSensorListener(mLightSensor); 141 } 142 } 143 } 144 sendBrightnessAdjustment(int brightnessValue)145 private void sendBrightnessAdjustment(int brightnessValue) { 146 Message msg = Message.obtain(mHandler, MSG_BRIGHTNESS_CALLBACK, brightnessValue); 147 mHandler.sendMessage(msg); 148 } 149 150 @MainThread handleBrightnessCallback(int brightnessValue)151 private void handleBrightnessCallback(int brightnessValue) { 152 synchronized (sAmbientControllerLock) { 153 for (AmbientKeyboardBacklightListener listener : mAmbientKeyboardBacklightListeners) { 154 listener.onKeyboardBacklightValueChanged(brightnessValue); 155 } 156 } 157 } 158 159 @MainThread handleAmbientLuxChange(float rawLux)160 private void handleAmbientLuxChange(float rawLux) { 161 if (rawLux < 0) { 162 Slog.w(TAG, "Light sensor doesn't have valid value"); 163 return; 164 } 165 updateSmoothedLux(rawLux); 166 167 if (mHysteresisState != HysteresisState.IMMEDIATE 168 && mSmoothedLux == mSmoothedLuxAtLastAdjustment) { 169 mHysteresisState = HysteresisState.STABLE; 170 return; 171 } 172 173 int newStepIndex = Math.max(0, mCurrentBrightnessStepIndex); 174 int numSteps = mBrightnessSteps.length; 175 176 if (mSmoothedLux > mSmoothedLuxAtLastAdjustment) { 177 if (mHysteresisState != HysteresisState.IMMEDIATE 178 && mHysteresisState != HysteresisState.INCREASING) { 179 if (DEBUG) { 180 Slog.d(TAG, "ALS transitioned to brightness increasing state"); 181 } 182 mHysteresisState = HysteresisState.INCREASING; 183 mHysteresisCount = 0; 184 } 185 for (; newStepIndex < numSteps; newStepIndex++) { 186 if (mSmoothedLux < mBrightnessSteps[newStepIndex].mIncreaseLuxThreshold) { 187 break; 188 } 189 } 190 } else if (mSmoothedLux < mSmoothedLuxAtLastAdjustment) { 191 if (mHysteresisState != HysteresisState.IMMEDIATE 192 && mHysteresisState != HysteresisState.DECREASING) { 193 if (DEBUG) { 194 Slog.d(TAG, "ALS transitioned to brightness decreasing state"); 195 } 196 mHysteresisState = HysteresisState.DECREASING; 197 mHysteresisCount = 0; 198 } 199 for (; newStepIndex >= 0; newStepIndex--) { 200 if (mSmoothedLux > mBrightnessSteps[newStepIndex].mDecreaseLuxThreshold) { 201 break; 202 } 203 } 204 } 205 206 if (mHysteresisState == HysteresisState.IMMEDIATE) { 207 mCurrentBrightnessStepIndex = newStepIndex; 208 mSmoothedLuxAtLastAdjustment = mSmoothedLux; 209 mHysteresisState = HysteresisState.STABLE; 210 mHysteresisCount = 0; 211 sendBrightnessAdjustment(mBrightnessSteps[newStepIndex].mBrightnessValue); 212 return; 213 } 214 215 if (newStepIndex == mCurrentBrightnessStepIndex) { 216 return; 217 } 218 219 mHysteresisCount++; 220 if (DEBUG) { 221 Slog.d(TAG, "Incremented hysteresis count to " + mHysteresisCount + " (lux went from " 222 + mSmoothedLuxAtLastAdjustment + " to " + mSmoothedLux + ")"); 223 } 224 if (mHysteresisCount >= HYSTERESIS_THRESHOLD) { 225 mCurrentBrightnessStepIndex = newStepIndex; 226 mSmoothedLuxAtLastAdjustment = mSmoothedLux; 227 mHysteresisCount = 1; 228 sendBrightnessAdjustment(mBrightnessSteps[newStepIndex].mBrightnessValue); 229 } 230 } 231 232 @MainThread handleDisplayChange()233 private void handleDisplayChange() { 234 DisplayManagerInternal displayManagerInternal = LocalServices.getService( 235 DisplayManagerInternal.class); 236 DisplayInfo displayInfo = displayManagerInternal.getDisplayInfo(Display.DEFAULT_DISPLAY); 237 if (displayInfo == null) { 238 return; 239 } 240 synchronized (sAmbientControllerLock) { 241 if (Objects.equals(mCurrentDefaultDisplayUniqueId, displayInfo.uniqueId)) { 242 return; 243 } 244 if (DEBUG) { 245 Slog.d(TAG, "Default display changed: resetting the light sensor"); 246 } 247 // Keep track of current default display 248 mCurrentDefaultDisplayUniqueId = displayInfo.uniqueId; 249 // Clear all existing sensor listeners 250 if (!mAmbientKeyboardBacklightListeners.isEmpty()) { 251 removeSensorListener(mLightSensor); 252 } 253 mLightSensor = getAmbientLightSensor( 254 displayManagerInternal.getAmbientLightSensorData(Display.DEFAULT_DISPLAY)); 255 // Re-add sensor listeners if required; 256 if (!mAmbientKeyboardBacklightListeners.isEmpty()) { 257 addSensorListener(mLightSensor); 258 } 259 } 260 } 261 getAmbientLightSensor( DisplayManagerInternal.AmbientLightSensorData ambientSensor)262 private Sensor getAmbientLightSensor( 263 DisplayManagerInternal.AmbientLightSensorData ambientSensor) { 264 SensorManager sensorManager = Objects.requireNonNull( 265 mContext.getSystemService(SensorManager.class)); 266 if (DEBUG) { 267 Slog.d(TAG, "Ambient Light sensor data: " + ambientSensor); 268 } 269 return SensorUtils.findSensor(sensorManager, ambientSensor.sensorType, 270 ambientSensor.sensorName, Sensor.TYPE_LIGHT); 271 } 272 updateSmoothedLux(float rawLux)273 private void updateSmoothedLux(float rawLux) { 274 // For the first sensor reading, use raw lux value directly without smoothing. 275 if (mHysteresisState == HysteresisState.IMMEDIATE) { 276 mSmoothedLux = (int) rawLux; 277 } else { 278 mSmoothedLux = 279 (int) (mSmoothingConstant * rawLux + (1 - mSmoothingConstant) * mSmoothedLux); 280 } 281 if (DEBUG) { 282 Slog.d(TAG, "Current smoothed lux from ALS = " + mSmoothedLux); 283 } 284 } 285 286 @VisibleForTesting addSensorListener(@ullable Sensor sensor)287 public void addSensorListener(@Nullable Sensor sensor) { 288 SensorManager sensorManager = mContext.getSystemService(SensorManager.class); 289 if (sensorManager == null || sensor == null) { 290 return; 291 } 292 // Reset values before registering listener 293 reset(); 294 sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL, mHandler); 295 if (DEBUG) { 296 Slog.d(TAG, "Registering ALS listener"); 297 } 298 } 299 removeSensorListener(@ullable Sensor sensor)300 private void removeSensorListener(@Nullable Sensor sensor) { 301 SensorManager sensorManager = mContext.getSystemService(SensorManager.class); 302 if (sensorManager == null || sensor == null) { 303 return; 304 } 305 sensorManager.unregisterListener(this, sensor); 306 if (DEBUG) { 307 Slog.d(TAG, "Unregistering ALS listener"); 308 } 309 } 310 initConfiguration()311 private void initConfiguration() { 312 Resources res = mContext.getResources(); 313 int[] brightnessValueArray = res.getIntArray( 314 com.android.internal.R.array.config_autoKeyboardBacklightBrightnessValues); 315 int[] decreaseThresholdArray = res.getIntArray( 316 com.android.internal.R.array.config_autoKeyboardBacklightDecreaseLuxThreshold); 317 int[] increaseThresholdArray = res.getIntArray( 318 com.android.internal.R.array.config_autoKeyboardBacklightIncreaseLuxThreshold); 319 if (brightnessValueArray.length != decreaseThresholdArray.length 320 || decreaseThresholdArray.length != increaseThresholdArray.length) { 321 throw new IllegalArgumentException( 322 "The config files for auto keyboard backlight brightness must contain arrays " 323 + "of equal lengths"); 324 } 325 final int size = brightnessValueArray.length; 326 mBrightnessSteps = new BrightnessStep[size]; 327 for (int i = 0; i < size; i++) { 328 int increaseThreshold = 329 increaseThresholdArray[i] < 0 ? Integer.MAX_VALUE : increaseThresholdArray[i]; 330 int decreaseThreshold = 331 decreaseThresholdArray[i] < 0 ? Integer.MIN_VALUE : decreaseThresholdArray[i]; 332 mBrightnessSteps[i] = new BrightnessStep(brightnessValueArray[i], increaseThreshold, 333 decreaseThreshold); 334 } 335 336 int numSteps = mBrightnessSteps.length; 337 if (numSteps == 0 || mBrightnessSteps[0].mDecreaseLuxThreshold != Integer.MIN_VALUE 338 || mBrightnessSteps[numSteps - 1].mIncreaseLuxThreshold != Integer.MAX_VALUE) { 339 throw new IllegalArgumentException( 340 "The config files for auto keyboard backlight brightness must contain arrays " 341 + "of length > 0 and have -1 or Integer.MIN_VALUE as lower bound for " 342 + "decrease thresholds and -1 or Integer.MAX_VALUE as upper bound for " 343 + "increase thresholds"); 344 } 345 346 final TypedValue smoothingConstantValue = new TypedValue(); 347 res.getValue( 348 com.android.internal.R.dimen.config_autoKeyboardBrightnessSmoothingConstant, 349 smoothingConstantValue, 350 true /*resolveRefs*/); 351 mSmoothingConstant = smoothingConstantValue.getFloat(); 352 if (mSmoothingConstant <= 0.0 || mSmoothingConstant > 1.0) { 353 throw new IllegalArgumentException( 354 "The config files for auto keyboard backlight brightness must contain " 355 + "smoothing constant in range (0.0, 1.0]."); 356 } 357 358 if (DEBUG) { 359 Log.d(TAG, "Brightness steps: " + Arrays.toString(mBrightnessSteps) 360 + " Smoothing constant = " + mSmoothingConstant); 361 } 362 } 363 364 private void reset() { 365 mHysteresisState = HysteresisState.IMMEDIATE; 366 mSmoothedLux = 0; 367 mSmoothedLuxAtLastAdjustment = 0; 368 mCurrentBrightnessStepIndex = -1; 369 } 370 371 private boolean handleMessage(Message msg) { 372 switch (msg.what) { 373 case MSG_BRIGHTNESS_CALLBACK: 374 handleBrightnessCallback((int) msg.obj); 375 return true; 376 case MSG_SETUP_DISPLAY_AND_SENSOR: 377 handleDisplayChange(); 378 return true; 379 } 380 return false; 381 } 382 383 @Override 384 public void onSensorChanged(SensorEvent event) { 385 handleAmbientLuxChange(event.values[0]); 386 } 387 388 @Override 389 public void onAccuracyChanged(Sensor sensor, int accuracy) { 390 } 391 392 @Override 393 public void onDisplayAdded(int displayId) { 394 handleDisplayChange(); 395 } 396 397 @Override 398 public void onDisplayRemoved(int displayId) { 399 handleDisplayChange(); 400 } 401 402 @Override 403 public void onDisplayChanged(int displayId) { 404 handleDisplayChange(); 405 } 406 407 public interface AmbientKeyboardBacklightListener { 408 /** 409 * @param value between [0, 255] to which keyboard backlight needs to be set according 410 * to Ambient light sensor. 411 */ 412 void onKeyboardBacklightValueChanged(int value); 413 } 414 415 private static class BrightnessStep { 416 private final int mBrightnessValue; 417 private final int mIncreaseLuxThreshold; 418 private final int mDecreaseLuxThreshold; 419 420 private BrightnessStep(int brightnessValue, int increaseLuxThreshold, 421 int decreaseLuxThreshold) { 422 mBrightnessValue = brightnessValue; 423 mIncreaseLuxThreshold = increaseLuxThreshold; 424 mDecreaseLuxThreshold = decreaseLuxThreshold; 425 } 426 427 @Override 428 public String toString() { 429 return "BrightnessStep{" + "mBrightnessValue=" + mBrightnessValue 430 + ", mIncreaseThreshold=" + mIncreaseLuxThreshold + ", mDecreaseThreshold=" 431 + mDecreaseLuxThreshold + '}'; 432 } 433 } 434 } 435