Merge "ImageCardView: use textAppearance" into mnc-ub-dev
diff --git a/v4/api/current.txt b/v4/api/current.txt
index ef8da00..3e1f91a 100644
--- a/v4/api/current.txt
+++ b/v4/api/current.txt
@@ -1094,12 +1094,24 @@
 
   public final class ColorUtils {
     method public static int HSLToColor(float[]);
+    method public static int LABToColor(double, double, double);
+    method public static void LABToXYZ(double, double, double, double[]);
     method public static void RGBToHSL(int, int, int, float[]);
+    method public static void RGBToLAB(int, int, int, double[]);
+    method public static void RGBToXYZ(int, int, int, double[]);
+    method public static int XYZToColor(double, double, double);
+    method public static void XYZToLAB(double, double, double, double[]);
+    method public static int blendARGB(int, int, float);
+    method public static void blendHSL(float[], float[], float[], float);
+    method public static void blendLAB(double[], double[], double[], double);
     method public static double calculateContrast(int, int);
     method public static double calculateLuminance(int);
     method public static int calculateMinimumAlpha(int, int, float);
     method public static void colorToHSL(int, float[]);
+    method public static void colorToLAB(int, double[]);
+    method public static void colorToXYZ(int, double[]);
     method public static int compositeColors(int, int);
+    method public static double distanceEuclidean(double[], double[]);
     method public static int setAlphaComponent(int, int);
   }
 
