/* * 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.FloatRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; 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 com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import org.xmlpull.v1.XmlPullParserException; 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 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 TAG_BRIGHTNESS_PARAMS = "brightness-params"; 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 static final String ATTR_COLLECT_COLOR = "collect-color"; private static final String ATTR_MODEL_TIMEOUT = "model-timeout"; private static final String ATTR_MODEL_LOWER_BOUND = "model-lower-bound"; private static final String ATTR_MODEL_UPPER_BOUND = "model-upper-bound"; /** * Returned from {@link #getShortTermModelTimeoutMillis()} if no timeout has been set. * In this case the device will use the default timeout available in the * {@link BrightnessConfiguration} returned from * {@link DisplayManager#getDefaultBrightnessConfiguration()}. */ public static final long SHORT_TERM_TIMEOUT_UNSET = -1; private final float[] mLux; private final float[] mNits; private final Map mCorrectionsByPackageName; private final Map mCorrectionsByCategory; private final String mDescription; private final boolean mShouldCollectColorSamples; private final long mShortTermModelTimeout; private final float mShortTermModelLowerLuxMultiplier; private final float mShortTermModelUpperLuxMultiplier; private BrightnessConfiguration(float[] lux, float[] nits, Map correctionsByPackageName, Map correctionsByCategory, String description, boolean shouldCollectColorSamples, long shortTermModelTimeout, float shortTermModelLowerLuxMultiplier, float shortTermModelUpperLuxMultiplier) { mLux = lux; mNits = nits; mCorrectionsByPackageName = correctionsByPackageName; mCorrectionsByCategory = correctionsByCategory; mDescription = description; mShouldCollectColorSamples = shouldCollectColorSamples; mShortTermModelTimeout = shortTermModelTimeout; mShortTermModelLowerLuxMultiplier = shortTermModelLowerLuxMultiplier; mShortTermModelUpperLuxMultiplier = shortTermModelUpperLuxMultiplier; } /** * 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 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; } /** * Returns whether color samples should be collected in * {@link BrightnessChangeEvent#colorValueBuckets}. */ public boolean shouldCollectColorSamples() { return mShouldCollectColorSamples; } /** * Returns the timeout for the short term model in milliseconds. * * If the screen is inactive for this timeout then the short term model * will check the lux range defined by {@link #getShortTermModelLowerLuxMultiplier()} and * {@link #getShortTermModelUpperLuxMultiplier()} to decide whether to keep any adjustment * the user has made to adaptive brightness. */ public long getShortTermModelTimeoutMillis() { return mShortTermModelTimeout; } /** * Returns the multiplier used to calculate the upper bound for which * a users adaptive brightness is considered valid. * * For example if a user changes the brightness when the ambient light level * is 100 lux, the adjustment will be kept if the current ambient light level * is {@code <= 100 + (100 * getShortTermModelUpperLuxMultiplier())}. */ public float getShortTermModelUpperLuxMultiplier() { return mShortTermModelUpperLuxMultiplier; } /** * Returns the multiplier used to calculate the lower bound for which * a users adaptive brightness is considered valid. * * For example if a user changes the brightness when the ambient light level * is 100 lux, the adjustment will be kept if the current ambient light level * is {@code >= 100 - (100 * getShortTermModelLowerLuxMultiplier())}. */ public float getShortTermModelLowerLuxMultiplier() { return mShortTermModelLowerLuxMultiplier; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeFloatArray(mLux); dest.writeFloatArray(mNits); dest.writeInt(mCorrectionsByPackageName.size()); for (Entry 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 entry : mCorrectionsByCategory.entrySet()) { final int category = entry.getKey(); final BrightnessCorrection correction = entry.getValue(); dest.writeInt(category); correction.writeToParcel(dest, flags); } dest.writeString(mDescription); dest.writeBoolean(mShouldCollectColorSamples); dest.writeLong(mShortTermModelTimeout); dest.writeFloat(mShortTermModelLowerLuxMultiplier); dest.writeFloat(mShortTermModelUpperLuxMultiplier); } @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 entry : mCorrectionsByPackageName.entrySet()) { sb.append("'" + entry.getKey() + "': " + entry.getValue() + ", "); } for (Entry entry : mCorrectionsByCategory.entrySet()) { sb.append(entry.getKey() + ": " + entry.getValue() + ", "); } sb.append("}, '"); if (mDescription != null) { sb.append(mDescription); } sb.append(", shouldCollectColorSamples = " + mShouldCollectColorSamples); if (mShortTermModelTimeout >= 0) { sb.append(", shortTermModelTimeout = " + mShortTermModelTimeout); } if (!Float.isNaN(mShortTermModelLowerLuxMultiplier)) { sb.append(", shortTermModelLowerLuxMultiplier = " + mShortTermModelLowerLuxMultiplier); } if (!Float.isNaN(mShortTermModelLowerLuxMultiplier)) { sb.append(", shortTermModelUpperLuxMultiplier = " + mShortTermModelUpperLuxMultiplier); } 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(); } result = result * 31 + Boolean.hashCode(mShouldCollectColorSamples); result = result * 31 + Long.hashCode(mShortTermModelTimeout); result = result * 31 + Float.hashCode(mShortTermModelLowerLuxMultiplier); result = result * 31 + Float.hashCode(mShortTermModelUpperLuxMultiplier); 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) && mShouldCollectColorSamples == other.mShouldCollectColorSamples && mShortTermModelTimeout == other.mShortTermModelTimeout && checkFloatEquals(mShortTermModelLowerLuxMultiplier, other.mShortTermModelLowerLuxMultiplier) && checkFloatEquals(mShortTermModelUpperLuxMultiplier, other.mShortTermModelUpperLuxMultiplier); } private boolean checkFloatEquals(float one, float two) { if (Float.isNaN(one) && Float.isNaN(two)) { return true; } return one == two; } public static final @android.annotation.NonNull Creator CREATOR = new Creator() { 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); final boolean shouldCollectColorSamples = in.readBoolean(); builder.setShouldCollectColorSamples(shouldCollectColorSamples); builder.setShortTermModelTimeoutMillis(in.readLong()); builder.setShortTermModelLowerLuxMultiplier(in.readFloat()); builder.setShortTermModelUpperLuxMultiplier(in.readFloat()); 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 TypedXmlSerializer 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.attributeFloat(null, ATTR_LUX, mLux[i]); serializer.attributeFloat(null, ATTR_NITS, mNits[i]); serializer.endTag(null, TAG_BRIGHTNESS_POINT); } serializer.endTag(null, TAG_BRIGHTNESS_CURVE); serializer.startTag(null, TAG_BRIGHTNESS_CORRECTIONS); for (Map.Entry 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 entry : mCorrectionsByCategory.entrySet()) { final int category = entry.getKey(); final BrightnessCorrection correction = entry.getValue(); serializer.startTag(null, TAG_BRIGHTNESS_CORRECTION); serializer.attributeInt(null, ATTR_CATEGORY, category); correction.saveToXml(serializer); serializer.endTag(null, TAG_BRIGHTNESS_CORRECTION); } serializer.endTag(null, TAG_BRIGHTNESS_CORRECTIONS); serializer.startTag(null, TAG_BRIGHTNESS_PARAMS); if (mShouldCollectColorSamples) { serializer.attributeBoolean(null, ATTR_COLLECT_COLOR, true); } if (mShortTermModelTimeout >= 0) { serializer.attributeLong(null, ATTR_MODEL_TIMEOUT, mShortTermModelTimeout); } if (!Float.isNaN(mShortTermModelLowerLuxMultiplier)) { serializer.attributeFloat(null, ATTR_MODEL_LOWER_BOUND, mShortTermModelLowerLuxMultiplier); } if (!Float.isNaN(mShortTermModelUpperLuxMultiplier)) { serializer.attributeFloat(null, ATTR_MODEL_UPPER_BOUND, mShortTermModelUpperLuxMultiplier); } serializer.endTag(null, TAG_BRIGHTNESS_PARAMS); } /** * 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 TypedXmlPullParser parser) throws IOException, XmlPullParserException { String description = null; List luxList = new ArrayList<>(); List nitsList = new ArrayList<>(); Map correctionsByPackageName = new HashMap<>(); Map correctionsByCategory = new HashMap<>(); boolean shouldCollectColorSamples = false; long shortTermModelTimeout = SHORT_TERM_TIMEOUT_UNSET; float shortTermModelLowerLuxMultiplier = Float.NaN; float shortTermModelUpperLuxMultiplier = Float.NaN; 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); } } else 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 int category = parser.getAttributeInt(null, ATTR_CATEGORY, -1); BrightnessCorrection correction = BrightnessCorrection.loadFromXml(parser); if (packageName != null) { correctionsByPackageName.put(packageName, correction); } else if (category != -1) { correctionsByCategory.put(category, correction); } } } else if (TAG_BRIGHTNESS_PARAMS.equals(parser.getName())) { shouldCollectColorSamples = parser.getAttributeBoolean(null, ATTR_COLLECT_COLOR, false); Long timeout = loadLongFromXml(parser, ATTR_MODEL_TIMEOUT); if (timeout != null) { shortTermModelTimeout = timeout; } shortTermModelLowerLuxMultiplier = loadFloatFromXml(parser, ATTR_MODEL_LOWER_BOUND); shortTermModelUpperLuxMultiplier = loadFloatFromXml(parser, ATTR_MODEL_UPPER_BOUND); } } 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 entry : correctionsByPackageName.entrySet()) { final String packageName = entry.getKey(); final BrightnessCorrection correction = entry.getValue(); builder.addCorrectionByPackageName(packageName, correction); } for (Map.Entry entry : correctionsByCategory.entrySet()) { final int category = entry.getKey(); final BrightnessCorrection correction = entry.getValue(); builder.addCorrectionByCategory(category, correction); } builder.setShouldCollectColorSamples(shouldCollectColorSamples); builder.setShortTermModelTimeoutMillis(shortTermModelTimeout); builder.setShortTermModelLowerLuxMultiplier(shortTermModelLowerLuxMultiplier); builder.setShortTermModelUpperLuxMultiplier(shortTermModelUpperLuxMultiplier); return builder.build(); } private static float loadFloatFromXml(TypedXmlPullParser parser, String attribute) { return parser.getAttributeFloat(null, attribute, Float.NaN); } private static Long loadLongFromXml(TypedXmlPullParser parser, String attribute) { try { return parser.getAttributeLong(null, attribute); } catch (Exception e) { return null; } } /** * 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 mCorrectionsByPackageName; private Map mCorrectionsByCategory; private String mDescription; private boolean mShouldCollectColorSamples; private long mShortTermModelTimeout = SHORT_TERM_TIMEOUT_UNSET; private float mShortTermModelLowerLuxMultiplier = Float.NaN; private float mShortTermModelUpperLuxMultiplier = Float.NaN; /** * 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) { Objects.requireNonNull(lux); Objects.requireNonNull(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; } /** * Control whether screen color samples should be returned in * {@link BrightnessChangeEvent#colorValueBuckets} if supported by the device. * * @param shouldCollectColorSamples true if color samples should be collected. * @return */ @NonNull public Builder setShouldCollectColorSamples(boolean shouldCollectColorSamples) { mShouldCollectColorSamples = shouldCollectColorSamples; return this; } /** * Sets the timeout for the short term model in milliseconds. * * If the screen is inactive for this timeout then the short term model * will check the lux range defined by {@link #setShortTermModelLowerLuxMultiplier(float))} * and {@link #setShortTermModelUpperLuxMultiplier(float)} to decide whether to keep any * adjustment the user has made to adaptive brightness. */ @NonNull public Builder setShortTermModelTimeoutMillis(long shortTermModelTimeoutMillis) { mShortTermModelTimeout = shortTermModelTimeoutMillis; return this; } /** * Sets the multiplier used to calculate the upper bound for which * a users adaptive brightness is considered valid. * * For example if a user changes the brightness when the ambient light level * is 100 lux, the adjustment will be kept if the current ambient light level * is {@code <= 100 + (100 * shortTermModelUpperLuxMultiplier)}. * * @throws IllegalArgumentException if shortTermModelUpperLuxMultiplier is negative. */ @NonNull public Builder setShortTermModelUpperLuxMultiplier( @FloatRange(from = 0.0f) float shortTermModelUpperLuxMultiplier) { if (shortTermModelUpperLuxMultiplier < 0.0f) { throw new IllegalArgumentException("Negative lux multiplier"); } mShortTermModelUpperLuxMultiplier = shortTermModelUpperLuxMultiplier; return this; } /** * Returns the multiplier used to calculate the lower bound for which * a users adaptive brightness is considered valid. * * For example if a user changes the brightness when the ambient light level * is 100 lux, the adjustment will be kept if the current ambient light level * is {@code >= 100 - (100 * shortTermModelLowerLuxMultiplier)}. * * @throws IllegalArgumentException if shortTermModelUpperLuxMultiplier is negative. */ @NonNull public Builder setShortTermModelLowerLuxMultiplier( @FloatRange(from = 0.0f) float shortTermModelLowerLuxMultiplier) { if (shortTermModelLowerLuxMultiplier < 0.0f) { throw new IllegalArgumentException("Negative lux multiplier"); } mShortTermModelLowerLuxMultiplier = shortTermModelLowerLuxMultiplier; 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, mShouldCollectColorSamples, mShortTermModelTimeout, mShortTermModelLowerLuxMultiplier, mShortTermModelUpperLuxMultiplier); } 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]; } } } }