1 /*
2  * Copyright (C) 2016 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.display.config;
18 
19 import android.annotation.ArrayRes;
20 import android.annotation.Nullable;
21 import android.content.res.Resources;
22 import android.util.Pair;
23 import android.util.Slog;
24 
25 import com.android.internal.annotations.VisibleForTesting;
26 import com.android.server.display.utils.DebugUtils;
27 
28 import java.util.Arrays;
29 import java.util.List;
30 
31 /**
32  * A helper class for handling access to illuminance hysteresis level values.
33  */
34 public class HysteresisLevels {
35     private static final String TAG = "HysteresisLevels";
36 
37     private static final float[] DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS = new float[]{100f};
38     private static final float[] DEFAULT_AMBIENT_DARKENING_THRESHOLDS = new float[]{200f};
39     private static final float[] DEFAULT_AMBIENT_THRESHOLD_LEVELS = new float[]{0f};
40     private static final float[] DEFAULT_SCREEN_THRESHOLD_LEVELS = new float[]{0f};
41     private static final float[] DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS = new float[]{100f};
42     private static final float[] DEFAULT_SCREEN_DARKENING_THRESHOLDS = new float[]{200f};
43 
44     // To enable these logs, run:
45     // 'adb shell setprop persist.log.tag.HysteresisLevels DEBUG && adb reboot'
46     private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
47 
48     /**
49      * The array that describes the brightness threshold percentage change
50      * at each brightness level described in mBrighteningThresholdLevels.
51      */
52     private final float[] mBrighteningThresholdsPercentages;
53 
54     /**
55      * The array that describes the brightness threshold percentage change
56      * at each brightness level described in mDarkeningThresholdLevels.
57      */
58     private final float[] mDarkeningThresholdsPercentages;
59 
60     /**
61      * The array that describes the range of brightness that each threshold percentage applies to
62      *
63      * The (zero-based) index is calculated as follows
64      * value = current brightness value
65      * level = mBrighteningThresholdLevels
66      *
67      * condition                       return
68      * value < mBrighteningThresholdLevels[0]                = 0.0f
69      * level[n] <= value < level[n+1]  = mBrighteningThresholdsPercentages[n]
70      * level[MAX] <= value             = mBrighteningThresholdsPercentages[MAX]
71      */
72     private final float[] mBrighteningThresholdLevels;
73 
74     /**
75      * The array that describes the range of brightness that each threshold percentage applies to
76      *
77      * The (zero-based) index is calculated as follows
78      * value = current brightness value
79      * level = mDarkeningThresholdLevels
80      *
81      * condition                       return
82      * value < level[0]                = 0.0f
83      * level[n] <= value < level[n+1]  = mDarkeningThresholdsPercentages[n]
84      * level[MAX] <= value             = mDarkeningThresholdsPercentages[MAX]
85      */
86     private final float[] mDarkeningThresholdLevels;
87 
88     /**
89      * The minimum value decrease for darkening event
90      */
91     private final float mMinDarkening;
92 
93     /**
94      * The minimum value increase for brightening event.
95      */
96     private final float mMinBrightening;
97 
98     /**
99      * Creates a {@code HysteresisLevels} object with the given equal-length
100      * float arrays.
101      *
102      * @param brighteningThresholdsPercentages 0-100 of thresholds
103      * @param darkeningThresholdsPercentages   0-100 of thresholds
104      * @param brighteningThresholdLevels       float array of brightness values in the relevant
105      *                                         units
106      * @param minBrighteningThreshold          the minimum value for which the brightening value
107      *                                         needs to
108      *                                         return.
109      * @param minDarkeningThreshold            the minimum value for which the darkening value needs
110      *                                         to return.
111      */
112     @VisibleForTesting
HysteresisLevels(float[] brighteningThresholdsPercentages, float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels, float[] darkeningThresholdLevels, float minDarkeningThreshold, float minBrighteningThreshold)113     public HysteresisLevels(float[] brighteningThresholdsPercentages,
114             float[] darkeningThresholdsPercentages,
115             float[] brighteningThresholdLevels, float[] darkeningThresholdLevels,
116             float minDarkeningThreshold, float minBrighteningThreshold) {
117         if (brighteningThresholdsPercentages.length != brighteningThresholdLevels.length
118                 || darkeningThresholdsPercentages.length != darkeningThresholdLevels.length) {
119             throw new IllegalArgumentException("Mismatch between hysteresis array lengths.");
120         }
121         mBrighteningThresholdsPercentages =
122                 setArrayFormat(brighteningThresholdsPercentages, 100.0f);
123         mDarkeningThresholdsPercentages =
124                 setArrayFormat(darkeningThresholdsPercentages, 100.0f);
125         mBrighteningThresholdLevels = setArrayFormat(brighteningThresholdLevels, 1.0f);
126         mDarkeningThresholdLevels = setArrayFormat(darkeningThresholdLevels, 1.0f);
127         mMinDarkening = minDarkeningThreshold;
128         mMinBrightening = minBrighteningThreshold;
129     }
130 
131     /**
132      * Return the brightening hysteresis threshold for the given value level.
133      */
getBrighteningThreshold(float value)134     public float getBrighteningThreshold(float value) {
135         final float brightConstant = getReferenceLevel(value,
136                 mBrighteningThresholdLevels, mBrighteningThresholdsPercentages);
137 
138         float brightThreshold = value * (1.0f + brightConstant);
139         if (DEBUG) {
140             Slog.d(TAG, "bright hysteresis constant=" + brightConstant + ", threshold="
141                     + brightThreshold + ", value=" + value);
142         }
143 
144         brightThreshold = Math.max(brightThreshold, value + mMinBrightening);
145         return brightThreshold;
146     }
147 
148     /**
149      * Return the darkening hysteresis threshold for the given value level.
150      */
getDarkeningThreshold(float value)151     public float getDarkeningThreshold(float value) {
152         final float darkConstant = getReferenceLevel(value,
153                 mDarkeningThresholdLevels, mDarkeningThresholdsPercentages);
154         float darkThreshold = value * (1.0f - darkConstant);
155         if (DEBUG) {
156             Slog.d(TAG, "dark hysteresis constant=: " + darkConstant + ", threshold="
157                     + darkThreshold + ", value=" + value);
158         }
159         darkThreshold = Math.min(darkThreshold, value - mMinDarkening);
160         return Math.max(darkThreshold, 0.0f);
161     }
162 
163     @VisibleForTesting
getBrighteningThresholdsPercentages()164     public float[] getBrighteningThresholdsPercentages() {
165         return mBrighteningThresholdsPercentages;
166     }
167 
168     @VisibleForTesting
getDarkeningThresholdsPercentages()169     public float[] getDarkeningThresholdsPercentages() {
170         return mDarkeningThresholdsPercentages;
171     }
172 
173     @VisibleForTesting
getBrighteningThresholdLevels()174     public float[] getBrighteningThresholdLevels() {
175         return mBrighteningThresholdLevels;
176     }
177 
178     @VisibleForTesting
getDarkeningThresholdLevels()179     public float[] getDarkeningThresholdLevels() {
180         return mDarkeningThresholdLevels;
181     }
182 
183     @VisibleForTesting
getMinDarkening()184     public float getMinDarkening() {
185         return mMinDarkening;
186     }
187 
188     @VisibleForTesting
getMinBrightening()189     public float getMinBrightening() {
190         return mMinBrightening;
191     }
192 
193     /**
194      * Return the hysteresis constant for the closest threshold value from the given array.
195      */
getReferenceLevel(float value, float[] thresholdLevels, float[] thresholdPercentages)196     private float getReferenceLevel(float value, float[] thresholdLevels,
197             float[] thresholdPercentages) {
198         if (thresholdLevels == null || thresholdLevels.length == 0 || value < thresholdLevels[0]) {
199             return 0.0f;
200         }
201         int index = 0;
202         while (index < thresholdLevels.length - 1 && value >= thresholdLevels[index + 1]) {
203             index++;
204         }
205         return thresholdPercentages[index];
206     }
207 
208     /**
209      * Return a float array where each i-th element equals {@code configArray[i]/divideFactor}.
210      */
setArrayFormat(float[] configArray, float divideFactor)211     private float[] setArrayFormat(float[] configArray, float divideFactor) {
212         float[] levelArray = new float[configArray.length];
213         for (int index = 0; levelArray.length > index; ++index) {
214             levelArray[index] = configArray[index] / divideFactor;
215         }
216         return levelArray;
217     }
218 
219     @Override
toString()220     public String toString() {
221         return "HysteresisLevels {"
222                 + "\n"
223                 + "    mBrighteningThresholdLevels=" + Arrays.toString(mBrighteningThresholdLevels)
224                 + ",\n"
225                 + "    mBrighteningThresholdsPercentages="
226                 + Arrays.toString(mBrighteningThresholdsPercentages)
227                 + ",\n"
228                 + "    mMinBrightening=" + mMinBrightening
229                 + ",\n"
230                 + "    mDarkeningThresholdLevels=" + Arrays.toString(mDarkeningThresholdLevels)
231                 + ",\n"
232                 + "    mDarkeningThresholdsPercentages="
233                 + Arrays.toString(mDarkeningThresholdsPercentages)
234                 + ",\n"
235                 + "    mMinDarkening=" + mMinDarkening
236                 + "\n"
237                 + "}";
238     }
239 
240     /**
241      * Creates hysteresis levels for Active Ambient Lux
242      */
loadAmbientBrightnessConfig( @ullable DisplayConfiguration config, @Nullable Resources resources)243     public static HysteresisLevels loadAmbientBrightnessConfig(
244             @Nullable DisplayConfiguration config, @Nullable Resources resources) {
245         return createHysteresisLevels(
246                 config == null ? null : config.getAmbientBrightnessChangeThresholds(),
247                 com.android.internal.R.array.config_ambientThresholdLevels,
248                 com.android.internal.R.array.config_ambientBrighteningThresholds,
249                 com.android.internal.R.array.config_ambientDarkeningThresholds,
250                 DEFAULT_AMBIENT_THRESHOLD_LEVELS,
251                 DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS,
252                 DEFAULT_AMBIENT_DARKENING_THRESHOLDS,
253                 resources, /* potentialOldBrightnessScale= */ false);
254     }
255 
256     /**
257      * Creates hysteresis levels for Active Screen Brightness
258      */
loadDisplayBrightnessConfig( @ullable DisplayConfiguration config, @Nullable Resources resources)259     public static HysteresisLevels loadDisplayBrightnessConfig(
260             @Nullable DisplayConfiguration config, @Nullable Resources resources) {
261         return createHysteresisLevels(
262                 config == null ? null : config.getDisplayBrightnessChangeThresholds(),
263                 com.android.internal.R.array.config_screenThresholdLevels,
264                 com.android.internal.R.array.config_screenBrighteningThresholds,
265                 com.android.internal.R.array.config_screenDarkeningThresholds,
266                 DEFAULT_SCREEN_THRESHOLD_LEVELS,
267                 DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS,
268                 DEFAULT_SCREEN_DARKENING_THRESHOLDS,
269                 resources, /* potentialOldBrightnessScale= */ true);
270     }
271 
272     /**
273      * Creates hysteresis levels for Idle Ambient Lux
274      */
loadAmbientBrightnessIdleConfig( @ullable DisplayConfiguration config, @Nullable Resources resources)275     public static HysteresisLevels loadAmbientBrightnessIdleConfig(
276             @Nullable DisplayConfiguration config, @Nullable Resources resources) {
277         return createHysteresisLevels(
278                 config == null ? null : config.getAmbientBrightnessChangeThresholdsIdle(),
279                 com.android.internal.R.array.config_ambientThresholdLevels,
280                 com.android.internal.R.array.config_ambientBrighteningThresholds,
281                 com.android.internal.R.array.config_ambientDarkeningThresholds,
282                 DEFAULT_AMBIENT_THRESHOLD_LEVELS,
283                 DEFAULT_AMBIENT_BRIGHTENING_THRESHOLDS,
284                 DEFAULT_AMBIENT_DARKENING_THRESHOLDS,
285                 resources, /* potentialOldBrightnessScale= */ false);
286     }
287 
288     /**
289      * Creates hysteresis levels for Idle Screen Brightness
290      */
loadDisplayBrightnessIdleConfig( @ullable DisplayConfiguration config, @Nullable Resources resources)291     public static HysteresisLevels loadDisplayBrightnessIdleConfig(
292             @Nullable DisplayConfiguration config, @Nullable Resources resources) {
293         return createHysteresisLevels(
294                 config == null ? null : config.getDisplayBrightnessChangeThresholdsIdle(),
295                 com.android.internal.R.array.config_screenThresholdLevels,
296                 com.android.internal.R.array.config_screenBrighteningThresholds,
297                 com.android.internal.R.array.config_screenDarkeningThresholds,
298                 DEFAULT_SCREEN_THRESHOLD_LEVELS,
299                 DEFAULT_SCREEN_BRIGHTENING_THRESHOLDS,
300                 DEFAULT_SCREEN_DARKENING_THRESHOLDS,
301                 resources, /* potentialOldBrightnessScale= */ true);
302     }
303 
304 
createHysteresisLevels( @ullable Thresholds thresholds, @ArrayRes int configLevels, @ArrayRes int configBrighteningThresholds, @ArrayRes int configDarkeningThresholds, float[] defaultLevels, float[] defaultBrighteningThresholds, float[] defaultDarkeningThresholds, @Nullable Resources resources, boolean potentialOldBrightnessScale )305     private static HysteresisLevels createHysteresisLevels(
306             @Nullable Thresholds thresholds,
307             @ArrayRes int configLevels,
308             @ArrayRes int configBrighteningThresholds,
309             @ArrayRes int configDarkeningThresholds,
310             float[] defaultLevels,
311             float[] defaultBrighteningThresholds,
312             float[] defaultDarkeningThresholds,
313             @Nullable Resources resources,
314             boolean potentialOldBrightnessScale
315     ) {
316         BrightnessThresholds brighteningThresholds =
317                 thresholds == null ? null : thresholds.getBrighteningThresholds();
318         BrightnessThresholds darkeningThresholds =
319                 thresholds == null ? null : thresholds.getDarkeningThresholds();
320 
321         Pair<float[], float[]> brighteningPair = getBrightnessLevelAndPercentage(
322                 brighteningThresholds,
323                 configLevels, configBrighteningThresholds,
324                 defaultLevels, defaultBrighteningThresholds,
325                 potentialOldBrightnessScale, resources);
326 
327         Pair<float[], float[]> darkeningPair = getBrightnessLevelAndPercentage(
328                 darkeningThresholds,
329                 configLevels, configDarkeningThresholds,
330                 defaultLevels, defaultDarkeningThresholds,
331                 potentialOldBrightnessScale, resources);
332 
333         float brighteningMinThreshold =
334                 brighteningThresholds != null && brighteningThresholds.getMinimum() != null
335                         ? brighteningThresholds.getMinimum().floatValue() : 0f;
336         float darkeningMinThreshold =
337                 darkeningThresholds != null && darkeningThresholds.getMinimum() != null
338                         ? darkeningThresholds.getMinimum().floatValue() : 0f;
339 
340         return new HysteresisLevels(
341                 brighteningPair.second,
342                 darkeningPair.second,
343                 brighteningPair.first,
344                 darkeningPair.first,
345                 darkeningMinThreshold,
346                 brighteningMinThreshold
347         );
348     }
349 
350     // Returns two float arrays, one of the brightness levels and one of the corresponding threshold
351     // percentages for brightness levels at or above the lux value.
352     // Historically, config.xml would have an array for brightness levels that was 1 shorter than
353     // the levels array. Now we prepend a 0 to this array so they can be treated the same in the
354     // rest of the framework. Values were also defined in different units (permille vs percent).
getBrightnessLevelAndPercentage( @ullable BrightnessThresholds thresholds, int configFallbackThreshold, int configFallbackPermille, float[] defaultLevels, float[] defaultPercentage, boolean potentialOldBrightnessScale, @Nullable Resources resources)355     private static Pair<float[], float[]> getBrightnessLevelAndPercentage(
356             @Nullable BrightnessThresholds thresholds,
357             int configFallbackThreshold, int configFallbackPermille,
358             float[] defaultLevels, float[] defaultPercentage, boolean potentialOldBrightnessScale,
359             @Nullable Resources resources) {
360         if (thresholds != null
361                 && thresholds.getBrightnessThresholdPoints() != null
362                 && !thresholds.getBrightnessThresholdPoints().getBrightnessThresholdPoint()
363                 .isEmpty()) {
364 
365             // The level and percentages arrays are equal length in the ddc (new system)
366             List<ThresholdPoint> points =
367                     thresholds.getBrightnessThresholdPoints().getBrightnessThresholdPoint();
368             final int size = points.size();
369 
370             float[] thresholdLevels = new float[size];
371             float[] thresholdPercentages = new float[size];
372 
373             int i = 0;
374             for (ThresholdPoint point : points) {
375                 thresholdLevels[i] = point.getThreshold().floatValue();
376                 thresholdPercentages[i] = point.getPercentage().floatValue();
377                 i++;
378             }
379             return new Pair<>(thresholdLevels, thresholdPercentages);
380         } else if (resources != null) {
381             // The level and percentages arrays are unequal length in config.xml (old system)
382             // We prefix the array with a 0 value to ensure they can be handled consistently
383             // with the new system.
384 
385             // Load levels array
386             int[] configThresholdArray = resources.getIntArray(configFallbackThreshold);
387             int configThresholdsSize;
388             // null check is not needed here, however it test we are mocking resources that might
389             // return null
390             if (configThresholdArray == null || configThresholdArray.length == 0) {
391                 configThresholdsSize = 1;
392             } else {
393                 configThresholdsSize = configThresholdArray.length + 1;
394             }
395 
396             // Load percentage array
397             int[] configPermille = resources.getIntArray(configFallbackPermille);
398 
399             // Ensure lengths match up
400             // null check is not needed here, however it test we are mocking resources that might
401             // return null
402             boolean emptyArray = configPermille == null || configPermille.length == 0;
403             if (emptyArray && configThresholdsSize == 1) {
404                 return new Pair<>(defaultLevels, defaultPercentage);
405             }
406             if (emptyArray || configPermille.length != configThresholdsSize) {
407                 throw new IllegalArgumentException(
408                         "Brightness threshold arrays do not align in length");
409             }
410 
411             // Calculate levels array
412             float[] configThresholdWithZeroPrefixed = new float[configThresholdsSize];
413             // Start at 1, so that 0 index value is 0.0f (default)
414             for (int i = 1; i < configThresholdsSize; i++) {
415                 configThresholdWithZeroPrefixed[i] = (float) configThresholdArray[i - 1];
416             }
417             if (potentialOldBrightnessScale) {
418                 configThresholdWithZeroPrefixed =
419                         constraintInRangeIfNeeded(configThresholdWithZeroPrefixed);
420             }
421 
422             // Calculate percentages array
423             float[] configPercentage = new float[configThresholdsSize];
424             for (int i = 0; i < configPermille.length; i++) {
425                 configPercentage[i] = configPermille[i] / 10.0f;
426             }
427             return new Pair<>(configThresholdWithZeroPrefixed, configPercentage);
428         } else {
429             return new Pair<>(defaultLevels, defaultPercentage);
430         }
431     }
432 
433     /**
434      * This check is due to historical reasons, where screen thresholdLevels used to be
435      * integer values in the range of [0-255], but then was changed to be float values from [0,1].
436      * To accommodate both the possibilities, we first check if all the thresholdLevels are in
437      * [0,1], and if not, we divide all the levels with 255 to bring them down to the same scale.
438      */
constraintInRangeIfNeeded(float[] thresholdLevels)439     private static float[] constraintInRangeIfNeeded(float[] thresholdLevels) {
440         if (isAllInRange(thresholdLevels, /* minValueInclusive= */ 0.0f,
441                 /* maxValueInclusive= */ 1.0f)) {
442             return thresholdLevels;
443         }
444 
445         Slog.w(TAG, "Detected screen thresholdLevels on a deprecated brightness scale");
446         float[] thresholdLevelsScaled = new float[thresholdLevels.length];
447         for (int index = 0; thresholdLevels.length > index; ++index) {
448             thresholdLevelsScaled[index] = thresholdLevels[index] / 255.0f;
449         }
450         return thresholdLevelsScaled;
451     }
452 
isAllInRange(float[] configArray, float minValueInclusive, float maxValueInclusive)453     private static boolean isAllInRange(float[] configArray, float minValueInclusive,
454             float maxValueInclusive) {
455         for (float v : configArray) {
456             if (v < minValueInclusive || v > maxValueInclusive) {
457                 return false;
458             }
459         }
460         return true;
461     }
462 
463 }
464