diff --git a/v4/java/android/support/v4/graphics/ColorUtils.java b/v4/java/android/support/v4/graphics/ColorUtils.java
index 7fef657..2a3074e 100644
--- a/v4/java/android/support/v4/graphics/ColorUtils.java
+++ b/v4/java/android/support/v4/graphics/ColorUtils.java
@@ -21,15 +21,25 @@
 import android.support.annotation.FloatRange;
 import android.support.annotation.IntRange;
 import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.util.TypedValue;
 
 /**
  * A set of color-related utility methods, building upon those available in {@code Color}.
  */
 public final class ColorUtils {
 
+    private static final double XYZ_WHITE_REFERENCE_X = 95.047;
+    private static final double XYZ_WHITE_REFERENCE_Y = 100;
+    private static final double XYZ_WHITE_REFERENCE_Z = 108.883;
+    private static final double XYZ_EPSILON = 0.008856;
+    private static final double XYZ_KAPPA = 903.3;
+
     private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;
     private static final int MIN_ALPHA_SEARCH_PRECISION = 1;
 
+    private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>();
+
     private ColorUtils() {}
 
     /**
@@ -60,24 +70,19 @@
     }
 
     /**
-     * Returns the luminance of a color.
-     * <p>
-     * Formula defined
-     * <a href="http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef">here</a>.
-     * </p>
+     * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}.
+     * <p>Defined as the Y component in the XYZ representation of {@code color}.</p>
      */
     @FloatRange(from = 0.0, to = 1.0)
     public static double calculateLuminance(@ColorInt int color) {
-        double red = Color.red(color) / 255d;
-        red = red < 0.03928 ? red / 12.92 : Math.pow((red + 0.055) / 1.055, 2.4);
-
-        double green = Color.green(color) / 255d;
-        green = green < 0.03928 ? green / 12.92 : Math.pow((green + 0.055) / 1.055, 2.4);
-
-        double blue = Color.blue(color) / 255d;
-        blue = blue < 0.03928 ? blue / 12.92 : Math.pow((blue + 0.055) / 1.055, 2.4);
-
-        return (0.2126 * red) + (0.7152 * green) + (0.0722 * blue);
+        double[] result = TEMP_ARRAY.get();
+        if (result == null) {
+            result = new double[3];
+            TEMP_ARRAY.set(result);
+        }
+        colorToXYZ(color, result);
+        // Luminance is the Y component
+        return result[1] / 100;
     }
 
     /**
@@ -300,6 +305,219 @@
         return (color & 0x00ffffff) | (alpha << 24);
     }
 
+    /**
+     * Convert the ARGB color to its CIE Lab representative components.
+     *
+     * @param color the ARGB color to convert. The alpha component is ignored.
+     * @param result 3 element array which holds the resulting LAB components.
+     */
+    public static void colorToLAB(@ColorInt int color, @NonNull double[] result) {
+        RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), result);
+    }
+
+    /**
+     * Convert RGB components to its CIE Lab representative components.
+     *
+     * <ul>
+     * <li>result[0] is L [0 ...1)</li>
+     * <li>result[1] is a [-128...127)</li>
+     * <li>result[2] is b [-128...127)</li>
+     * </ul>
+     *
+     * @param r   red component value [0..255)
+     * @param g   green component value [0..255)
+     * @param b   blue component value [0..255)
+     * @param result 3 element array which holds the resulting LAB components.
+     */
+    public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r,
+            @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
+            @NonNull double[] result) {
+        // First we convert RGB to XYZ
+        RGBToXYZ(r, g, b, result);
+        // result now contains XYZ
+        XYZToLAB(result[0], result[1], result[2], result);
+        // result now contains LAB representation
+    }
+
+    /**
+     * Convert the ARGB color to it's CIE XYZ representative components.
+     *
+     * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
+     * 2° Standard Observer (1931).</p>
+     *
+     * <ul>
+     * <li>result[0] is X [0 ...95.047)</li>
+     * <li>result[1] is Y [0...100)</li>
+     * <li>result[2] is Z [0...108.883)</li>
+     * </ul>
+     *
+     * @param color the ARGB color to convert. The alpha component is ignored.
+     * @param result 3 element array which holds the resulting LAB components.
+     */
+    public static void colorToXYZ(@ColorInt int color, @NonNull double[] result) {
+        RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), result);
+    }
+
+    /**
+     * Convert RGB components to it's CIE XYZ representative components.
+     *
+     * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
+     * 2° Standard Observer (1931).</p>
+     *
+     * <ul>
+     * <li>result[0] is X [0 ...95.047)</li>
+     * <li>result[1] is Y [0...100)</li>
+     * <li>result[2] is Z [0...108.883)</li>
+     * </ul>
+     *
+     * @param r   red component value [0..255)
+     * @param g   green component value [0..255)
+     * @param b   blue component value [0..255)
+     * @param result 3 element array which holds the resulting XYZ components.
+     */
+    public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r,
+            @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b,
+            @NonNull double[] result) {
+        if (result.length != 3) {
+            throw new IllegalArgumentException("result must have a length of 3.");
+        }
+
+        double sr = r / 255.0;
+        sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4);
+        double sg = g / 255.0;
+        sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4);
+        double sb = b / 255.0;
+        sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4);
+
+        result[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805);
+        result[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722);
+        result[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505);
+    }
+
+    /**
+     * Converts a color from CIE XYZ to CIE Lab representation.
+     *
+     * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
+     * 2° Standard Observer (1931).</p>
+     *
+     * <ul>
+     * <li>result[0] is L [0 ...1)</li>
+     * <li>result[1] is a [-128...127)</li>
+     * <li>result[2] is b [-128...127)</li>
+     * </ul>
+     *
+     * @param x X component value [0...95.047)
+     * @param y Y component value [0...100)
+     * @param z Z component value [0...108.883)
+     * @param result 3 element array which holds the resulting Lab components.
+     */
+    public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
+            @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
+            @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z,
+            @NonNull double[] result) {
+        if (result.length != 3) {
+            throw new IllegalArgumentException("result must have a length of 3.");
+        }
+        x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X);
+        y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y);
+        z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z);
+        result[0] = Math.max(0, 116 * y - 16);
+        result[1] = 500 * (x - y);
+        result[2] = 200 * (y - z);
+    }
+
+    /**
+     * Converts a color from CIE Lab to CIE XYZ representation.
+     *
+     * <p>The resulting XYZ representation will use the D65 illuminant and the CIE
+     * 2° Standard Observer (1931).</p>
+     *
+     * <ul>
+     * <li>result[0] is X [0 ...95.047)</li>
+     * <li>result[1] is Y [0...100)</li>
+     * <li>result[2] is Z [0...108.883)</li>
+     * </ul>
+     *
+     * @param l L component value [0...100)
+     * @param a A component value [-128...127)
+     * @param b B component value [-128...127)
+     * @param result 3 element array which holds the resulting XYZ components.
+     */
+    public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l,
+            @FloatRange(from = -128, to = 127) final double a,
+            @FloatRange(from = -128, to = 127) final double b,
+            @NonNull double[] result) {
+        final double fy = (l + 16) / 116;
+        final double fx = a / 500 + fy;
+        final double fz = fy - b / 200;
+
+        double tmp = Math.pow(fx, 3);
+        final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA;
+        final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA;
+
+        tmp = Math.pow(fz, 3);
+        final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA;
+
+        result[0] = xr * XYZ_WHITE_REFERENCE_X;
+        result[1] = yr * XYZ_WHITE_REFERENCE_Y;
+        result[2] = zr * XYZ_WHITE_REFERENCE_Z;
+    }
+
+    /**
+     * Converts a color from CIE XYZ to its RGB representation.
+     *
+     * <p>This method expects the XYZ representation to use the D65 illuminant and the CIE
+     * 2° Standard Observer (1931).</p>
+     *
+     * @param x X component value [0...95.047)
+     * @param y Y component value [0...100)
+     * @param z Z component value [0...108.883)
+     * @return int containing the RGB representation
+     */
+    @ColorInt
+    public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x,
+            @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y,
+            @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) {
+        double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100;
+        double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100;
+        double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100;
+
+        r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r;
+        g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g;
+        b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b;
+
+        return Color.rgb(
+                constrain((int) Math.round(r * 255), 0, 255),
+                constrain((int) Math.round(g * 255), 0, 255),
+                constrain((int) Math.round(b * 255), 0, 255));
+    }
+
+    /**
+     * Converts a color from CIE Lab to its RGB representation.
+     *
+     * @param l L component value [0...100)
+     * @param a A component value [-128...127)
+     * @param b B component value [-128...127)
+     * @return int containing the RGB representation
+     */
+    @ColorInt
+    public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l,
+            @FloatRange(from = -128, to = 127) final double a,
+            @FloatRange(from = -128, to = 127) final double b) {
+        final double[] result = new double[3];
+        LABToXYZ(l, a, b, result);
+        return XYZToColor(result[0], result[1], result[2]);
+    }
+
+    /**
+     * Returns the euclidean distance between two LAB colors.
+     */
+    public static double distanceEuclidean(@NonNull double[] labX, @NonNull double[] labY) {
+        return Math.sqrt(Math.pow(labX[0] - labY[0], 2)
+                + Math.pow(labX[1] - labY[1], 2)
+                + Math.pow(labX[2] - labY[2], 2));
+    }
+
     private static float constrain(float amount, float low, float high) {
         return amount < low ? low : (amount > high ? high : amount);
     }
@@ -308,4 +526,82 @@
         return amount < low ? low : (amount > high ? high : amount);
     }
 
