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