Add an API for setting a new brightness curve.

In addition, this also provides multiple strategies for mapping from
ambient room brightness in lux to display brightness. The default one is
the classic strategy, where we map directly from lux to backlight
brightness in an arbitrary unit. The newer and preferred strategy is to
use the physical brightness of the display, but requires that the
brightness properties of the display are appropriately configured.

Bug: 69406783
Test: atest com.android.server.display.BrightnessMappingStrategyTest &&
      atest android.hardware.display.BrightnessConfigurationTest &&
      atest android.hardware.display.PersistentDataStoreTeset

Change-Id: I60227bdb6c299d0fa92686cbf3e5994b336a3a79
diff --git a/core/java/android/hardware/display/BrightnessConfiguration.aidl b/core/java/android/hardware/display/BrightnessConfiguration.aidl
new file mode 100644
index 0000000..5b6b464
--- /dev/null
+++ b/core/java/android/hardware/display/BrightnessConfiguration.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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;
+
+parcelable BrightnessConfiguration;
diff --git a/core/java/android/hardware/display/BrightnessConfiguration.java b/core/java/android/hardware/display/BrightnessConfiguration.java
new file mode 100644
index 0000000..6c3be81
--- /dev/null
+++ b/core/java/android/hardware/display/BrightnessConfiguration.java
@@ -0,0 +1,175 @@
+/*
+ * 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.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Arrays;
+
+/** @hide */
+public final class BrightnessConfiguration implements Parcelable {
+    private final float[] mLux;
+    private final float[] mNits;
+
+    private BrightnessConfiguration(float[] lux, float[] nits) {
+        mLux = lux;
+        mNits = nits;
+    }
+
+    /**
+     * 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));
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeFloatArray(mLux);
+        dest.writeFloatArray(mNits);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @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("]}");
+        return sb.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 1;
+        result = result * 31 + Arrays.hashCode(mLux);
+        result = result * 31 + Arrays.hashCode(mNits);
+        return result;
+    }
+
+    @Override
+    public boolean equals(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);
+    }
+
+    public static final Creator<BrightnessConfiguration> CREATOR =
+            new Creator<BrightnessConfiguration>() {
+        public BrightnessConfiguration createFromParcel(Parcel in) {
+            Builder builder = new Builder();
+            float[] lux = in.createFloatArray();
+            float[] nits = in.createFloatArray();
+            builder.setCurve(lux, nits);
+            return builder.build();
+        }
+
+        public BrightnessConfiguration[] newArray(int size) {
+            return new BrightnessConfiguration[size];
+        }
+    };
+
+    /**
+     * A builder class for {@link BrightnessConfiguration}s.
+     */
+    public static class Builder {
+        private float[] mCurveLux;
+        private float[] mCurveNits;
+
+        /**
+         * Sets 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 setCurve(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;
+            return this;
+        }
+
+        /**
+         * Builds the {@link BrightnessConfiguration}.
+         *
+         * A brightness curve <b>must</b> be set before calling this.
+         */
+        public BrightnessConfiguration build() {
+            if (mCurveLux == null || mCurveNits == null) {
+                throw new IllegalStateException("A curve must be set!");
+            }
+            return new BrightnessConfiguration(mCurveLux, mCurveNits);
+        }
+
+        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];
+            }
+        }
+    }
+}
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index 97ca231..7de667d 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -27,6 +27,7 @@
 import android.graphics.Point;
 import android.media.projection.MediaProjection;
 import android.os.Handler;
+import android.os.UserHandle;
 import android.util.SparseArray;
 import android.view.Display;
 import android.view.Surface;
@@ -634,6 +635,27 @@
     }
 
     /**
+     * Sets the global display brightness configuration.
+     *
+     * @hide
+     */
+    public void setBrightnessConfiguration(BrightnessConfiguration c) {
+        setBrightnessConfigurationForUser(c, UserHandle.myUserId());
+    }
+
+    /**
+     * Sets the global display brightness configuration for a specific user.
+     *
+     * Note this requires the INTERACT_ACROSS_USERS permission if setting the configuration for a
+     * user other than the one you're currently running as.
+     *
+     * @hide
+     */
+    public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userId) {
+        mGlobal.setBrightnessConfigurationForUser(c, userId);
+    }
+
+    /**
      * Listens for changes in available display devices.
      */
     public interface DisplayListener {
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index c3f82f5..bf4cc1d 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -487,6 +487,19 @@
         }
     }
 