+    private static double pivotXyzComponent(double component) {
+        return component > XYZ_EPSILON
+                ? Math.pow(component, 1 / 3.0)
+                : (XYZ_KAPPA * component + 16) / 116;
+    }
+
+    /**
+     * Blend between two ARGB colors using the given ratio.
+     *
+     * @param ratio of which to blend. 0.0 will return {@code color1}, 0.5 will give an even blend,
+     *              1.0 will return {@code color2}.
+     */
+    @ColorInt
+    public static int blendARGB(@ColorInt int color1, @ColorInt int color2,
+            @FloatRange(from = 0.0, to = 1.0) float ratio) {
+        final float inverseRatio = 1 - ratio;
+        float a = Color.alpha(color1) * inverseRatio + Color.alpha(color2) * ratio;
+        float r = Color.red(color1) * inverseRatio + Color.red(color2) * ratio;
+        float g = Color.green(color1) * inverseRatio + Color.green(color2) * ratio;
+        float b = Color.blue(color1) * inverseRatio + Color.blue(color2) * ratio;
+        return Color.argb((int) a, (int) r, (int) g, (int) b);
+    }
+
+    /**
+     * Blend between {@code hsl1} and {@code hsl2} using the given ratio. This will interpolate
+     * the hue using the shortest angle.
+     *
+     * @param hsl1 3 element array which holds the first HSL color.
+     * @param hsl2 3 element array which holds the second HSL color.
+     * @param result 3 element array which holds the resulting HSL components.
+     * @param ratio of which to blend. 0.0 will result in {@code hsl1},
+     *              0.5 will give an even blend, 1.0 will return {@code hsl2}.
+     */
+    public static void blendHSL(@NonNull float[] hsl1, @NonNull float[] hsl2,
+            @NonNull float[] result, @FloatRange(from = 0.0, to = 1.0) float ratio) {
+        if (result.length != 3) {
+            throw new IllegalArgumentException("result must have a length of 3.");
+        }
+        final float inverseRatio = 1 - ratio;
+        // Since hue is circular we will need to interpolate carefully
+        result[0] = circularInterpolate(hsl1[0], hsl2[0], ratio);
+        result[1] = hsl1[1] * inverseRatio + hsl2[1] * ratio;
+        result[2] = hsl1[2] * inverseRatio + hsl2[2] * ratio;
+    }
+
+    /**
+     * Blend between two CIE-LAB colors using the given ratio.
+     *
+     * @param lab1 3 element array which holds the first LAB color.
+     * @param lab2 3 element array which holds the second LAB color.
+     * @param result 3 element array which holds the resulting LAB components.
+     * @param ratio of which to blend. 0.0 will result in {@code lab1}, 0.5 will give an even blend,
+     *              1.0 will return {@code lab2}.
+     */
+    public static void blendLAB(@NonNull double[] lab1,
+            @NonNull double[] lab2, @NonNull double[] result,
+            @FloatRange(from = 0.0, to = 1.0) double ratio) {
+        if (result.length != 3) {
+            throw new IllegalArgumentException("result must have a length of 3.");
+        }
+        final double inverseRatio = 1 - ratio;
+        result[0] = lab1[0] * inverseRatio + lab2[0] * ratio;
+        result[1] = lab1[1] * inverseRatio + lab2[1] * ratio;
+        result[2] = lab1[2] * inverseRatio + lab2[2] * ratio;
+    }
+
+    @VisibleForTesting
+    static float circularInterpolate(float a, float b, float f) {
+        if (Math.abs(b - a) > 180) {
+            if (b > a) {
+                a += 360;
+            } else {
+                b += 360;
+            }
+        }
+        return (a + ((b - a) * f)) % 360;
+    }
+
 }
diff --git a/v4/tests/java/android/support/v4/graphics/ColorUtilsTest.java b/v4/tests/java/android/support/v4/graphics/ColorUtilsTest.java
index 56cb6fb..6296903 100644
--- a/v4/tests/java/android/support/v4/graphics/ColorUtilsTest.java
+++ b/v4/tests/java/android/support/v4/graphics/ColorUtilsTest.java
@@ -32,6 +32,8 @@
     private static final float ALLOWED_OFFSET_SATURATION = 0.005f;
     private static final float ALLOWED_OFFSET_LIGHTNESS = 0.005f;
     private static final float ALLOWED_OFFSET_MIN_ALPHA = 0.01f;
+    private static final double ALLOWED_OFFSET_LAB = 0.01;
+    private static final double ALLOWED_OFFSET_XYZ = 0.01;
 
     private static final int ALLOWED_OFFSET_RGB_COMPONENT = 2;
 
@@ -39,38 +41,55 @@
 
     static {
         sEntryList.add(new TestEntry(Color.BLACK).setHsl(0f, 0f, 0f)
+                .setLab(0, 0, 0).setXyz(0, 0, 0)
                 .setWhiteMinAlpha30(0.35f).setWhiteMinAlpha45(0.46f));
+
         sEntryList.add(new TestEntry(Color.WHITE).setHsl(0f, 0f, 1f)
+                .setLab(100, 0.005, -0.01).setXyz(95.05, 100, 108.9)
                 .setBlackMinAlpha30(0.42f).setBlackMinAlpha45(0.54f));
+
         sEntryList.add(new TestEntry(Color.BLUE).setHsl(240f, 1f, 0.5f)
+                .setLab(32.303, 79.197, -107.864).setXyz(18.05, 7.22, 95.05)
                 .setWhiteMinAlpha30(0.55f).setWhiteMinAlpha45(0.71f));
+
         sEntryList.add(new TestEntry(Color.GREEN).setHsl(120f, 1f, 0.5f)
+                .setLab(87.737, -86.185, 83.181).setXyz(35.76, 71.520, 11.920)
                 .setBlackMinAlpha30(0.43f).setBlackMinAlpha45(0.55f));
+
         sEntryList.add(new TestEntry(Color.RED).setHsl(0f, 1f, 0.5f)
+                .setLab(53.233, 80.109, 67.22).setXyz(41.24, 21.26, 1.93)
                 .setWhiteMinAlpha30(0.84f).setBlackMinAlpha30(0.55f).setBlackMinAlpha45(0.78f));
+
         sEntryList.add(new TestEntry(Color.CYAN).setHsl(180f, 1f, 0.5f)
+                .setLab(91.117, -48.08, -14.138).setXyz(53.81, 78.74, 106.97)
                 .setBlackMinAlpha30(0.43f).setBlackMinAlpha45(0.55f));
+
         sEntryList.add(new TestEntry(0xFF2196F3).setHsl(207f, 0.9f, 0.54f)
+                .setLab(60.433, 2.091, -55.116).setXyz(27.711, 28.607, 88.855)
                 .setBlackMinAlpha30(0.52f).setWhiteMinAlpha30(0.97f).setBlackMinAlpha45(0.7f));
+
         sEntryList.add(new TestEntry(0xFFD1C4E9).setHsl(261f, 0.46f, 0.84f)
+                .setLab(81.247, 11.513, -16.677).setXyz(60.742, 58.918, 85.262)
                 .setBlackMinAlpha30(0.45f).setBlackMinAlpha45(0.58f));
+
         sEntryList.add(new TestEntry(0xFF311B92).setHsl(251.09f, 0.687f, 0.339f)
+                .setLab(21.988, 44.301, -60.942).setXyz(6.847, 3.512, 27.511)
                 .setWhiteMinAlpha30(0.39f).setWhiteMinAlpha45(0.54f));
     }
 
