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         synchronized (sAmbientControllerLock) {
238             if (Objects.equals(mCurrentDefaultDisplayUniqueId, displayInfo.uniqueId)) {
239                 return;
240             }
241             if (DEBUG) {
242                 Slog.d(TAG, "Default display changed: resetting the light sensor");
243             }
244             // Keep track of current default display
245             mCurrentDefaultDisplayUniqueId = displayInfo.uniqueId;
246             // Clear all existing sensor listeners
247             if (!mAmbientKeyboardBacklightListeners.isEmpty()) {
248                 removeSensorListener(mLightSensor);
249             }
250             mLightSensor = getAmbientLightSensor(
251                     displayManagerInternal.getAmbientLightSensorData(Display.DEFAULT_DISPLAY));
252             // Re-add sensor listeners if required;
253             if (!mAmbientKeyboardBacklightListeners.isEmpty()) {
254                 addSensorListener(mLightSensor);
255             }
256         }
257     }
258 
getAmbientLightSensor( DisplayManagerInternal.AmbientLightSensorData ambientSensor)259     private Sensor getAmbientLightSensor(
260             DisplayManagerInternal.AmbientLightSensorData ambientSensor) {
261         SensorManager sensorManager = Objects.requireNonNull(
262                 mContext.getSystemService(SensorManager.class));
263         if (DEBUG) {
264             Slog.d(TAG, "Ambient Light sensor data: " + ambientSensor);
265         }
266         return SensorUtils.findSensor(sensorManager, ambientSensor.sensorType,
267                 ambientSensor.sensorName, Sensor.TYPE_LIGHT);
268     }
269 
updateSmoothedLux(float rawLux)270     private void updateSmoothedLux(float rawLux) {
271         // For the first sensor reading, use raw lux value directly without smoothing.
272         if (mHysteresisState == HysteresisState.IMMEDIATE) {
273             mSmoothedLux = (int) rawLux;
274         } else {
275             mSmoothedLux =
276                     (int) (mSmoothingConstant * rawLux + (1 - mSmoothingConstant) * mSmoothedLux);
277         }
278         if (DEBUG) {
279             Slog.d(TAG, "Current smoothed lux from ALS = " + mSmoothedLux);
280         }
281     }
282 
283     @VisibleForTesting
addSensorListener(@ullable Sensor sensor)284     public void addSensorListener(@Nullable Sensor sensor) {
285         SensorManager sensorManager = mContext.getSystemService(SensorManager.class);
286         if (sensorManager == null || sensor == null) {
287             return;
288         }
289         // Reset values before registering listener
290         reset();
291         sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL, mHandler);
292         if (DEBUG) {
293             Slog.d(TAG, "Registering ALS listener");
294         }
295     }
296 
removeSensorListener(@ullable Sensor sensor)297     private void removeSensorListener(@Nullable Sensor sensor) {
298         SensorManager sensorManager = mContext.getSystemService(SensorManager.class);
299         if (sensorManager == null || sensor == null) {
300             return;
301         }
302         sensorManager.unregisterListener(this, sensor);
303         if (DEBUG) {
304             Slog.d(TAG, "Unregistering ALS listener");
305         }
306     }
307 
initConfiguration()308     private void initConfiguration() {
309         Resources res = mContext.getResources();
310         int[] brightnessValueArray = res.getIntArray(
311                 com.android.internal.R.array.config_autoKeyboardBacklightBrightnessValues);
312         int[] decreaseThresholdArray = res.getIntArray(
313                 com.android.internal.R.array.config_autoKeyboardBacklightDecreaseLuxThreshold);
314         int[] increaseThresholdArray = res.getIntArray(
315                 com.android.internal.R.array.config_autoKeyboardBacklightIncreaseLuxThreshold);
316         if (brightnessValueArray.length != decreaseThresholdArray.length
317                 || decreaseThresholdArray.length != increaseThresholdArray.length) {
318             throw new IllegalArgumentException(
319                     "The config files for auto keyboard backlight brightness must contain arrays "
320                             + "of equal lengths");
321         }
322         final int size = brightnessValueArray.length;
323         mBrightnessSteps = new BrightnessStep[size];
324         for (int i = 0; i < size; i++) {
325             int increaseThreshold =
326                     increaseThresholdArray[i] < 0 ? Integer.MAX_VALUE : increaseThresholdArray[i];
327             int decreaseThreshold =
328                     decreaseThresholdArray[i] < 0 ? Integer.MIN_VALUE : decreaseThresholdArray[i];
329             mBrightnessSteps[i] = new BrightnessStep(brightnessValueArray[i], increaseThreshold,
330                     decreaseThreshold);
331         }
332 
333         int numSteps = mBrightnessSteps.length;
334         if (numSteps == 0 || mBrightnessSteps[0].mDecreaseLuxThreshold != Integer.MIN_VALUE
335                 || mBrightnessSteps[numSteps - 1].mIncreaseLuxThreshold != Integer.MAX_VALUE) {
336             throw new IllegalArgumentException(
337                     "The config files for auto keyboard backlight brightness must contain arrays "
338                             + "of length > 0 and have -1 or Integer.MIN_VALUE as lower bound for "
339                             + "decrease thresholds and -1 or Integer.MAX_VALUE as upper bound for "
340                             + "increase thresholds");
341         }
342 
343         final TypedValue smoothingConstantValue = new TypedValue();
344         res.getValue(
345                 com.android.internal.R.dimen.config_autoKeyboardBrightnessSmoothingConstant,
346                 smoothingConstantValue,
347                 true /*resolveRefs*/);
348         mSmoothingConstant = smoothingConstantValue.getFloat();
349         if (mSmoothingConstant <= 0.0 || mSmoothingConstant > 1.0) {
350             throw new IllegalArgumentException(
351                     "The config files for auto keyboard backlight brightness must contain "
352                             + "smoothing constant in range (0.0, 1.0].");
353         }
354 
355         if (DEBUG) {
356             Log.d(TAG, "Brightness steps: " + Arrays.toString(mBrightnessSteps)
357                     + " Smoothing constant = " + mSmoothingConstant);
358         }
359     }
360 
361     private void reset() {
362         mHysteresisState = HysteresisState.IMMEDIATE;
363         mSmoothedLux = 0;
364         mSmoothedLuxAtLastAdjustment = 0;
365         mCurrentBrightnessStepIndex = -1;
366     }
367 
368     private boolean handleMessage(Message msg) {
369         switch (msg.what) {
370             case MSG_BRIGHTNESS_CALLBACK:
371                 handleBrightnessCallback((int) msg.obj);
372                 return true;
373             case MSG_SETUP_DISPLAY_AND_SENSOR:
374                 handleDisplayChange();
375                 return true;
376         }
377         return false;
378     }
379 
380     @Override
381     public void onSensorChanged(SensorEvent event) {
382         handleAmbientLuxChange(event.values[0]);
383     }
384 
385     @Override
386     public void onAccuracyChanged(Sensor sensor, int accuracy) {
387     }
388 
389     @Override
390     public void onDisplayAdded(int displayId) {
391         handleDisplayChange();
392     }
393 
394     @Override
395     public void onDisplayRemoved(int displayId) {
396         handleDisplayChange();
397     }
398 
399     @Override
400     public void onDisplayChanged(int displayId) {
401         handleDisplayChange();
402     }
403 
404     public interface AmbientKeyboardBacklightListener {
405         /**
406          * @param value between [0, 255] to which keyboard backlight needs to be set according
407          *              to Ambient light sensor.
408          */
409         void onKeyboardBacklightValueChanged(int value);
410     }
411 
412     private static class BrightnessStep {
413         private final int mBrightnessValue;
414         private final int mIncreaseLuxThreshold;
415         private final int mDecreaseLuxThreshold;
416 
417         private BrightnessStep(int brightnessValue, int increaseLuxThreshold,
418                 int decreaseLuxThreshold) {
419             mBrightnessValue = brightnessValue;
420             mIncreaseLuxThreshold = increaseLuxThreshold;
421             mDecreaseLuxThreshold = decreaseLuxThreshold;
422         }
423 
424         @Override
425         public String toString() {
426             return "BrightnessStep{" + "mBrightnessValue=" + mBrightnessValue
427                     + ", mIncreaseThreshold=" + mIncreaseLuxThreshold + ", mDecreaseThreshold="
428                     + mDecreaseLuxThreshold + '}';
429         }
430     }
431 }
432