+    /**
+     * Sets the global brightness configuration for a given user.
+     *
+     * @hide
+     */
+    public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userId) {
+        try {
+            mDm.setBrightnessConfigurationForUser(c, userId);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
     private final class DisplayManagerCallback extends IDisplayManagerCallback.Stub {
         @Override
         public void onDisplayEvent(int displayId, int event) {
diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl
index f2ed9e7..8afae6e 100644
--- a/core/java/android/hardware/display/IDisplayManager.aidl
+++ b/core/java/android/hardware/display/IDisplayManager.aidl
@@ -18,6 +18,7 @@
 
 import android.content.pm.ParceledListSlice;
 import android.graphics.Point;
+import android.hardware.display.BrightnessConfiguration;
 import android.hardware.display.IDisplayManagerCallback;
 import android.hardware.display.IVirtualDisplayCallback;
 import android.hardware.display.WifiDisplay;
@@ -89,4 +90,9 @@
     // STOPSHIP remove when adaptive brightness code is updated to accept curves.
     // Requires BRIGHTNESS_SLIDER_USAGE permission.
     void setBrightness(int brightness);
+
+    // Sets the global brightness configuration for a given user. Requires
+    // CONFIGURE_DISPLAY_BRIGHTNESS, and INTERACT_ACROSS_USER if the user being configured is not
+    // the same as the calling user.
+    void setBrightnessConfigurationForUser(in BrightnessConfiguration c, int userId);
 }
diff --git a/core/java/com/android/internal/util/Preconditions.java b/core/java/com/android/internal/util/Preconditions.java
index e5d5716..91c76af 100644
--- a/core/java/com/android/internal/util/Preconditions.java
+++ b/core/java/com/android/internal/util/Preconditions.java
@@ -494,4 +494,38 @@
 
         return value;
     }
+
+    /**
+     * Ensures that all elements in the argument integer array are within the inclusive range
+     *
+     * @param value an integer array of values
+     * @param lower the lower endpoint of the inclusive range
+     * @param upper the upper endpoint of the inclusive range
+     * @param valueName the name of the argument to use if the check fails
+     *
+     * @return the validated integer array
+     *
+     * @throws IllegalArgumentException if any of the elements in {@code value} were out of range
+     * @throws NullPointerException if the {@code value} was {@code null}
+     */
+    public static int[] checkArrayElementsInRange(int[] value, int lower, int upper,
+            String valueName) {
+        checkNotNull(value, valueName + " must not be null");
+
+        for (int i = 0; i < value.length; ++i) {
+            int v = value[i];
+
+            if (v < lower) {
+                throw new IllegalArgumentException(
+                        String.format("%s[%d] is out of range of [%d, %d] (too low)",
+                                valueName, i, lower, upper));
+            } else if (v > upper) {
+                throw new IllegalArgumentException(
+                        String.format("%s[%d] is out of range of [%d, %d] (too high)",
+                                valueName, i, lower, upper));
+            }
+        }
+
+        return value;
+    }
 }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 0920426..5645b64 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2923,6 +2923,11 @@
     <permission android:name="android.permission.BRIGHTNESS_SLIDER_USAGE"
         android:protectionLevel="signature|privileged" />
 
+    <!-- Allows an application to modify the display brightness configuration
+         @hide -->
+    <permission android:name="android.permission.CONFIGURE_DISPLAY_BRIGHTNESS"
+        android:protectionLevel="signature|privileged|development" />
+
     <!-- @SystemApi Allows an application to control VPN.
          <p>Not for use by third-party applications.</p>
          @hide -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 0b38d1b..e09ed18 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1301,6 +1301,22 @@
     <integer-array name="config_autoBrightnessLcdBacklightValues">
     </integer-array>
 
+    <!-- Array of desired screen brightness in nits corresponding to the lux values
+         in the config_autoBrightnessLevels array. As with config_screenBrightnessMinimumNits and
+         config_screenBrightnessMaximumNits, the display brightness is defined as the measured
+         brightness of an all-white image.
+
+         If this is defined then:
+            - config_autoBrightnessLcdBacklightValues should not be defined
+            - config_screenBrightnessMinimumNits must be defined
+            - config_screenBrightnessMaximumNits must be defined
+
+         This array should have size one greater than the size of the config_autoBrightnessLevels
+         array. The brightness values must be non-negative and non-decreasing. This must be
+         overridden in platform specific overlays -->
+    <array name="config_autoBrightnessDisplayValuesNits">
+    </array>
+
     <!-- Array of output values for button backlight corresponding to the LUX values
          in the config_autoBrightnessLevels array.  This array should have size one greater
          than the size of the config_autoBrightnessLevels array.
@@ -1337,6 +1353,29 @@
         <item>200</item>
     </integer-array>
 
+    <!-- The minimum brightness of the display in nits. On OLED displays this should be measured
+         with an all white image while the display is fully on and the backlight is set to
+         config_screenBrightnessSettingMinimum or config_screenBrightnessSettingDark, whichever
+         is darker.
+
+         If this and config_screenBrightnessMinimumNits are set, then the display's brightness
+         range is assumed to be linear between
+         (config_screenBrightnessSettingMinimum, config_screenBrightnessMinimumNits) and
+         (config_screenBrightnessSettingMaximum, config_screenBrightnessMaximumNits). -->
+    <item name="config_screenBrightnessMinimumNits" format="float" type="dimen">-1.0</item>
+
+    <!-- The maximum brightness of the display in nits. On OLED displays this should be measured
+         with an all white image while the display is fully on and the "backlight" is set to
+         config_screenBrightnessSettingMaximum. Note that this value should *not* reflect the
+         maximum brightness value for any high brightness modes but only the maximum brightness
+         value obtainable in a sustainable manner.
+
+         If this and config_screenBrightnessMinimumNits are set to something non-negative, then the
+         display's brightness range is assumed to be linear between
+         (config_screenBrightnessSettingMinimum, config_screenBrightnessMaximumNits) and
+         (config_screenBrightnessSettingMaximum, config_screenBrightnessMaximumNits). -->
+    <item name="config_screenBrightnessMaximumNits" format="float" type="dimen">-1.0</item>
+
     <!-- Array of ambient lux threshold values. This is used for determining hysteresis constraint
          values by calculating the index to use for lookup and then setting the constraint value
          to the corresponding value of the array. The new brightening hysteresis constraint value
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 6da24bd..52e4bca02 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3171,4 +3171,8 @@
 
   <java-symbol type="string" name="global_action_logout" />
   <java-symbol type="drawable" name="ic_logout" />
+
+  <java-symbol type="dimen" name="config_screenBrightnessMinimumNits" />
+  <java-symbol type="dimen" name="config_screenBrightnessMaximumNits" />
+  <java-symbol type="array" name="config_autoBrightnessDisplayValuesNits" />
 </resources>
diff --git a/core/tests/coretests/src/android/hardware/display/BrightnessConfigurationTest.java b/core/tests/coretests/src/android/hardware/display/BrightnessConfigurationTest.java
new file mode 100644
index 0000000..bad0d25
--- /dev/null
+++ b/core/tests/coretests/src/android/hardware/display/BrightnessConfigurationTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.os.Parcel;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Pair;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BrightnessConfigurationTest {
+    private static final float[] LUX_LEVELS = {
+        0f,
+        10f,
+        100f,
+    };
+
+    private static final float[] NITS_LEVELS = {
+        0.5f,
+        90f,
+        100f,
+    };
+
+    @Test
+    public void testSetCurveIsUnmodified() {
+        BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
+        builder.setCurve(LUX_LEVELS, NITS_LEVELS);
+        BrightnessConfiguration config = builder.build();
+        Pair<float[], float[]> curve = config.getCurve();
+        assertArrayEquals(LUX_LEVELS, curve.first, "lux");
+        assertArrayEquals(NITS_LEVELS, curve.second, "nits");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testCurveMustHaveZeroLuxPoint() {
+        BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
+        float[] lux = Arrays.copyOf(LUX_LEVELS, LUX_LEVELS.length);
+        lux[0] = 1f;
+        builder.setCurve(lux, NITS_LEVELS);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testCurveMustBeSet() {
+        BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
+        builder.build();
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testCurveMustNotHaveNullArrays() {
+        BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
+        builder.setCurve(null, null);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testCurveMustNotHaveEmptyArrays() {
+        BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
+        builder.setCurve(new float[0], new float[0]);
+    }
+
+    @Test
+    public void testCurveMustNotHaveArraysOfDifferentLengths() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
+            float[] lux = Arrays.copyOf(LUX_LEVELS, LUX_LEVELS.length + 1);
+            lux[lux.length - 1] = lux[lux.length - 2] + 1;
+            boolean exceptionThrown = false;
+            builder.setCurve(lux, NITS_LEVELS);
+        });
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
+            float[] nits = Arrays.copyOf(NITS_LEVELS, NITS_LEVELS.length + 1);
+            nits[nits.length - 1] = nits[nits.length - 2] + 1;
+            builder.setCurve(LUX_LEVELS, nits);
+        });
+    }
+
+    @Test
+    public void testCurvesMustNotContainNaN() {
+        assertThrows(IllegalArgumentException.class, () -> {
+            float[] lux = Arrays.copyOf(LUX_LEVELS, LUX_LEVELS.length);
+            lux[lux.length - 1] = Float.NaN;
+            BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
+            builder.setCurve(lux, NITS_LEVELS);
+        });
+
+        assertThrows(IllegalArgumentException.class, () -> {
+            float[] nits = Arrays.copyOf(NITS_LEVELS, NITS_LEVELS.length);
+            nits[nits.length - 1] = Float.NaN;
+            BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
+            builder.setCurve(LUX_LEVELS, nits);
+        });
+    }
+
+
+    @Test
+    public void testParceledConfigIsEquivalent() {
+        BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
+        builder.setCurve(LUX_LEVELS, NITS_LEVELS);
+        BrightnessConfiguration config = builder.build();
+        Parcel p = Parcel.obtain();
+        p.writeParcelable(config, 0 /*flags*/);
+        p.setDataPosition(0);
+        BrightnessConfiguration newConfig =
+                p.readParcelable(BrightnessConfiguration.class.getClassLoader());
+        assertEquals(config, newConfig);
+    }
+
+    @Test
+    public void testEquals() {
+        BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
+        builder.setCurve(LUX_LEVELS, NITS_LEVELS);
+        BrightnessConfiguration baseConfig = builder.build();
+
+        builder = new BrightnessConfiguration.Builder();
+        builder.setCurve(LUX_LEVELS, NITS_LEVELS);
+        BrightnessConfiguration identicalConfig = builder.build();
+        assertEquals(baseConfig, identicalConfig);
+        assertEquals("hashCodes must be equal for identical configs",
+                baseConfig.hashCode(), identicalConfig.hashCode());
+
+        float[] lux = Arrays.copyOf(LUX_LEVELS, LUX_LEVELS.length);
+        lux[lux.length - 1] = lux[lux.length - 1] * 2;
+        builder = new BrightnessConfiguration.Builder();
+        builder.setCurve(lux, NITS_LEVELS);
+        BrightnessConfiguration luxDifferConfig = builder.build();
+        assertNotEquals(baseConfig, luxDifferConfig);
+
+        float[] nits = Arrays.copyOf(NITS_LEVELS, NITS_LEVELS.length);
+        nits[nits.length - 1] = nits[nits.length - 1] * 2;
+        builder = new BrightnessConfiguration.Builder();
+        builder.setCurve(LUX_LEVELS, nits);
+        BrightnessConfiguration nitsDifferConfig = builder.build();
+        assertNotEquals(baseConfig, nitsDifferConfig);
+    }
+
+    private static void assertArrayEquals(float[] expected, float[] actual, String name) {
+        assertEquals("Expected " + name + " arrays to be the same length!",
+                expected.length, actual.length);
+        for (int i = 0; i < expected.length; i++) {
+            assertEquals("Expected " + name + " arrays to be equivalent when value " + i
+                    + "differs", expected[i], actual[i], 0.01 /*tolerance*/);
+        }
+    }
+
+    private interface ExceptionRunnable {
+        void run() throws Exception;
+    }
+
+    private static void assertThrows(Class<? extends Throwable> exceptionClass,
+            ExceptionRunnable r) {
+        try {
+            r.run();
+        } catch (Throwable e) {
+            assertTrue("Expected exception type " + exceptionClass.getName() + " but got "
+                    + e.getClass().getName(), exceptionClass.isAssignableFrom(e.getClass()));
+            return;
+        }
+        fail("Expected exception type " + exceptionClass.getName()
+                + ", but no exception was thrown");
+    }
+}