-    public void testToHSL() {
+    public void testColorToHSL() {
         for (TestEntry entry : sEntryList) {
             testColorToHSL(entry.rgb, entry.hsl);
         }
     }
 
-    public void testFromHSL() {
+    public void testHSLToColor() {
         for (TestEntry entry : sEntryList) {
             testHSLToColor(entry.hsl, entry.rgb);
         }
     }
 
-    public void testToHslLimits() {
+    public void testColorToHslLimits() {
         final float[] hsl = new float[3];
 
         for (TestEntry entry : sEntryList) {
@@ -82,6 +101,36 @@
         }
     }
 
+    public void testColorToXYZ() {
+        for (TestEntry entry : sEntryList) {
+            testColorToXYZ(entry.rgb, entry.xyz);
+        }
+    }
+
+    public void testColorToLAB() {
+        for (TestEntry entry : sEntryList) {
+            testColorToLAB(entry.rgb, entry.lab);
+        }
+    }
+
+    public void testLABToXYZ() {
+        for (TestEntry entry : sEntryList) {
+            testLABToXYZ(entry.lab, entry.xyz);
+        }
+    }
+
+    public void testXYZToColor() {
+        for (TestEntry entry : sEntryList) {
+            testXYZToColor(entry.xyz, entry.rgb);
+        }
+    }
+
+    public void testLABToColor() {
+        for (TestEntry entry : sEntryList) {
+            testLABToColor(entry.lab, entry.rgb);
+        }
+    }
+
     public void testMinAlphas() {
         for (TestEntry entry : sEntryList) {
             testMinAlpha("Black title", entry.rgb, entry.blackMinAlpha30,
@@ -95,57 +144,107 @@
         }
     }
 
+    public void testCircularInterpolationForwards() {
+        assertEquals(0f, ColorUtils.circularInterpolate(0, 180, 0f));
+        assertEquals(90f, ColorUtils.circularInterpolate(0, 180, 0.5f));
+        assertEquals(180f, ColorUtils.circularInterpolate(0, 180, 1f));
+    }
+
+    public void testCircularInterpolationBackwards() {
+        assertEquals(180f, ColorUtils.circularInterpolate(180, 0, 0f));
+        assertEquals(90f, ColorUtils.circularInterpolate(180, 0, 0.5f));
+        assertEquals(0f, ColorUtils.circularInterpolate(180, 0, 1f));
+    }
+
+    public void testCircularInterpolationCrossZero() {
+        assertEquals(270f, ColorUtils.circularInterpolate(270, 90, 0f));
+        assertEquals(180f, ColorUtils.circularInterpolate(270, 90, 0.5f));
+        assertEquals(90f, ColorUtils.circularInterpolate(270, 90, 1f));
+    }
+
     private static void testMinAlpha(String title, int color, float expected, int actual) {
         final String message = title + " text within error for #" + Integer.toHexString(color);
         if (expected < 0) {
             assertEquals(message, actual, -1);
         } else {
-            assertClose(message, expected, actual / 255f, ALLOWED_OFFSET_MIN_ALPHA);
+            assertEquals(message, expected, actual / 255f, ALLOWED_OFFSET_MIN_ALPHA);
         }
     }
 
-    private static void assertClose(String message, float expected, float actual,
-            float allowedOffset) {
-        StringBuilder sb = new StringBuilder(message);
-        sb.append(". Expected: ").append(expected).append(". Actual: ").append(actual);
-
-        assertTrue(sb.toString(), Math.abs(expected - actual) <= allowedOffset);
-    }
-
-    private static void assertClose(String message, int expected, int actual,
-            int allowedOffset) {
-        StringBuilder sb = new StringBuilder(message);
-        sb.append(". Expected: ").append(expected).append(". Actual: ").append(actual);
-
-        assertTrue(sb.toString(), Math.abs(expected - actual) <= allowedOffset);
-    }
-
-    private static void testColorToHSL(int color, float[] expectedHsl) {
+    private static void testColorToHSL(int color, float[] expected) {
         float[] actualHSL = new float[3];
         ColorUtils.colorToHSL(color, actualHSL);
 
-        assertClose("Hue not within offset", expectedHsl[0], actualHSL[0],
+        assertEquals("Hue not within offset", expected[0], actualHSL[0],
                 ALLOWED_OFFSET_HUE);
-        assertClose("Saturation not within offset", expectedHsl[1], actualHSL[1],
+        assertEquals("Saturation not within offset", expected[1], actualHSL[1],
                 ALLOWED_OFFSET_SATURATION);
-        assertClose("Lightness not within offset", expectedHsl[2], actualHSL[2],
+        assertEquals("Lightness not within offset", expected[2], actualHSL[2],
                 ALLOWED_OFFSET_LIGHTNESS);
     }
 
-    private static void testHSLToColor(float[] hsl, int expectedRgb) {
+    private static void testHSLToColor(float[] hsl, int expected) {
         final int actualRgb = ColorUtils.HSLToColor(hsl);
 
-        assertClose("Red not within offset",
-                Color.red(expectedRgb), Color.red(actualRgb), ALLOWED_OFFSET_RGB_COMPONENT);
-        assertClose("Green not within offset",
-                Color.green(expectedRgb), Color.green(actualRgb), ALLOWED_OFFSET_RGB_COMPONENT);
-        assertClose("Blue not within offset",
-                Color.blue(expectedRgb), Color.blue(actualRgb), ALLOWED_OFFSET_RGB_COMPONENT);
+        assertEquals("Red not within offset", Color.red(expected), Color.red(actualRgb),
+                ALLOWED_OFFSET_RGB_COMPONENT);
+        assertEquals("Green not within offset", Color.green(expected), Color.green(actualRgb),
+                ALLOWED_OFFSET_RGB_COMPONENT);
+        assertEquals("Blue not within offset", Color.blue(expected), Color.blue(actualRgb),
+                ALLOWED_OFFSET_RGB_COMPONENT);
+    }
+
+    private static void testColorToLAB(int color, double[] expected) {
+        double[] result = new double[3];
+        ColorUtils.colorToLAB(color, result);
+
+        assertEquals("L not within offset", expected[0], result[0], ALLOWED_OFFSET_LAB);
+        assertEquals("A not within offset", expected[1], result[1], ALLOWED_OFFSET_LAB);
+        assertEquals("B not within offset", expected[2], result[2], ALLOWED_OFFSET_LAB);
+    }
+
+    private static void testColorToXYZ(int color, double[] expected) {
+        double[] result = new double[3];
+        ColorUtils.colorToXYZ(color, result);
+
+        assertEquals("X not within offset", expected[0], result[0], ALLOWED_OFFSET_XYZ);
+        assertEquals("Y not within offset", expected[1], result[1], ALLOWED_OFFSET_XYZ);
+        assertEquals("Z not within offset", expected[2], result[2], ALLOWED_OFFSET_XYZ);
+    }
+
+    private static void testLABToXYZ(double[] lab, double[] expected) {
+        double[] result = new double[3];
+        ColorUtils.LABToXYZ(lab[0], lab[1], lab[2], result);
+
+        assertEquals("X not within offset", expected[0], result[0], ALLOWED_OFFSET_XYZ);
+        assertEquals("Y not within offset", expected[1], result[1], ALLOWED_OFFSET_XYZ);
+        assertEquals("Z not within offset", expected[2], result[2], ALLOWED_OFFSET_XYZ);
+    }
+
+    private static void testXYZToColor(double[] xyz, int expected) {
+        final int result = ColorUtils.XYZToColor(xyz[0], xyz[1], xyz[2]);
+        assertRGBComponentsClose(expected, result);
+    }
+
+    private static void testLABToColor(double[] lab, int expected) {
+        final int result = ColorUtils.LABToColor(lab[0], lab[1], lab[2]);
+        assertRGBComponentsClose(expected, result);
+    }
+
+    private static void assertRGBComponentsClose(int expected, int actual) {
+        final String message = "Expected: #" + Integer.toHexString(expected)
+                + ", Actual: #" + Integer.toHexString(actual);
+        assertEquals("R not equal: " + message, Color.red(expected), Color.red(actual), 1);
+        assertEquals("G not equal: " + message, Color.green(expected), Color.green(actual), 1);
+        assertEquals("B not equal: " + message, Color.blue(expected), Color.blue(actual), 1);
     }
 
     private static class TestEntry {
         final int rgb;
         final float[] hsl = new float[3];
+        final double[] xyz = new double[3];
+        final double[] lab = new double[3];
+
         float blackMinAlpha45 = -1;
         float blackMinAlpha30 = -1;
         float whiteMinAlpha45 = -1;
@@ -162,6 +261,20 @@
             return this;
         }
 
+        TestEntry setXyz(double x, double y, double z) {
+            xyz[0] = x;
+            xyz[1] = y;
+            xyz[2] = z;
+            return this;
+        }
+
+        TestEntry setLab(double l, double a, double b) {
+            lab[0] = l;
+            lab[1] = a;
+            lab[2] = b;
+            return this;
+        }
+
         TestEntry setBlackMinAlpha30(float minAlpha) {
             blackMinAlpha30 = minAlpha;
             return this;
diff --git a/v7/appcompat/res/values/styles_base.xml b/v7/appcompat/res/values/styles_base.xml
index f952530..dad7570 100644
--- a/v7/appcompat/res/values/styles_base.xml
+++ b/v7/appcompat/res/values/styles_base.xml
@@ -301,7 +301,7 @@
         <item name="subtitleTextAppearance">@style/TextAppearance.Widget.AppCompat.Toolbar.Subtitle</item>
         <item name="android:minHeight">?attr/actionBarSize</item>
         <item name="titleMargins">4dp</item>
-        <item name="maxButtonHeight">56dp</item>
+        <item name="maxButtonHeight">@dimen/abc_action_bar_default_height_material</item>
         <item name="collapseIcon">?attr/homeAsUpIndicator</item>
         <item name="collapseContentDescription">@string/abc_toolbar_collapse_description</item>
         <item name="contentInsetStart">16dp</item>
diff --git a/v7/appcompat/tests/res/values/strings.xml b/v7/appcompat/tests/res/values/strings.xml
index 3780f1b..2cbf27e 100644
--- a/v7/appcompat/tests/res/values/strings.xml
+++ b/v7/appcompat/tests/res/values/strings.xml
@@ -28,4 +28,10 @@
     <string name="alert_dialog_show">Show alert dialog</string>
     <string name="alert_dialog_title">Dialog title</string>
     <string name="alert_dialog_content">Dialog content</string>
+    <string-array name="alert_dialog_items">
+        <item>Albania</item>
+        <item>Belize</item>
+        <item>Chad</item>
+        <item>Djibouti</item>
+    </string-array>
 </resources>
diff --git a/v7/appcompat/tests/src/android/support/v7/app/AlertDialogTest.java b/v7/appcompat/tests/src/android/support/v7/app/AlertDialogTest.java
index db92d76..4213d0e 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/AlertDialogTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/AlertDialogTest.java
@@ -26,7 +26,11 @@
 import android.test.suitebuilder.annotation.SmallTest;
 import android.view.View;
 import android.widget.Button;
+import android.widget.CheckedTextView;
 import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import org.hamcrest.Matcher;
 
 import static android.support.test.espresso.Espresso.onData;
 import static android.support.test.espresso.Espresso.onView;
@@ -40,6 +44,22 @@
 import static org.hamcrest.core.AllOf.allOf;
 import static org.hamcrest.core.Is.is;
 
+/**
+ * Tests in this class make a few assumptions about the underlying implementation of
+ * <code>AlertDialog</code>. While the assumptions don't go all the way down to individual
+ * <code>R.id</code> references or very specific layout arrangements, internal refactoring
+ * of <code>AlertDialog</code> might require corresponding restructuring of the matching
+ * tests. Specifically:
+ *
+ * <ul>
+ *     <li>Testing <code>setIcon</code> API assumes that the icon is displayed by a separate
+ *     <code>ImageView</code> which is a sibling of a title view.</li>
+ *     <li>Testing <code>setMultiChoiceItems</code> API assumes that each item in the list
+ *     is rendered by a single <code></code>CheckedTextView</code>.</li>
+ *     <li>Testing <code>setSingleChoiceItems</code> API assumes that each item in the list
+ *     is rendered by a single <code></code>CheckedTextView</code>.</li>
+ * </ul>
+ */
 public class AlertDialogTest extends ActivityInstrumentationTestCase2<AlertDialogTestActivity> {
     private Button mButton;
 
@@ -86,6 +106,9 @@
         onView(withText("Dialog content")).inRoot(isDialog()).check(matches(isDisplayed()));
         onView(withText("Dialog content")).inRoot(isDialog()).check(
                 isBelow(withText("Dialog title")));
+
+        ListView listView = mAlertDialog.getListView();
+        assertNull("No list view", listView);
     }
 
     @SmallTest
@@ -134,12 +157,42 @@
         assertFalse("Dialog is not canceled", mIsCanceledCalled);
     }
 
+    private void verifySimpleItemsContent(String[] expectedContent) {
+        final int expectedCount = expectedContent.length;
+
+        onView(withId(R.id.test_button)).perform(click());
+
+        final ListView listView = mAlertDialog.getListView();
+        assertNotNull("List view is shown", listView);
+
+        final ListAdapter listAdapter = listView.getAdapter();
+        assertEquals("List has " + expectedCount + " entries",
+                expectedCount, listAdapter.getCount());
+        for (int i = 0; i < expectedCount; i++) {
+            assertEquals("List entry #" + i, expectedContent[i], listAdapter.getItem(i));
+        }
+
+        // Test that all items are showing
+        onView(withText("Dialog title")).inRoot(isDialog()).check(matches(isDisplayed()));
+        for (int i = 0; i < expectedCount; i++) {
+            onData(allOf(is(instanceOf(String.class)), is(expectedContent[i]))).inRoot(isDialog()).
+                    check(matches(isDisplayed()));
+        }
+
+        // Test that a click on an item invokes the registered listener
+        assertEquals("Before list item click", -1, mClickedItemIndex);
+        int indexToClick = expectedCount - 2;
+        onData(allOf(is(instanceOf(String.class)), is(expectedContent[indexToClick]))).
+                inRoot(isDialog()).perform(click());
+        assertEquals("List item clicked", indexToClick, mClickedItemIndex);
+    }
 
     @SmallTest
-    public void testListContent() {
+    public void testSimpleItemsFromRuntimeArray() {
+        final String[] content = new String[] { "Alice", "Bob", "Charlie", "Delta" };
         AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
                 .setTitle(R.string.alert_dialog_title)
-                .setItems(new String[]{"Alice", "Bob", "Charlie", "Delta"},
+                .setItems(content,
                         new DialogInterface.OnClickListener() {
                             @Override
                             public void onClick(DialogInterface dialog, int which) {
@@ -148,23 +201,259 @@
                         });
         wireBuilder(builder);
 
+        verifySimpleItemsContent(content);
+    }
+
+    @SmallTest
+    public void testSimpleItemsFromResourcesArray() {
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
+                .setTitle(R.string.alert_dialog_title)
+                .setItems(R.array.alert_dialog_items,
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int which) {
+                                mClickedItemIndex = which;
+                            }
+                        });
+        wireBuilder(builder);
+
+        verifySimpleItemsContent(
+                getActivity().getResources().getStringArray(R.array.alert_dialog_items));
+    }
+
+    /**
+     * Helper method to verify the state of the multi-choice items list. It gets the String
+     * array of content and verifies that:
+     *
+     * 1. The items in the array are rendered as CheckedTextViews inside a ListView
+     * 2. Each item in the array is displayed
+     * 3. Checked state of each row in the ListView corresponds to the matching entry in the
+     *    passed boolean array
+     */
+    private void verifyMultiChoiceItemsState(String[] expectedContent,
+            boolean[] checkedTracker) {
+        final int expectedCount = expectedContent.length;
+
+        final ListView listView = mAlertDialog.getListView();
+        assertNotNull("List view is shown", listView);
+
+        final ListAdapter listAdapter = listView.getAdapter();
+        assertEquals("List has " + expectedCount + " entries",
+                expectedCount, listAdapter.getCount());
+        for (int i = 0; i < expectedCount; i++) {
+            assertEquals("List entry #" + i, expectedContent[i], listAdapter.getItem(i));
+        }
+
+        for (int i = 0; i < expectedCount; i++) {
+            Matcher checkedStateMatcher = checkedTracker[i] ? TestUtilsMatchers.isCheckedTextView() :
+                    TestUtilsMatchers.isNonCheckedTextView();
+            // Check that the corresponding row is rendered as CheckedTextView with expected
+            // checked state.
+            onData(allOf(is(instanceOf(String.class)), is(expectedContent[i]))).inRoot(isDialog()).
+                    check(matches(allOf(
+                            isDisplayed(),
+                            isAssignableFrom(CheckedTextView.class),
+                            isDescendantOfA(isAssignableFrom(ListView.class)),
+                            checkedStateMatcher)));
+        }
+    }
+
+    private void verifyMultiChoiceItemsContent(String[] expectedContent,
+            final boolean[] checkedTracker) {
+        final int expectedCount = expectedContent.length;
+
+        onView(withId(R.id.test_button)).perform(click());
+
+        final ListView listView = mAlertDialog.getListView();
+        assertNotNull("List view is shown", listView);
+
+        final ListAdapter listAdapter = listView.getAdapter();
+        assertEquals("List has " + expectedCount + " entries",
+                expectedCount, listAdapter.getCount());
+        for (int i = 0; i < expectedCount; i++) {
+            assertEquals("List entry #" + i, expectedContent[i], listAdapter.getItem(i));
+        }
+
+        // Test that all items are showing
+        onView(withText("Dialog title")).inRoot(isDialog()).check(matches(isDisplayed()));
+        verifyMultiChoiceItemsState(expectedContent, checkedTracker);
+
+        // We're going to click item #1 and test that the click listener has been invoked to
+        // update the original state array
+        boolean[] expectedAfterClick1 = checkedTracker.clone();
+        expectedAfterClick1[1] = !expectedAfterClick1[1];
+        onData(allOf(is(instanceOf(String.class)), is(expectedContent[1]))).
+                inRoot(isDialog()).perform(click());
+        verifyMultiChoiceItemsState(expectedContent, expectedAfterClick1);
+
+        // Now click item #1 again and test that the click listener has been invoked to update the
+        // original state array again
+        expectedAfterClick1[1] = !expectedAfterClick1[1];
+        onData(allOf(is(instanceOf(String.class)), is(expectedContent[1]))).
+                inRoot(isDialog()).perform(click());
+        verifyMultiChoiceItemsState(expectedContent, expectedAfterClick1);
+
+        // Now we're going to click the last item and test that the click listener has been invoked
+        // to update the original state array
+        boolean[] expectedAfterClickLast = checkedTracker.clone();
+        expectedAfterClickLast[expectedCount - 1] = !expectedAfterClickLast[expectedCount - 1];
+        onData(allOf(is(instanceOf(String.class)), is(expectedContent[expectedCount - 1]))).
+                inRoot(isDialog()).perform(click());
+        verifyMultiChoiceItemsState(expectedContent, expectedAfterClickLast);
+    }
+
+    @SmallTest
+    public void testMultiChoiceItemsFromRuntimeArray() {
+        final String[] content = new String[] { "Alice", "Bob", "Charlie", "Delta" };
+        final boolean[] checkedTracker = new boolean[] { false, true, false, false };
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
+                .setTitle(R.string.alert_dialog_title)
+                .setMultiChoiceItems(
+                        content, checkedTracker,
+                        new DialogInterface.OnMultiChoiceClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int which,
+                                    boolean isChecked) {
+                                checkedTracker[which] = isChecked;
+                            }
+                        });
+        wireBuilder(builder);
+
+        // Pass the same boolean[] array as used for initialization since our click listener
+        // will be updating its content.
+        verifyMultiChoiceItemsContent(content, checkedTracker);
+    }
+
+    @SmallTest
+    public void testMultiChoiceItemsFromResourcesArray() {
+        final boolean[] checkedTracker = new boolean[] { true, false, true, false };
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
+                .setTitle(R.string.alert_dialog_title)
+                .setMultiChoiceItems(R.array.alert_dialog_items, checkedTracker,
+                        new DialogInterface.OnMultiChoiceClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int which,
+                                    boolean isChecked) {
+                                checkedTracker[which] = isChecked;
+                            }
+                        });
+        wireBuilder(builder);
+
+        verifyMultiChoiceItemsContent(
+                getActivity().getResources().getStringArray(R.array.alert_dialog_items),
+                checkedTracker);
+    }
+
+    /**
+     * Helper method to verify the state of the single-choice items list. It gets the String
+     * array of content and verifies that:
+     *
+     * 1. The items in the array are rendered as CheckedTextViews inside a ListView
+     * 2. Each item in the array is displayed
+     * 3. Only one row in the ListView is checked, and that corresponds to the passed
+     *    integer index.
+     */
+    private void verifySingleChoiceItemsState(String[] expectedContent,
+            int currentlyExpectedSelectionIndex) {
+        final int expectedCount = expectedContent.length;
+
+        final ListView listView = mAlertDialog.getListView();
+        assertNotNull("List view is shown", listView);
+
+        final ListAdapter listAdapter = listView.getAdapter();
+        assertEquals("List has " + expectedCount + " entries",
+                expectedCount, listAdapter.getCount());
+        for (int i = 0; i < expectedCount; i++) {
+            assertEquals("List entry #" + i, expectedContent[i], listAdapter.getItem(i));
+        }
+
+        for (int i = 0; i < expectedCount; i++) {
+            Matcher checkedStateMatcher = (i == currentlyExpectedSelectionIndex) ?
+                    TestUtilsMatchers.isCheckedTextView() :
+                    TestUtilsMatchers.isNonCheckedTextView();
+            // Check that the corresponding row is rendered as CheckedTextView with expected
+            // checked state.
+            onData(allOf(is(instanceOf(String.class)), is(expectedContent[i]))).inRoot(isDialog()).
+                    check(matches(allOf(
+                            isDisplayed(),
+                            isAssignableFrom(CheckedTextView.class),
+                            isDescendantOfA(isAssignableFrom(ListView.class)),
+                            checkedStateMatcher)));
+        }
+    }
+
+    private void verifySingleChoiceItemsContent(String[] expectedContent,
+            int initialSelectionIndex) {
+        final int expectedCount = expectedContent.length;
+        int currentlyExpectedSelectionIndex = initialSelectionIndex;
+
         onView(withId(R.id.test_button)).perform(click());
 
         // Test that all items are showing
         onView(withText("Dialog title")).inRoot(isDialog()).check(matches(isDisplayed()));
-        onData(allOf(is(instanceOf(String.class)), is("Alice"))).inRoot(isDialog()).
-                check(matches(isDisplayed()));
-        onData(allOf(is(instanceOf(String.class)), is("Bob"))).inRoot(isDialog()).
-                check(matches(isDisplayed()));
-        onData(allOf(is(instanceOf(String.class)), is("Charlie"))).inRoot(isDialog()).
-                check(matches(isDisplayed()));
-        onData(allOf(is(instanceOf(String.class)), is("Delta"))).inRoot(isDialog()).
-                check(matches(isDisplayed()));
+        verifySingleChoiceItemsState(expectedContent, currentlyExpectedSelectionIndex);
 
-        // Test that a click on an item invokes the registered listener
-        onData(allOf(is(instanceOf(String.class)), is("Charlie"))).inRoot(isDialog()).
-                perform(click());
-        assertEquals("List item clicked", 2, mClickedItemIndex);
+        // We're going to click the first unselected item and test that the click listener has
+        // been invoked.
+        currentlyExpectedSelectionIndex = (currentlyExpectedSelectionIndex == 0) ? 1 : 0;
+        onData(allOf(is(instanceOf(String.class)),
+                is(expectedContent[currentlyExpectedSelectionIndex]))).
+                    inRoot(isDialog()).perform(click());
+        assertEquals("Selected first single-choice item",
+                currentlyExpectedSelectionIndex, mClickedItemIndex);
+        verifySingleChoiceItemsState(expectedContent, currentlyExpectedSelectionIndex);
+
+        // Now click the same item again and test that the selection has not changed
+        onData(allOf(is(instanceOf(String.class)),
+                is(expectedContent[currentlyExpectedSelectionIndex]))).
+                inRoot(isDialog()).perform(click());
+        assertEquals("Selected first single-choice item again",
+                currentlyExpectedSelectionIndex, mClickedItemIndex);
+        verifySingleChoiceItemsState(expectedContent, currentlyExpectedSelectionIndex);
+
+        // Now we're going to click the last item and test that the click listener has been invoked
+        // to update the original state array
+        currentlyExpectedSelectionIndex = expectedCount - 1;
+        onData(allOf(is(instanceOf(String.class)),
+                is(expectedContent[currentlyExpectedSelectionIndex]))).
+                inRoot(isDialog()).perform(click());
+        assertEquals("Selected last single-choice item",
+                currentlyExpectedSelectionIndex, mClickedItemIndex);
+        verifySingleChoiceItemsState(expectedContent, currentlyExpectedSelectionIndex);
+    }
+
+    @SmallTest
+    public void testSingleChoiceItemsFromRuntimeArray() {
+        final String[] content = new String[] { "Alice", "Bob", "Charlie", "Delta" };
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
+                .setTitle(R.string.alert_dialog_title)
+                .setSingleChoiceItems(
+                        content, 2,
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int which) {
+                                mClickedItemIndex = which;
+                            }
+                        });
+        wireBuilder(builder);
+
+        verifySingleChoiceItemsContent(content, 2);
+    }
+
+    @SmallTest
+    public void testSingleChoiceItemsFromResourcesArray() {
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
+                .setTitle(R.string.alert_dialog_title)
+                .setSingleChoiceItems(R.array.alert_dialog_items, 1,
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int which) {
+                                mClickedItemIndex = which;
+                            }
+                        });
+        wireBuilder(builder);
+
+        verifySingleChoiceItemsContent(new String[] { "Albania", "Belize", "Chad", "Djibouti" }, 1);
     }
 
     @SmallTest
diff --git a/v7/appcompat/tests/src/android/support/v7/testutils/TestUtilsMatchers.java b/v7/appcompat/tests/src/android/support/v7/testutils/TestUtilsMatchers.java
index 6aa8ec2..319583e 100644
--- a/v7/appcompat/tests/src/android/support/v7/testutils/TestUtilsMatchers.java
+++ b/v7/appcompat/tests/src/android/support/v7/testutils/TestUtilsMatchers.java
@@ -20,6 +20,7 @@
 import android.support.annotation.ColorInt;
 import android.support.test.espresso.matcher.BoundedMatcher;
 import android.view.View;
+import android.widget.CheckedTextView;
 import android.widget.ImageView;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
@@ -64,4 +65,56 @@
             }
         };
     }
