Merge "Second pass for AlertDialog tests." 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/java/android/support/v4/view/ViewPager.java b/v4/java/android/support/v4/view/ViewPager.java
index c6f3648..11acb79 100644
--- a/v4/java/android/support/v4/view/ViewPager.java
+++ b/v4/java/android/support/v4/view/ViewPager.java
@@ -2420,20 +2420,22 @@
             throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
         }
 
-        final VelocityTracker velocityTracker = mVelocityTracker;
-        velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
-        int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
-                velocityTracker, mActivePointerId);
-        mPopulatePending = true;
-        final int width = getClientWidth();
-        final int scrollX = getScrollX();
-        final ItemInfo ii = infoForCurrentScrollPosition();
-        final int currentPage = ii.position;
-        final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor;
-        final int totalDelta = (int) (mLastMotionX - mInitialMotionX);
-        int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
-                totalDelta);
-        setCurrentItemInternal(nextPage, true, true, initialVelocity);
+        if (mAdapter != null) {
+            final VelocityTracker velocityTracker = mVelocityTracker;
+            velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+            int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
+                    velocityTracker, mActivePointerId);
+            mPopulatePending = true;
+            final int width = getClientWidth();
+            final int scrollX = getScrollX();
+            final ItemInfo ii = infoForCurrentScrollPosition();
+            final int currentPage = ii.position;
+            final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor;
+            final int totalDelta = (int) (mLastMotionX - mInitialMotionX);
+            int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
+                    totalDelta);
+            setCurrentItemInternal(nextPage, true, true, initialVelocity);
+        }
         endDrag();
 
         mFakeDragging = false;
@@ -2451,6 +2453,10 @@
             throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first.");
         }
 
+        if (mAdapter == null) {
+            return;
+        }
+
         mLastMotionX += xOffset;
 
         float oldScrollX = getScrollX();
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>