1 /*
2  * Copyright (C) 2018 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.car.util;
18 
19 import android.car.builtin.power.PowerManagerHelper;
20 
21 import com.android.internal.annotations.VisibleForTesting;
22 
23 /**
24  * This is the minimized version of {@code com.android.settingslib.display.BrightnessUtils} not to
25  * depend on the library which uses the hidden api.
26  */
27 public class BrightnessUtils {
28 
29     public static final int GAMMA_SPACE_MIN = 0;
30     public static final int GAMMA_SPACE_MAX = 65535;
31     @VisibleForTesting
32     public static final float INVALID_BRIGHTNESS_IN_FLOAT = -1.f;
33 
34     // Hybrid Log Gamma constant values
35     private static final float R = 0.5f;
36     private static final float A = 0.17883277f;
37     private static final float B = 0.28466892f;
38     private static final float C = 0.55991073f;
39 
40     // The tolerance within which we consider brightness values approximately equal to each other.
41     // This value is approximately 1/3 of the smallest possible brightness value.
42     private static final float EPSILON = 0.001f;
43 
44     /**
45      * A function for converting from the gamma space that the slider works in to the
46      * linear space that the setting works in.
47      *
48      * The gamma space effectively provides us a way to make linear changes to the slider that
49      * result in linear changes in perception. If we made changes to the slider in the linear space
50      * then we'd see an approximately logarithmic change in perception (c.f. Fechner's Law).
51      *
52      * Internally, this implements the Hybrid Log Gamma electro-optical transfer function, which is
53      * a slight improvement to the typical gamma transfer function for displays whose max
54      * brightness exceeds the 120 nit reference point, but doesn't set a specific reference
55      * brightness like the PQ function does.
56      *
57      * Note that this transfer function is only valid if the display's backlight value is a linear
58      * control. If it's calibrated to be something non-linear, then a different transfer function
59      * should be used.
60      *
61      * @param val The slider value.
62      * @param min The minimum acceptable value for the setting.
63      * @param max The maximum acceptable value for the setting.
64      * @return The corresponding setting value.
65      */
convertGammaToLinear(int val, int min, int max)66     public static final int convertGammaToLinear(int val, int min, int max) {
67         final float normalizedVal = MathUtils.norm(GAMMA_SPACE_MIN, GAMMA_SPACE_MAX, val);
68         final float ret;
69         if (normalizedVal <= R) {
70             ret = MathUtils.sq(normalizedVal / R);
71         } else {
72             ret = MathUtils.exp((normalizedVal - C) / A) + B;
73         }
74 
75         // HLG is normalized to the range [0, 12], so we need to re-normalize to the range [0, 1]
76         // in order to derive the correct setting value.
77         return Math.round(MathUtils.lerp(min, max, ret / 12));
78     }
79 
80     /**
81      * A function for converting from the linear space that the setting works in to the
82      * gamma space that the slider works in.
83      *
84      * The gamma space effectively provides us a way to make linear changes to the slider that
85      * result in linear changes in perception. If we made changes to the slider in the linear space
86      * then we'd see an approximately logarithmic change in perception (c.f. Fechner's Law).
87      *
88      * Internally, this implements the Hybrid Log Gamma opto-electronic transfer function, which is
89      * a slight improvement to the typical gamma transfer function for displays whose max
90      * brightness exceeds the 120 nit reference point, but doesn't set a specific reference
91      * brightness like the PQ function does.
92      *
93      * Note that this transfer function is only valid if the display's backlight value is a linear
94      * control. If it's calibrated to be something non-linear, then a different transfer function
95      * should be used.
96      *
97      * @param val The brightness setting value.
98      * @param min The minimum acceptable value for the setting.
99      * @param max The maximum acceptable value for the setting.
100      * @return The corresponding slider value
101      */
convertLinearToGamma(int val, int min, int max)102     public static final int convertLinearToGamma(int val, int min, int max) {
103         return convertLinearToGammaFloat((float) val, (float) min, (float) max);
104     }
105 
106     /**
107      * Version of {@link #convertLinearToGamma} that takes float values.
108      * TODO: brightnessfloat merge with above method(?)
109      * @param val The brightness setting value.
110      * @param min The minimum acceptable value for the setting.
111      * @param max The maximum acceptable value for the setting.
112      * @return The corresponding slider value
113      */
convertLinearToGammaFloat(float val, float min, float max)114     public static final int convertLinearToGammaFloat(float val, float min, float max) {
115         // For some reason, HLG normalizes to the range [0, 12] rather than [0, 1]
116         final float normalizedVal = MathUtils.norm(min, max, val) * 12;
117         final float ret;
118         if (normalizedVal <= 1f) {
119             ret = MathUtils.sqrt(normalizedVal) * R;
120         } else {
121             ret = A * MathUtils.log(normalizedVal - B) + C;
122         }
123 
124         return Math.round(MathUtils.lerp(GAMMA_SPACE_MIN, GAMMA_SPACE_MAX, ret));
125     }
126 
127     /**
128      * Converts between the int brightness system and the float brightness system.
129      *
130      * <p>This is the copy of
131      * {@code com.android.internal.display.BrightnessSynchronizer#brightnessIntToFloat}.
132      */
brightnessIntToFloat(int brightnessInt)133     public static float brightnessIntToFloat(int brightnessInt) {
134         if (brightnessInt == PowerManagerHelper.BRIGHTNESS_OFF) {
135             return PowerManagerHelper.BRIGHTNESS_OFF_FLOAT;
136         } else if (brightnessInt == PowerManagerHelper.BRIGHTNESS_INVALID) {
137             return PowerManagerHelper.BRIGHTNESS_INVALID_FLOAT;
138         } else {
139             final float minFloat = PowerManagerHelper.BRIGHTNESS_MIN;
140             final float maxFloat = PowerManagerHelper.BRIGHTNESS_MAX;
141             final float minInt = PowerManagerHelper.BRIGHTNESS_OFF + 1;
142             final float maxInt = PowerManagerHelper.BRIGHTNESS_ON;
143             return MathUtils.constrainedMap(minFloat, maxFloat, minInt, maxInt, brightnessInt);
144         }
145     }
146 
147     /**
148      * Converts between the float brightness system and the int brightness system.
149      *
150      * <p>This is the copy of
151      * {@code com.android.internal.display.BrightnessSynchronizer#brightnessFloatToInt}.
152      */
brightnessFloatToInt(float brightnessFloat)153     public static int brightnessFloatToInt(float brightnessFloat) {
154         return Math.round(brightnessFloatToIntRange(brightnessFloat));
155     }
156 
157     /**
158      * Translates specified value from the float brightness system to the int brightness system,
159      * given the min/max of each range. Accounts for special values such as OFF and invalid values.
160      * Value returned as a float primitive (to preserve precision), but is a value within the
161      * int-system range.
162      *
163      * <p>This is the copy of
164      * {@code com.android.internal.display.BrightnessSynchronizer#brightnessFloatToIntRange}.
165      */
brightnessFloatToIntRange(float brightnessFloat)166     private static float brightnessFloatToIntRange(float brightnessFloat) {
167         if (floatEquals(brightnessFloat, PowerManagerHelper.BRIGHTNESS_OFF_FLOAT)) {
168             return PowerManagerHelper.BRIGHTNESS_OFF;
169         } else if (Float.isNaN(brightnessFloat)) {
170             return PowerManagerHelper.BRIGHTNESS_INVALID;
171         } else {
172             final float minFloat = PowerManagerHelper.BRIGHTNESS_MIN;
173             final float maxFloat = PowerManagerHelper.BRIGHTNESS_MAX;
174             final float minInt = PowerManagerHelper.BRIGHTNESS_OFF + 1;
175             final float maxInt = PowerManagerHelper.BRIGHTNESS_ON;
176             return MathUtils.constrainedMap(minInt, maxInt, minFloat, maxFloat, brightnessFloat);
177         }
178     }
179 
180     /**
181      * Tests whether two brightness float values are within a small enough tolerance
182      * of each other.
183      *
184      * <p>This is the copy of
185      * {@code com.android.internal.display.BrightnessSynchronizer#floatEquals}.
186      *
187      * @param a first float to compare
188      * @param b second float to compare
189      * @return whether the two values are within a small enough tolerance value
190      */
floatEquals(float a, float b)191     private static boolean floatEquals(float a, float b) {
192         return a == b
193                 || (Float.isNaN(a) && Float.isNaN(b))
194                 || (Math.abs(a - b) < EPSILON);
195     }
196 
197     /**
198      * This is the minimized version of {@code android.util.MathUtils} which is the hidden api.
199      */
200     private static final class MathUtils {
MathUtils()201         private MathUtils() {
202         }
203 
constrain(float amount, float low, float high)204         public static float constrain(float amount, float low, float high) {
205             return amount < low ? low : (amount > high ? high : amount);
206         }
207 
log(float a)208         public static float log(float a) {
209             return (float) Math.log(a);
210         }
211 
exp(float a)212         public static float exp(float a) {
213             return (float) Math.exp(a);
214         }
215 
sqrt(float a)216         public static float sqrt(float a) {
217             return (float) Math.sqrt(a);
218         }
219 
sq(float v)220         public static float sq(float v) {
221             return v * v;
222         }
223 
lerp(float start, float stop, float amount)224         public static float lerp(float start, float stop, float amount) {
225             return start + (stop - start) * amount;
226         }
227 
lerpInv(float a, float b, float value)228         public static float lerpInv(float a, float b, float value) {
229             return a != b ? ((value - a) / (b - a)) : 0.0f;
230         }
231 
saturate(float value)232         public static float saturate(float value) {
233             return constrain(value, 0.0f, 1.0f);
234         }
235 
lerpInvSat(float a, float b, float value)236         public static float lerpInvSat(float a, float b, float value) {
237             return saturate(lerpInv(a, b, value));
238         }
239 
norm(float start, float stop, float value)240         public static float norm(float start, float stop, float value) {
241             return (value - start) / (stop - start);
242         }
243 
constrainedMap( float rangeMin, float rangeMax, float valueMin, float valueMax, float value)244         public static float constrainedMap(
245                 float rangeMin, float rangeMax, float valueMin, float valueMax, float value) {
246             return lerp(rangeMin, rangeMax, lerpInvSat(valueMin, valueMax, value));
247         }
248     }
249 }
250