+
+    /**
+     * Returns a matcher that matches <code>CheckedTextView</code>s which are in checked state.
+     */
+    public static Matcher isCheckedTextView() {
+        return new BoundedMatcher<View, CheckedTextView>(CheckedTextView.class) {
+            private String failedDescription;
+
+            @Override
+            public void describeTo(final Description description) {
+                description.appendText("checked text view: ");
+
+                description.appendText(failedDescription);
+            }
+
+            @Override
+            public boolean matchesSafely(final CheckedTextView view) {
+                if (view.isChecked()) {
+                    return true;
+                }
+
+                failedDescription = "not checked";
+                return false;
+            }
+        };
+    }
+
+    /**
+     * Returns a matcher that matches <code>CheckedTextView</code>s which are in checked state.
+     */
+    public static Matcher isNonCheckedTextView() {
+        return new BoundedMatcher<View, CheckedTextView>(CheckedTextView.class) {
+            private String failedDescription;
+
+            @Override
+            public void describeTo(final Description description) {
+                description.appendText("non checked text view: ");
+
+                description.appendText(failedDescription);
+            }
+
+            @Override
+            public boolean matchesSafely(final CheckedTextView view) {
+                if (!view.isChecked()) {
+                    return true;
+                }
+
+                failedDescription = "checked";
+                return false;
+            }
+        };
+    }
 }