| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.hardware.display; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.SystemApi; |
| import android.annotation.TestApi; |
| import android.content.pm.ApplicationInfo; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.util.Pair; |
| |
| import com.android.internal.util.Preconditions; |
| import com.android.internal.util.XmlUtils; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| import org.xmlpull.v1.XmlSerializer; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Objects; |
| |
| /** @hide */ |
| @SystemApi |
| @TestApi |
| public final class BrightnessConfiguration implements Parcelable { |
| private static final String TAG_BRIGHTNESS_CURVE = "brightness-curve"; |
| private static final String TAG_BRIGHTNESS_POINT = "brightness-point"; |
| private static final String TAG_BRIGHTNESS_CORRECTIONS = "brightness-corrections"; |
| private static final String TAG_BRIGHTNESS_CORRECTION = "brightness-correction"; |
| private static final String ATTR_LUX = "lux"; |
| private static final String ATTR_NITS = "nits"; |
| private static final String ATTR_DESCRIPTION = "description"; |
| private static final String ATTR_PACKAGE_NAME = "package-name"; |
| private static final String ATTR_CATEGORY = "category"; |
| |
| private final float[] mLux; |
| private final float[] mNits; |
| private final Map<String, BrightnessCorrection> mCorrectionsByPackageName; |
| private final Map<Integer, BrightnessCorrection> mCorrectionsByCategory; |
| private final String mDescription; |
| |
| private BrightnessConfiguration(float[] lux, float[] nits, |
| Map<String, BrightnessCorrection> correctionsByPackageName, |
| Map<Integer, BrightnessCorrection> correctionsByCategory, String description) { |
| mLux = lux; |
| mNits = nits; |
| mCorrectionsByPackageName = correctionsByPackageName; |
| mCorrectionsByCategory = correctionsByCategory; |
| mDescription = description; |
| } |
| |
| /** |
| * Gets the base brightness as curve. |
| * |
| * The curve is returned as a pair of float arrays, the first representing all of the lux |
| * points of the brightness curve and the second representing all of the nits values of the |
| * brightness curve. |
| * |
| * @return the control points for the brightness curve. |
| */ |
| public Pair<float[], float[]> getCurve() { |
| return Pair.create(Arrays.copyOf(mLux, mLux.length), Arrays.copyOf(mNits, mNits.length)); |
| } |
| |
| /** |
| * Returns a brightness correction by app, or null. |
| * |
| * @param packageName |
| * The app's package name. |
| * |
| * @return The matching brightness correction, or null. |
| * |
| */ |
| @Nullable |
| public BrightnessCorrection getCorrectionByPackageName(@NonNull String packageName) { |
| return mCorrectionsByPackageName.get(packageName); |
| } |
| |
| /** |
| * Returns a brightness correction by app category, or null. |
| * |
| * @param category |
| * The app category. |
| * |
| * @return The matching brightness correction, or null. |
| */ |
| @Nullable |
| public BrightnessCorrection getCorrectionByCategory(@ApplicationInfo.Category int category) { |
| return mCorrectionsByCategory.get(category); |
| } |
| |
| /** |
| * Returns description string. |
| * @hide |
| */ |
| public String getDescription() { |
| return mDescription; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| dest.writeFloatArray(mLux); |
| dest.writeFloatArray(mNits); |
| dest.writeInt(mCorrectionsByPackageName.size()); |
| for (Entry<String, BrightnessCorrection> entry : mCorrectionsByPackageName.entrySet()) { |
| final String packageName = entry.getKey(); |
| final BrightnessCorrection correction = entry.getValue(); |
| dest.writeString(packageName); |
| correction.writeToParcel(dest, flags); |
| } |
| dest.writeInt(mCorrectionsByCategory.size()); |
| for (Entry<Integer, BrightnessCorrection> entry : mCorrectionsByCategory.entrySet()) { |
| final int category = entry.getKey(); |
| final BrightnessCorrection correction = entry.getValue(); |
| dest.writeInt(category); |
| correction.writeToParcel(dest, flags); |
| } |
| dest.writeString(mDescription); |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @NonNull |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder("BrightnessConfiguration{["); |
| final int size = mLux.length; |
| for (int i = 0; i < size; i++) { |
| if (i != 0) { |
| sb.append(", "); |
| } |
| sb.append("(").append(mLux[i]).append(", ").append(mNits[i]).append(")"); |
| } |
| sb.append("], {"); |
| for (Entry<String, BrightnessCorrection> entry : mCorrectionsByPackageName.entrySet()) { |
| sb.append("'" + entry.getKey() + "': " + entry.getValue() + ", "); |
| } |
| for (Entry<Integer, BrightnessCorrection> entry : mCorrectionsByCategory.entrySet()) { |
| sb.append(entry.getKey() + ": " + entry.getValue() + ", "); |
| } |
| sb.append("}, '"); |
| if (mDescription != null) { |
| sb.append(mDescription); |
| } |
| sb.append("'}"); |
| return sb.toString(); |
| } |
| |
| @Override |
| public int hashCode() { |
| int result = 1; |
| result = result * 31 + Arrays.hashCode(mLux); |
| result = result * 31 + Arrays.hashCode(mNits); |
| result = result * 31 + mCorrectionsByPackageName.hashCode(); |
| result = result * 31 + mCorrectionsByCategory.hashCode(); |
| if (mDescription != null) { |
| result = result * 31 + mDescription.hashCode(); |
| } |
| return result; |
| } |
| |
| @Override |
| public boolean equals(@Nullable Object o) { |
| if (o == this) { |
| return true; |
| } |
| if (!(o instanceof BrightnessConfiguration)) { |
| return false; |
| } |
| final BrightnessConfiguration other = (BrightnessConfiguration) o; |
| return Arrays.equals(mLux, other.mLux) && Arrays.equals(mNits, other.mNits) |
| && mCorrectionsByPackageName.equals(other.mCorrectionsByPackageName) |
| && mCorrectionsByCategory.equals(other.mCorrectionsByCategory) |
| && Objects.equals(mDescription, other.mDescription); |
| } |
| |
| public static final @android.annotation.NonNull Creator<BrightnessConfiguration> CREATOR = |
| new Creator<BrightnessConfiguration>() { |
| public BrightnessConfiguration createFromParcel(Parcel in) { |
| float[] lux = in.createFloatArray(); |
| float[] nits = in.createFloatArray(); |
| Builder builder = new Builder(lux, nits); |
| |
| int n = in.readInt(); |
| for (int i = 0; i < n; i++) { |
| final String packageName = in.readString(); |
| final BrightnessCorrection correction = |
| BrightnessCorrection.CREATOR.createFromParcel(in); |
| builder.addCorrectionByPackageName(packageName, correction); |
| } |
| |
| n = in.readInt(); |
| for (int i = 0; i < n; i++) { |
| final int category = in.readInt(); |
| final BrightnessCorrection correction = |
| BrightnessCorrection.CREATOR.createFromParcel(in); |
| builder.addCorrectionByCategory(category, correction); |
| } |
| |
| final String description = in.readString(); |
| builder.setDescription(description); |
| return builder.build(); |
| } |
| |
| public BrightnessConfiguration[] newArray(int size) { |
| return new BrightnessConfiguration[size]; |
| } |
| }; |
| |
| /** |
| * Writes the configuration to an XML serializer. |
| * |
| * @param serializer |
| * The XML serializer. |
| * |
| * @hide |
| */ |
| public void saveToXml(@NonNull XmlSerializer serializer) throws IOException { |
| serializer.startTag(null, TAG_BRIGHTNESS_CURVE); |
| if (mDescription != null) { |
| serializer.attribute(null, ATTR_DESCRIPTION, mDescription); |
| } |
| for (int i = 0; i < mLux.length; i++) { |
| serializer.startTag(null, TAG_BRIGHTNESS_POINT); |
| serializer.attribute(null, ATTR_LUX, Float.toString(mLux[i])); |
| serializer.attribute(null, ATTR_NITS, Float.toString(mNits[i])); |
| serializer.endTag(null, TAG_BRIGHTNESS_POINT); |
| } |
| serializer.endTag(null, TAG_BRIGHTNESS_CURVE); |
| serializer.startTag(null, TAG_BRIGHTNESS_CORRECTIONS); |
| for (Map.Entry<String, BrightnessCorrection> entry : |
| mCorrectionsByPackageName.entrySet()) { |
| final String packageName = entry.getKey(); |
| final BrightnessCorrection correction = entry.getValue(); |
| serializer.startTag(null, TAG_BRIGHTNESS_CORRECTION); |
| serializer.attribute(null, ATTR_PACKAGE_NAME, packageName); |
| correction.saveToXml(serializer); |
| serializer.endTag(null, TAG_BRIGHTNESS_CORRECTION); |
| } |
| for (Map.Entry<Integer, BrightnessCorrection> entry : mCorrectionsByCategory.entrySet()) { |
| final int category = entry.getKey(); |
| final BrightnessCorrection correction = entry.getValue(); |
| serializer.startTag(null, TAG_BRIGHTNESS_CORRECTION); |
| serializer.attribute(null, ATTR_CATEGORY, Integer.toString(category)); |
| correction.saveToXml(serializer); |
| serializer.endTag(null, TAG_BRIGHTNESS_CORRECTION); |
| } |
| serializer.endTag(null, TAG_BRIGHTNESS_CORRECTIONS); |
| } |
| |
| /** |
| * Read a configuration from an XML parser. |
| * |
| * @param parser |
| * The XML parser. |
| * |
| * @throws IOException |
| * The parser failed to read the XML file. |
| * @throws XmlPullParserException |
| * The parser failed to parse the XML file. |
| * |
| * @hide |
| */ |
| public static BrightnessConfiguration loadFromXml(@NonNull XmlPullParser parser) |
| throws IOException, XmlPullParserException { |
| String description = null; |
| List<Float> luxList = new ArrayList<>(); |
| List<Float> nitsList = new ArrayList<>(); |
| Map<String, BrightnessCorrection> correctionsByPackageName = new HashMap<>(); |
| Map<Integer, BrightnessCorrection> correctionsByCategory = new HashMap<>(); |
| final int configDepth = parser.getDepth(); |
| while (XmlUtils.nextElementWithin(parser, configDepth)) { |
| if (TAG_BRIGHTNESS_CURVE.equals(parser.getName())) { |
| description = parser.getAttributeValue(null, ATTR_DESCRIPTION); |
| final int curveDepth = parser.getDepth(); |
| while (XmlUtils.nextElementWithin(parser, curveDepth)) { |
| if (!TAG_BRIGHTNESS_POINT.equals(parser.getName())) { |
| continue; |
| } |
| final float lux = loadFloatFromXml(parser, ATTR_LUX); |
| final float nits = loadFloatFromXml(parser, ATTR_NITS); |
| luxList.add(lux); |
| nitsList.add(nits); |
| } |
| } |
| if (TAG_BRIGHTNESS_CORRECTIONS.equals(parser.getName())) { |
| final int correctionsDepth = parser.getDepth(); |
| while (XmlUtils.nextElementWithin(parser, correctionsDepth)) { |
| if (!TAG_BRIGHTNESS_CORRECTION.equals(parser.getName())) { |
| continue; |
| } |
| final String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME); |
| final String categoryText = parser.getAttributeValue(null, ATTR_CATEGORY); |
| BrightnessCorrection correction = BrightnessCorrection.loadFromXml(parser); |
| if (packageName != null) { |
| correctionsByPackageName.put(packageName, correction); |
| } else if (categoryText != null) { |
| try { |
| final int category = Integer.parseInt(categoryText); |
| correctionsByCategory.put(category, correction); |
| } catch (NullPointerException | NumberFormatException e) { |
| continue; |
| } |
| } |
| } |
| } |
| } |
| final int n = luxList.size(); |
| float[] lux = new float[n]; |
| float[] nits = new float[n]; |
| for (int i = 0; i < n; i++) { |
| lux[i] = luxList.get(i); |
| nits[i] = nitsList.get(i); |
| } |
| final BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder(lux, |
| nits); |
| builder.setDescription(description); |
| for (Map.Entry<String, BrightnessCorrection> entry : correctionsByPackageName.entrySet()) { |
| final String packageName = entry.getKey(); |
| final BrightnessCorrection correction = entry.getValue(); |
| builder.addCorrectionByPackageName(packageName, correction); |
| } |
| for (Map.Entry<Integer, BrightnessCorrection> entry : correctionsByCategory.entrySet()) { |
| final int category = entry.getKey(); |
| final BrightnessCorrection correction = entry.getValue(); |
| builder.addCorrectionByCategory(category, correction); |
| } |
| return builder.build(); |
| } |
| |
| private static float loadFloatFromXml(XmlPullParser parser, String attribute) { |
| final String string = parser.getAttributeValue(null, attribute); |
| try { |
| return Float.parseFloat(string); |
| } catch (NullPointerException | NumberFormatException e) { |
| return Float.NaN; |
| } |
| } |
| |
| /** |
| * A builder class for {@link BrightnessConfiguration}s. |
| */ |
| public static class Builder { |
| private static final int MAX_CORRECTIONS_BY_PACKAGE_NAME = 20; |
| private static final int MAX_CORRECTIONS_BY_CATEGORY = 20; |
| |
| private float[] mCurveLux; |
| private float[] mCurveNits; |
| private Map<String, BrightnessCorrection> mCorrectionsByPackageName; |
| private Map<Integer, BrightnessCorrection> mCorrectionsByCategory; |
| private String mDescription; |
| |
| /** |
| * Constructs the builder with the control points for the brightness curve. |
| * |
| * Brightness curves must have strictly increasing ambient brightness values in lux and |
| * monotonically increasing display brightness values in nits. In addition, the initial |
| * control point must be 0 lux. |
| * |
| * @throws IllegalArgumentException if the initial control point is not at 0 lux. |
| * @throws IllegalArgumentException if the lux levels are not strictly increasing. |
| * @throws IllegalArgumentException if the nit levels are not monotonically increasing. |
| */ |
| public Builder(float[] lux, float[] nits) { |
| Preconditions.checkNotNull(lux); |
| Preconditions.checkNotNull(nits); |
| if (lux.length == 0 || nits.length == 0) { |
| throw new IllegalArgumentException("Lux and nits arrays must not be empty"); |
| } |
| if (lux.length != nits.length) { |
| throw new IllegalArgumentException("Lux and nits arrays must be the same length"); |
| } |
| if (lux[0] != 0) { |
| throw new IllegalArgumentException("Initial control point must be for 0 lux"); |
| } |
| Preconditions.checkArrayElementsInRange(lux, 0, Float.MAX_VALUE, "lux"); |
| Preconditions.checkArrayElementsInRange(nits, 0, Float.MAX_VALUE, "nits"); |
| checkMonotonic(lux, true /*strictly increasing*/, "lux"); |
| checkMonotonic(nits, false /*strictly increasing*/, "nits"); |
| mCurveLux = lux; |
| mCurveNits = nits; |
| mCorrectionsByPackageName = new HashMap<>(); |
| mCorrectionsByCategory = new HashMap<>(); |
| } |
| |
| /** |
| * Returns the maximum number of corrections by package name allowed. |
| * |
| * @return The maximum number of corrections by package name allowed. |
| * |
| */ |
| public int getMaxCorrectionsByPackageName() { |
| return MAX_CORRECTIONS_BY_PACKAGE_NAME; |
| } |
| |
| /** |
| * Returns the maximum number of corrections by category allowed. |
| * |
| * @return The maximum number of corrections by category allowed. |
| * |
| */ |
| public int getMaxCorrectionsByCategory() { |
| return MAX_CORRECTIONS_BY_CATEGORY; |
| } |
| |
| /** |
| * Add a brightness correction by app package name. |
| * This correction is applied whenever an app with this package name has the top activity |
| * of the focused stack. |
| * |
| * @param packageName |
| * The app's package name. |
| * @param correction |
| * The brightness correction. |
| * |
| * @return The builder. |
| * |
| * @throws IllegalArgumentExceptions |
| * Maximum number of corrections by package name exceeded (see |
| * {@link #getMaxCorrectionsByPackageName}). |
| * |
| */ |
| @NonNull |
| public Builder addCorrectionByPackageName(@NonNull String packageName, |
| @NonNull BrightnessCorrection correction) { |
| Objects.requireNonNull(packageName, "packageName must not be null"); |
| Objects.requireNonNull(correction, "correction must not be null"); |
| if (mCorrectionsByPackageName.size() >= getMaxCorrectionsByPackageName()) { |
| throw new IllegalArgumentException("Too many corrections by package name"); |
| } |
| mCorrectionsByPackageName.put(packageName, correction); |
| return this; |
| } |
| |
| /** |
| * Add a brightness correction by app category. |
| * This correction is applied whenever an app with this category has the top activity of |
| * the focused stack, and only if a correction by package name has not been applied. |
| * |
| * @param category |
| * The {@link android.content.pm.ApplicationInfo#category app category}. |
| * @param correction |
| * The brightness correction. |
| * |
| * @return The builder. |
| * |
| * @throws IllegalArgumentException |
| * Maximum number of corrections by category exceeded (see |
| * {@link #getMaxCorrectionsByCategory}). |
| * |
| */ |
| @NonNull |
| public Builder addCorrectionByCategory(@ApplicationInfo.Category int category, |
| @NonNull BrightnessCorrection correction) { |
| Objects.requireNonNull(correction, "correction must not be null"); |
| if (mCorrectionsByCategory.size() >= getMaxCorrectionsByCategory()) { |
| throw new IllegalArgumentException("Too many corrections by category"); |
| } |
| mCorrectionsByCategory.put(category, correction); |
| return this; |
| } |
| |
| /** |
| * Set description of the brightness curve. |
| * |
| * @param description brief text describing the curve pushed. It maybe truncated |
| * and will not be displayed in the UI |
| */ |
| @NonNull |
| public Builder setDescription(@Nullable String description) { |
| mDescription = description; |
| return this; |
| } |
| |
| /** |
| * Builds the {@link BrightnessConfiguration}. |
| */ |
| @NonNull |
| public BrightnessConfiguration build() { |
| if (mCurveLux == null || mCurveNits == null) { |
| throw new IllegalStateException("A curve must be set!"); |
| } |
| return new BrightnessConfiguration(mCurveLux, mCurveNits, mCorrectionsByPackageName, |
| mCorrectionsByCategory, mDescription); |
| } |
| |
| private static void checkMonotonic(float[] vals, boolean strictlyIncreasing, String name) { |
| if (vals.length <= 1) { |
| return; |
| } |
| float prev = vals[0]; |
| for (int i = 1; i < vals.length; i++) { |
| if (prev > vals[i] || prev == vals[i] && strictlyIncreasing) { |
| String condition = strictlyIncreasing ? "strictly increasing" : "monotonic"; |
| throw new IllegalArgumentException(name + " values must be " + condition); |
| } |
| prev = vals[i]; |
| } |
| } |
| } |
| } |