1 /*
2  * Copyright (C) 2017 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 android.hardware.display;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.SystemApi;
22 import android.annotation.TestApi;
23 import android.content.pm.ApplicationInfo;
24 import android.os.Parcel;
25 import android.os.Parcelable;
26 import android.util.Pair;
27 
28 import com.android.internal.util.Preconditions;
29 import com.android.internal.util.XmlUtils;
30 
31 import org.xmlpull.v1.XmlPullParser;
32 import org.xmlpull.v1.XmlPullParserException;
33 import org.xmlpull.v1.XmlSerializer;
34 
35 import java.io.IOException;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.HashMap;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Map.Entry;
42 import java.util.Objects;
43 
44 /** @hide */
45 @SystemApi
46 @TestApi
47 public final class BrightnessConfiguration implements Parcelable {
48     private static final String TAG_BRIGHTNESS_CURVE = "brightness-curve";
49     private static final String TAG_BRIGHTNESS_POINT = "brightness-point";
50     private static final String TAG_BRIGHTNESS_CORRECTIONS = "brightness-corrections";
51     private static final String TAG_BRIGHTNESS_CORRECTION = "brightness-correction";
52     private static final String ATTR_LUX = "lux";
53     private static final String ATTR_NITS = "nits";
54     private static final String ATTR_DESCRIPTION = "description";
55     private static final String ATTR_PACKAGE_NAME = "package-name";
56     private static final String ATTR_CATEGORY = "category";
57 
58     private final float[] mLux;
59     private final float[] mNits;
60     private final Map<String, BrightnessCorrection> mCorrectionsByPackageName;
61     private final Map<Integer, BrightnessCorrection> mCorrectionsByCategory;
62     private final String mDescription;
63 
BrightnessConfiguration(float[] lux, float[] nits, Map<String, BrightnessCorrection> correctionsByPackageName, Map<Integer, BrightnessCorrection> correctionsByCategory, String description)64     private BrightnessConfiguration(float[] lux, float[] nits,
65             Map<String, BrightnessCorrection> correctionsByPackageName,
66             Map<Integer, BrightnessCorrection> correctionsByCategory, String description) {
67         mLux = lux;
68         mNits = nits;
69         mCorrectionsByPackageName = correctionsByPackageName;
70         mCorrectionsByCategory = correctionsByCategory;
71         mDescription = description;
72     }
73 
74     /**
75      * Gets the base brightness as curve.
76      *
77      * The curve is returned as a pair of float arrays, the first representing all of the lux
78      * points of the brightness curve and the second representing all of the nits values of the
79      * brightness curve.
80      *
81      * @return the control points for the brightness curve.
82      */
getCurve()83     public Pair<float[], float[]> getCurve() {
84         return Pair.create(Arrays.copyOf(mLux, mLux.length), Arrays.copyOf(mNits, mNits.length));
85     }
86 
87     /**
88      * Returns a brightness correction by app, or null.
89      *
90      * @param packageName
91      *      The app's package name.
92      *
93      * @return The matching brightness correction, or null.
94      *
95      */
96     @Nullable
getCorrectionByPackageName(@onNull String packageName)97     public BrightnessCorrection getCorrectionByPackageName(@NonNull String packageName) {
98         return mCorrectionsByPackageName.get(packageName);
99     }
100 
101     /**
102      * Returns a brightness correction by app category, or null.
103      *
104      * @param category
105      *      The app category.
106      *
107      * @return The matching brightness correction, or null.
108      */
109     @Nullable
getCorrectionByCategory(@pplicationInfo.Category int category)110     public BrightnessCorrection getCorrectionByCategory(@ApplicationInfo.Category int category) {
111         return mCorrectionsByCategory.get(category);
112     }
113 
114     /**
115      * Returns description string.
116      * @hide
117      */
getDescription()118     public String getDescription() {
119         return mDescription;
120     }
121 
122     @Override
writeToParcel(Parcel dest, int flags)123     public void writeToParcel(Parcel dest, int flags) {
124         dest.writeFloatArray(mLux);
125         dest.writeFloatArray(mNits);
126         dest.writeInt(mCorrectionsByPackageName.size());
127         for (Entry<String, BrightnessCorrection> entry : mCorrectionsByPackageName.entrySet()) {
128             final String packageName = entry.getKey();
129             final BrightnessCorrection correction = entry.getValue();
130             dest.writeString(packageName);
131             correction.writeToParcel(dest, flags);
132         }
133         dest.writeInt(mCorrectionsByCategory.size());
134         for (Entry<Integer, BrightnessCorrection> entry : mCorrectionsByCategory.entrySet()) {
135             final int category = entry.getKey();
136             final BrightnessCorrection correction = entry.getValue();
137             dest.writeInt(category);
138             correction.writeToParcel(dest, flags);
139         }
140         dest.writeString(mDescription);
141     }
142 
143     @Override
describeContents()144     public int describeContents() {
145         return 0;
146     }
147 
148     @NonNull
149     @Override
toString()150     public String toString() {
151         StringBuilder sb = new StringBuilder("BrightnessConfiguration{[");
152         final int size = mLux.length;
153         for (int i = 0; i < size; i++) {
154             if (i != 0) {
155                 sb.append(", ");
156             }
157             sb.append("(").append(mLux[i]).append(", ").append(mNits[i]).append(")");
158         }
159         sb.append("], {");
160         for (Entry<String, BrightnessCorrection> entry : mCorrectionsByPackageName.entrySet()) {
161             sb.append("'" + entry.getKey() + "': " + entry.getValue() + ", ");
162         }
163         for (Entry<Integer, BrightnessCorrection> entry : mCorrectionsByCategory.entrySet()) {
164             sb.append(entry.getKey() + ": " + entry.getValue() + ", ");
165         }
166         sb.append("}, '");
167         if (mDescription != null) {
168             sb.append(mDescription);
169         }
170         sb.append("'}");
171         return sb.toString();
172     }
173 
174     @Override
hashCode()175     public int hashCode() {
176         int result = 1;
177         result = result * 31 + Arrays.hashCode(mLux);
178         result = result * 31 + Arrays.hashCode(mNits);
179         result = result * 31 + mCorrectionsByPackageName.hashCode();
180         result = result * 31 + mCorrectionsByCategory.hashCode();
181         if (mDescription != null) {
182             result = result * 31 + mDescription.hashCode();
183         }
184         return result;
185     }
186 
187     @Override
equals(@ullable Object o)188     public boolean equals(@Nullable Object o) {
189         if (o == this) {
190             return true;
191         }
192         if (!(o instanceof BrightnessConfiguration)) {
193             return false;
194         }
195         final BrightnessConfiguration other = (BrightnessConfiguration) o;
196         return Arrays.equals(mLux, other.mLux) && Arrays.equals(mNits, other.mNits)
197                 && mCorrectionsByPackageName.equals(other.mCorrectionsByPackageName)
198                 && mCorrectionsByCategory.equals(other.mCorrectionsByCategory)
199                 && Objects.equals(mDescription, other.mDescription);
200     }
201 
202     public static final @android.annotation.NonNull Creator<BrightnessConfiguration> CREATOR =
203             new Creator<BrightnessConfiguration>() {
204         public BrightnessConfiguration createFromParcel(Parcel in) {
205             float[] lux = in.createFloatArray();
206             float[] nits = in.createFloatArray();
207             Builder builder = new Builder(lux, nits);
208 
209             int n = in.readInt();
210             for (int i = 0; i < n; i++) {
211                 final String packageName = in.readString();
212                 final BrightnessCorrection correction =
213                         BrightnessCorrection.CREATOR.createFromParcel(in);
214                 builder.addCorrectionByPackageName(packageName, correction);
215             }
216 
217             n = in.readInt();
218             for (int i = 0; i < n; i++) {
219                 final int category = in.readInt();
220                 final BrightnessCorrection correction =
221                         BrightnessCorrection.CREATOR.createFromParcel(in);
222                 builder.addCorrectionByCategory(category, correction);
223             }
224 
225             final String description = in.readString();
226             builder.setDescription(description);
227             return builder.build();
228         }
229 
230         public BrightnessConfiguration[] newArray(int size) {
231             return new BrightnessConfiguration[size];
232         }
233     };
234 
235     /**
236      * Writes the configuration to an XML serializer.
237      *
238      * @param serializer
239      *      The XML serializer.
240      *
241      * @hide
242      */
saveToXml(@onNull XmlSerializer serializer)243     public void saveToXml(@NonNull XmlSerializer serializer) throws IOException {
244         serializer.startTag(null, TAG_BRIGHTNESS_CURVE);
245         if (mDescription != null) {
246             serializer.attribute(null, ATTR_DESCRIPTION, mDescription);
247         }
248         for (int i = 0; i < mLux.length; i++) {
249             serializer.startTag(null, TAG_BRIGHTNESS_POINT);
250             serializer.attribute(null, ATTR_LUX, Float.toString(mLux[i]));
251             serializer.attribute(null, ATTR_NITS, Float.toString(mNits[i]));
252             serializer.endTag(null, TAG_BRIGHTNESS_POINT);
253         }
254         serializer.endTag(null, TAG_BRIGHTNESS_CURVE);
255         serializer.startTag(null, TAG_BRIGHTNESS_CORRECTIONS);
256         for (Map.Entry<String, BrightnessCorrection> entry :
257                 mCorrectionsByPackageName.entrySet()) {
258             final String packageName = entry.getKey();
259             final BrightnessCorrection correction = entry.getValue();
260             serializer.startTag(null, TAG_BRIGHTNESS_CORRECTION);
261             serializer.attribute(null, ATTR_PACKAGE_NAME, packageName);
262             correction.saveToXml(serializer);
263             serializer.endTag(null, TAG_BRIGHTNESS_CORRECTION);
264         }
265         for (Map.Entry<Integer, BrightnessCorrection> entry : mCorrectionsByCategory.entrySet()) {
266             final int category = entry.getKey();
267             final BrightnessCorrection correction = entry.getValue();
268             serializer.startTag(null, TAG_BRIGHTNESS_CORRECTION);
269             serializer.attribute(null, ATTR_CATEGORY, Integer.toString(category));
270             correction.saveToXml(serializer);
271             serializer.endTag(null, TAG_BRIGHTNESS_CORRECTION);
272         }
273         serializer.endTag(null, TAG_BRIGHTNESS_CORRECTIONS);
274     }
275 
276     /**
277      * Read a configuration from an XML parser.
278      *
279      * @param parser
280      *      The XML parser.
281      *
282      * @throws IOException
283      *      The parser failed to read the XML file.
284      * @throws XmlPullParserException
285      *      The parser failed to parse the XML file.
286      *
287      * @hide
288      */
loadFromXml(@onNull XmlPullParser parser)289     public static BrightnessConfiguration loadFromXml(@NonNull XmlPullParser parser)
290             throws IOException, XmlPullParserException {
291         String description = null;
292         List<Float> luxList = new ArrayList<>();
293         List<Float> nitsList = new ArrayList<>();
294         Map<String, BrightnessCorrection> correctionsByPackageName = new HashMap<>();
295         Map<Integer, BrightnessCorrection> correctionsByCategory = new HashMap<>();
296         final int configDepth = parser.getDepth();
297         while (XmlUtils.nextElementWithin(parser, configDepth)) {
298             if (TAG_BRIGHTNESS_CURVE.equals(parser.getName())) {
299                 description = parser.getAttributeValue(null, ATTR_DESCRIPTION);
300                 final int curveDepth = parser.getDepth();
301                 while (XmlUtils.nextElementWithin(parser, curveDepth)) {
302                     if (!TAG_BRIGHTNESS_POINT.equals(parser.getName())) {
303                         continue;
304                     }
305                     final float lux = loadFloatFromXml(parser, ATTR_LUX);
306                     final float nits = loadFloatFromXml(parser, ATTR_NITS);
307                     luxList.add(lux);
308                     nitsList.add(nits);
309                 }
310             }
311             if (TAG_BRIGHTNESS_CORRECTIONS.equals(parser.getName())) {
312                 final int correctionsDepth = parser.getDepth();
313                 while (XmlUtils.nextElementWithin(parser, correctionsDepth)) {
314                     if (!TAG_BRIGHTNESS_CORRECTION.equals(parser.getName())) {
315                         continue;
316                     }
317                     final String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME);
318                     final String categoryText = parser.getAttributeValue(null, ATTR_CATEGORY);
319                     BrightnessCorrection correction = BrightnessCorrection.loadFromXml(parser);
320                     if (packageName != null) {
321                         correctionsByPackageName.put(packageName, correction);
322                     } else if (categoryText != null) {
323                         try {
324                             final int category = Integer.parseInt(categoryText);
325                             correctionsByCategory.put(category, correction);
326                         } catch (NullPointerException | NumberFormatException e) {
327                             continue;
328                         }
329                     }
330                 }
331             }
332         }
333         final int n = luxList.size();
334         float[] lux = new float[n];
335         float[] nits = new float[n];
336         for (int i = 0; i < n; i++) {
337             lux[i] = luxList.get(i);
338             nits[i] = nitsList.get(i);
339         }
340         final BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder(lux,
341                 nits);
342         builder.setDescription(description);
343         for (Map.Entry<String, BrightnessCorrection> entry : correctionsByPackageName.entrySet()) {
344             final String packageName = entry.getKey();
345             final BrightnessCorrection correction = entry.getValue();
346             builder.addCorrectionByPackageName(packageName, correction);
347         }
348         for (Map.Entry<Integer, BrightnessCorrection> entry : correctionsByCategory.entrySet()) {
349             final int category = entry.getKey();
350             final BrightnessCorrection correction = entry.getValue();
351             builder.addCorrectionByCategory(category, correction);
352         }
353         return builder.build();
354     }
355 
loadFloatFromXml(XmlPullParser parser, String attribute)356     private static float loadFloatFromXml(XmlPullParser parser, String attribute) {
357         final String string = parser.getAttributeValue(null, attribute);
358         try {
359             return Float.parseFloat(string);
360         } catch (NullPointerException | NumberFormatException e) {
361             return Float.NaN;
362         }
363     }
364 
365     /**
366      * A builder class for {@link BrightnessConfiguration}s.
367      */
368     public static class Builder {
369         private static final int MAX_CORRECTIONS_BY_PACKAGE_NAME = 20;
370         private static final int MAX_CORRECTIONS_BY_CATEGORY = 20;
371 
372         private float[] mCurveLux;
373         private float[] mCurveNits;
374         private Map<String, BrightnessCorrection> mCorrectionsByPackageName;
375         private Map<Integer, BrightnessCorrection> mCorrectionsByCategory;
376         private String mDescription;
377 
378         /**
379          * Constructs the builder with the control points for the brightness curve.
380          *
381          * Brightness curves must have strictly increasing ambient brightness values in lux and
382          * monotonically increasing display brightness values in nits. In addition, the initial
383          * control point must be 0 lux.
384          *
385          * @throws IllegalArgumentException if the initial control point is not at 0 lux.
386          * @throws IllegalArgumentException if the lux levels are not strictly increasing.
387          * @throws IllegalArgumentException if the nit levels are not monotonically increasing.
388          */
Builder(float[] lux, float[] nits)389         public Builder(float[] lux, float[] nits) {
390             Preconditions.checkNotNull(lux);
391             Preconditions.checkNotNull(nits);
392             if (lux.length == 0 || nits.length == 0) {
393                 throw new IllegalArgumentException("Lux and nits arrays must not be empty");
394             }
395             if (lux.length != nits.length) {
396                 throw new IllegalArgumentException("Lux and nits arrays must be the same length");
397             }
398             if (lux[0] != 0) {
399                 throw new IllegalArgumentException("Initial control point must be for 0 lux");
400             }
401             Preconditions.checkArrayElementsInRange(lux, 0, Float.MAX_VALUE, "lux");
402             Preconditions.checkArrayElementsInRange(nits, 0, Float.MAX_VALUE, "nits");
403             checkMonotonic(lux, true /*strictly increasing*/, "lux");
404             checkMonotonic(nits, false /*strictly increasing*/, "nits");
405             mCurveLux = lux;
406             mCurveNits = nits;
407             mCorrectionsByPackageName = new HashMap<>();
408             mCorrectionsByCategory = new HashMap<>();
409         }
410 
411         /**
412          * Returns the maximum number of corrections by package name allowed.
413          *
414          * @return The maximum number of corrections by package name allowed.
415          *
416          */
getMaxCorrectionsByPackageName()417         public int getMaxCorrectionsByPackageName() {
418             return MAX_CORRECTIONS_BY_PACKAGE_NAME;
419         }
420 
421         /**
422          * Returns the maximum number of corrections by category allowed.
423          *
424          * @return The maximum number of corrections by category allowed.
425          *
426          */
getMaxCorrectionsByCategory()427         public int getMaxCorrectionsByCategory() {
428             return MAX_CORRECTIONS_BY_CATEGORY;
429         }
430 
431         /**
432          * Add a brightness correction by app package name.
433          * This correction is applied whenever an app with this package name has the top activity
434          * of the focused stack.
435          *
436          * @param packageName
437          *      The app's package name.
438          * @param correction
439          *      The brightness correction.
440          *
441          * @return The builder.
442          *
443          * @throws IllegalArgumentExceptions
444          *      Maximum number of corrections by package name exceeded (see
445          *      {@link #getMaxCorrectionsByPackageName}).
446          *
447          */
448         @NonNull
addCorrectionByPackageName(@onNull String packageName, @NonNull BrightnessCorrection correction)449         public Builder addCorrectionByPackageName(@NonNull String packageName,
450                 @NonNull BrightnessCorrection correction) {
451             Objects.requireNonNull(packageName, "packageName must not be null");
452             Objects.requireNonNull(correction, "correction must not be null");
453             if (mCorrectionsByPackageName.size() >= getMaxCorrectionsByPackageName()) {
454                 throw new IllegalArgumentException("Too many corrections by package name");
455             }
456             mCorrectionsByPackageName.put(packageName, correction);
457             return this;
458         }
459 
460         /**
461          * Add a brightness correction by app category.
462          * This correction is applied whenever an app with this category has the top activity of
463          * the focused stack, and only if a correction by package name has not been applied.
464          *
465          * @param category
466          *      The {@link android.content.pm.ApplicationInfo#category app category}.
467          * @param correction
468          *      The brightness correction.
469          *
470          * @return The builder.
471          *
472          * @throws IllegalArgumentException
473          *      Maximum number of corrections by category exceeded (see
474          *      {@link #getMaxCorrectionsByCategory}).
475          *
476          */
477         @NonNull
addCorrectionByCategory(@pplicationInfo.Category int category, @NonNull BrightnessCorrection correction)478         public Builder addCorrectionByCategory(@ApplicationInfo.Category int category,
479                 @NonNull BrightnessCorrection correction) {
480             Objects.requireNonNull(correction, "correction must not be null");
481             if (mCorrectionsByCategory.size() >= getMaxCorrectionsByCategory()) {
482                 throw new IllegalArgumentException("Too many corrections by category");
483             }
484             mCorrectionsByCategory.put(category, correction);
485             return this;
486         }
487 
488         /**
489          * Set description of the brightness curve.
490          *
491          * @param description brief text describing the curve pushed. It maybe truncated
492          *                    and will not be displayed in the UI
493          */
494         @NonNull
setDescription(@ullable String description)495         public Builder setDescription(@Nullable String description) {
496             mDescription = description;
497             return this;
498         }
499 
500         /**
501          * Builds the {@link BrightnessConfiguration}.
502          */
503         @NonNull
build()504         public BrightnessConfiguration build() {
505             if (mCurveLux == null || mCurveNits == null) {
506                 throw new IllegalStateException("A curve must be set!");
507             }
508             return new BrightnessConfiguration(mCurveLux, mCurveNits, mCorrectionsByPackageName,
509                     mCorrectionsByCategory, mDescription);
510         }
511 
checkMonotonic(float[] vals, boolean strictlyIncreasing, String name)512         private static void checkMonotonic(float[] vals, boolean strictlyIncreasing, String name) {
513             if (vals.length <= 1) {
514                 return;
515             }
516             float prev = vals[0];
517             for (int i = 1; i < vals.length; i++) {
518                 if (prev > vals[i] || prev == vals[i] && strictlyIncreasing) {
519                     String condition = strictlyIncreasing ? "strictly increasing" : "monotonic";
520                     throw new IllegalArgumentException(name + " values must be " + condition);
521                 }
522                 prev = vals[i];
523             }
524         }
525     }
526 }
527