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 @Override toString()149 public String toString() { 150 StringBuilder sb = new StringBuilder("BrightnessConfiguration{["); 151 final int size = mLux.length; 152 for (int i = 0; i < size; i++) { 153 if (i != 0) { 154 sb.append(", "); 155 } 156 sb.append("(").append(mLux[i]).append(", ").append(mNits[i]).append(")"); 157 } 158 sb.append("], {"); 159 for (Entry<String, BrightnessCorrection> entry : mCorrectionsByPackageName.entrySet()) { 160 sb.append("'" + entry.getKey() + "': " + entry.getValue() + ", "); 161 } 162 for (Entry<Integer, BrightnessCorrection> entry : mCorrectionsByCategory.entrySet()) { 163 sb.append(entry.getKey() + ": " + entry.getValue() + ", "); 164 } 165 sb.append("}, '"); 166 if (mDescription != null) { 167 sb.append(mDescription); 168 } 169 sb.append("'}"); 170 return sb.toString(); 171 } 172 173 @Override hashCode()174 public int hashCode() { 175 int result = 1; 176 result = result * 31 + Arrays.hashCode(mLux); 177 result = result * 31 + Arrays.hashCode(mNits); 178 result = result * 31 + mCorrectionsByPackageName.hashCode(); 179 result = result * 31 + mCorrectionsByCategory.hashCode(); 180 if (mDescription != null) { 181 result = result * 31 + mDescription.hashCode(); 182 } 183 return result; 184 } 185 186 @Override equals(Object o)187 public boolean equals(Object o) { 188 if (o == this) { 189 return true; 190 } 191 if (!(o instanceof BrightnessConfiguration)) { 192 return false; 193 } 194 final BrightnessConfiguration other = (BrightnessConfiguration) o; 195 return Arrays.equals(mLux, other.mLux) && Arrays.equals(mNits, other.mNits) 196 && mCorrectionsByPackageName.equals(other.mCorrectionsByPackageName) 197 && mCorrectionsByCategory.equals(other.mCorrectionsByCategory) 198 && Objects.equals(mDescription, other.mDescription); 199 } 200 201 public static final @android.annotation.NonNull Creator<BrightnessConfiguration> CREATOR = 202 new Creator<BrightnessConfiguration>() { 203 public BrightnessConfiguration createFromParcel(Parcel in) { 204 float[] lux = in.createFloatArray(); 205 float[] nits = in.createFloatArray(); 206 Builder builder = new Builder(lux, nits); 207 208 int n = in.readInt(); 209 for (int i = 0; i < n; i++) { 210 final String packageName = in.readString(); 211 final BrightnessCorrection correction = 212 BrightnessCorrection.CREATOR.createFromParcel(in); 213 builder.addCorrectionByPackageName(packageName, correction); 214 } 215 216 n = in.readInt(); 217 for (int i = 0; i < n; i++) { 218 final int category = in.readInt(); 219 final BrightnessCorrection correction = 220 BrightnessCorrection.CREATOR.createFromParcel(in); 221 builder.addCorrectionByCategory(category, correction); 222 } 223 224 final String description = in.readString(); 225 builder.setDescription(description); 226 return builder.build(); 227 } 228 229 public BrightnessConfiguration[] newArray(int size) { 230 return new BrightnessConfiguration[size]; 231 } 232 }; 233 234 /** 235 * Writes the configuration to an XML serializer. 236 * 237 * @param serializer 238 * The XML serializer. 239 * 240 * @hide 241 */ saveToXml(@onNull XmlSerializer serializer)242 public void saveToXml(@NonNull XmlSerializer serializer) throws IOException { 243 serializer.startTag(null, TAG_BRIGHTNESS_CURVE); 244 if (mDescription != null) { 245 serializer.attribute(null, ATTR_DESCRIPTION, mDescription); 246 } 247 for (int i = 0; i < mLux.length; i++) { 248 serializer.startTag(null, TAG_BRIGHTNESS_POINT); 249 serializer.attribute(null, ATTR_LUX, Float.toString(mLux[i])); 250 serializer.attribute(null, ATTR_NITS, Float.toString(mNits[i])); 251 serializer.endTag(null, TAG_BRIGHTNESS_POINT); 252 } 253 serializer.endTag(null, TAG_BRIGHTNESS_CURVE); 254 serializer.startTag(null, TAG_BRIGHTNESS_CORRECTIONS); 255 for (Map.Entry<String, BrightnessCorrection> entry : 256 mCorrectionsByPackageName.entrySet()) { 257 final String packageName = entry.getKey(); 258 final BrightnessCorrection correction = entry.getValue(); 259 serializer.startTag(null, TAG_BRIGHTNESS_CORRECTION); 260 serializer.attribute(null, ATTR_PACKAGE_NAME, packageName); 261 correction.saveToXml(serializer); 262 serializer.endTag(null, TAG_BRIGHTNESS_CORRECTION); 263 } 264 for (Map.Entry<Integer, BrightnessCorrection> entry : mCorrectionsByCategory.entrySet()) { 265 final int category = entry.getKey(); 266 final BrightnessCorrection correction = entry.getValue(); 267 serializer.startTag(null, TAG_BRIGHTNESS_CORRECTION); 268 serializer.attribute(null, ATTR_CATEGORY, Integer.toString(category)); 269 correction.saveToXml(serializer); 270 serializer.endTag(null, TAG_BRIGHTNESS_CORRECTION); 271 } 272 serializer.endTag(null, TAG_BRIGHTNESS_CORRECTIONS); 273 } 274 275 /** 276 * Read a configuration from an XML parser. 277 * 278 * @param parser 279 * The XML parser. 280 * 281 * @throws IOException 282 * The parser failed to read the XML file. 283 * @throws XmlPullParserException 284 * The parser failed to parse the XML file. 285 * 286 * @hide 287 */ loadFromXml(@onNull XmlPullParser parser)288 public static BrightnessConfiguration loadFromXml(@NonNull XmlPullParser parser) 289 throws IOException, XmlPullParserException { 290 String description = null; 291 List<Float> luxList = new ArrayList<>(); 292 List<Float> nitsList = new ArrayList<>(); 293 Map<String, BrightnessCorrection> correctionsByPackageName = new HashMap<>(); 294 Map<Integer, BrightnessCorrection> correctionsByCategory = new HashMap<>(); 295 final int configDepth = parser.getDepth(); 296 while (XmlUtils.nextElementWithin(parser, configDepth)) { 297 if (TAG_BRIGHTNESS_CURVE.equals(parser.getName())) { 298 description = parser.getAttributeValue(null, ATTR_DESCRIPTION); 299 final int curveDepth = parser.getDepth(); 300 while (XmlUtils.nextElementWithin(parser, curveDepth)) { 301 if (!TAG_BRIGHTNESS_POINT.equals(parser.getName())) { 302 continue; 303 } 304 final float lux = loadFloatFromXml(parser, ATTR_LUX); 305 final float nits = loadFloatFromXml(parser, ATTR_NITS); 306 luxList.add(lux); 307 nitsList.add(nits); 308 } 309 } 310 if (TAG_BRIGHTNESS_CORRECTIONS.equals(parser.getName())) { 311 final int correctionsDepth = parser.getDepth(); 312 while (XmlUtils.nextElementWithin(parser, correctionsDepth)) { 313 if (!TAG_BRIGHTNESS_CORRECTION.equals(parser.getName())) { 314 continue; 315 } 316 final String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME); 317 final String categoryText = parser.getAttributeValue(null, ATTR_CATEGORY); 318 BrightnessCorrection correction = BrightnessCorrection.loadFromXml(parser); 319 if (packageName != null) { 320 correctionsByPackageName.put(packageName, correction); 321 } else if (categoryText != null) { 322 try { 323 final int category = Integer.parseInt(categoryText); 324 correctionsByCategory.put(category, correction); 325 } catch (NullPointerException | NumberFormatException e) { 326 continue; 327 } 328 } 329 } 330 } 331 } 332 final int n = luxList.size(); 333 float[] lux = new float[n]; 334 float[] nits = new float[n]; 335 for (int i = 0; i < n; i++) { 336 lux[i] = luxList.get(i); 337 nits[i] = nitsList.get(i); 338 } 339 final BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder(lux, 340 nits); 341 builder.setDescription(description); 342 for (Map.Entry<String, BrightnessCorrection> entry : correctionsByPackageName.entrySet()) { 343 final String packageName = entry.getKey(); 344 final BrightnessCorrection correction = entry.getValue(); 345 builder.addCorrectionByPackageName(packageName, correction); 346 } 347 for (Map.Entry<Integer, BrightnessCorrection> entry : correctionsByCategory.entrySet()) { 348 final int category = entry.getKey(); 349 final BrightnessCorrection correction = entry.getValue(); 350 builder.addCorrectionByCategory(category, correction); 351 } 352 return builder.build(); 353 } 354 loadFloatFromXml(XmlPullParser parser, String attribute)355 private static float loadFloatFromXml(XmlPullParser parser, String attribute) { 356 final String string = parser.getAttributeValue(null, attribute); 357 try { 358 return Float.parseFloat(string); 359 } catch (NullPointerException | NumberFormatException e) { 360 return Float.NaN; 361 } 362 } 363 364 /** 365 * A builder class for {@link BrightnessConfiguration}s. 366 */ 367 public static class Builder { 368 private static final int MAX_CORRECTIONS_BY_PACKAGE_NAME = 20; 369 private static final int MAX_CORRECTIONS_BY_CATEGORY = 20; 370 371 private float[] mCurveLux; 372 private float[] mCurveNits; 373 private Map<String, BrightnessCorrection> mCorrectionsByPackageName; 374 private Map<Integer, BrightnessCorrection> mCorrectionsByCategory; 375 private String mDescription; 376 377 /** 378 * Constructs the builder with the control points for the brightness curve. 379 * 380 * Brightness curves must have strictly increasing ambient brightness values in lux and 381 * monotonically increasing display brightness values in nits. In addition, the initial 382 * control point must be 0 lux. 383 * 384 * @throws IllegalArgumentException if the initial control point is not at 0 lux. 385 * @throws IllegalArgumentException if the lux levels are not strictly increasing. 386 * @throws IllegalArgumentException if the nit levels are not monotonically increasing. 387 */ Builder(float[] lux, float[] nits)388 public Builder(float[] lux, float[] nits) { 389 Preconditions.checkNotNull(lux); 390 Preconditions.checkNotNull(nits); 391 if (lux.length == 0 || nits.length == 0) { 392 throw new IllegalArgumentException("Lux and nits arrays must not be empty"); 393 } 394 if (lux.length != nits.length) { 395 throw new IllegalArgumentException("Lux and nits arrays must be the same length"); 396 } 397 if (lux[0] != 0) { 398 throw new IllegalArgumentException("Initial control point must be for 0 lux"); 399 } 400 Preconditions.checkArrayElementsInRange(lux, 0, Float.MAX_VALUE, "lux"); 401 Preconditions.checkArrayElementsInRange(nits, 0, Float.MAX_VALUE, "nits"); 402 checkMonotonic(lux, true /*strictly increasing*/, "lux"); 403 checkMonotonic(nits, false /*strictly increasing*/, "nits"); 404 mCurveLux = lux; 405 mCurveNits = nits; 406 mCorrectionsByPackageName = new HashMap<>(); 407 mCorrectionsByCategory = new HashMap<>(); 408 } 409 410 /** 411 * Returns the maximum number of corrections by package name allowed. 412 * 413 * @return The maximum number of corrections by package name allowed. 414 * 415 */ getMaxCorrectionsByPackageName()416 public int getMaxCorrectionsByPackageName() { 417 return MAX_CORRECTIONS_BY_PACKAGE_NAME; 418 } 419 420 /** 421 * Returns the maximum number of corrections by category allowed. 422 * 423 * @return The maximum number of corrections by category allowed. 424 * 425 */ getMaxCorrectionsByCategory()426 public int getMaxCorrectionsByCategory() { 427 return MAX_CORRECTIONS_BY_CATEGORY; 428 } 429 430 /** 431 * Add a brightness correction by app package name. 432 * This correction is applied whenever an app with this package name has the top activity 433 * of the focused stack. 434 * 435 * @param packageName 436 * The app's package name. 437 * @param correction 438 * The brightness correction. 439 * 440 * @return The builder. 441 * 442 * @throws IllegalArgumentExceptions 443 * Maximum number of corrections by package name exceeded (see 444 * {@link #getMaxCorrectionsByPackageName}). 445 * 446 */ 447 @NonNull addCorrectionByPackageName(@onNull String packageName, @NonNull BrightnessCorrection correction)448 public Builder addCorrectionByPackageName(@NonNull String packageName, 449 @NonNull BrightnessCorrection correction) { 450 Objects.requireNonNull(packageName, "packageName must not be null"); 451 Objects.requireNonNull(correction, "correction must not be null"); 452 if (mCorrectionsByPackageName.size() >= getMaxCorrectionsByPackageName()) { 453 throw new IllegalArgumentException("Too many corrections by package name"); 454 } 455 mCorrectionsByPackageName.put(packageName, correction); 456 return this; 457 } 458 459 /** 460 * Add a brightness correction by app category. 461 * This correction is applied whenever an app with this category has the top activity of 462 * the focused stack, and only if a correction by package name has not been applied. 463 * 464 * @param category 465 * The {@link android.content.pm.ApplicationInfo#category app category}. 466 * @param correction 467 * The brightness correction. 468 * 469 * @return The builder. 470 * 471 * @throws IllegalArgumentException 472 * Maximum number of corrections by category exceeded (see 473 * {@link #getMaxCorrectionsByCategory}). 474 * 475 */ 476 @NonNull addCorrectionByCategory(@pplicationInfo.Category int category, @NonNull BrightnessCorrection correction)477 public Builder addCorrectionByCategory(@ApplicationInfo.Category int category, 478 @NonNull BrightnessCorrection correction) { 479 Objects.requireNonNull(correction, "correction must not be null"); 480 if (mCorrectionsByCategory.size() >= getMaxCorrectionsByCategory()) { 481 throw new IllegalArgumentException("Too many corrections by category"); 482 } 483 mCorrectionsByCategory.put(category, correction); 484 return this; 485 } 486 487 /** 488 * Set description of the brightness curve. 489 * 490 * @param description brief text describing the curve pushed. It maybe truncated 491 * and will not be displayed in the UI 492 */ 493 @NonNull setDescription(@ullable String description)494 public Builder setDescription(@Nullable String description) { 495 mDescription = description; 496 return this; 497 } 498 499 /** 500 * Builds the {@link BrightnessConfiguration}. 501 */ 502 @NonNull build()503 public BrightnessConfiguration build() { 504 if (mCurveLux == null || mCurveNits == null) { 505 throw new IllegalStateException("A curve must be set!"); 506 } 507 return new BrightnessConfiguration(mCurveLux, mCurveNits, mCorrectionsByPackageName, 508 mCorrectionsByCategory, mDescription); 509 } 510 checkMonotonic(float[] vals, boolean strictlyIncreasing, String name)511 private static void checkMonotonic(float[] vals, boolean strictlyIncreasing, String name) { 512 if (vals.length <= 1) { 513 return; 514 } 515 float prev = vals[0]; 516 for (int i = 1; i < vals.length; i++) { 517 if (prev > vals[i] || prev == vals[i] && strictlyIncreasing) { 518 String condition = strictlyIncreasing ? "strictly increasing" : "monotonic"; 519 throw new IllegalArgumentException(name + " values must be " + condition); 520 } 521 prev = vals[i]; 522 } 523 } 524 } 525 } 526