Extract color for the hotseat.

- Only considers the bottom fourth of the wallpaper
- Is translucent black or white depending on how dark/light
  the wallpaper is
- Hotseat extends behind the nav bar

Bug: 27230217
Change-Id: Id4ea6ee91b4dd221b4c277d22d5041cab178801d
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index bb70be6..7e1ecf5 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -16,8 +16,12 @@
 
 package com.android.launcher3;
 
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
 import android.content.Context;
+import android.graphics.Color;
 import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
@@ -27,12 +31,13 @@
 import android.widget.FrameLayout;
 import android.widget.TextView;
 
+import com.android.launcher3.dynamicui.ExtractedColors;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 
 public class Hotseat extends FrameLayout
-        implements UserEventDispatcher.LaunchSourceProvider{
+        implements UserEventDispatcher.LaunchSourceProvider, Insettable {
 
     private CellLayout mContent;
 
@@ -44,6 +49,14 @@
     @ViewDebug.ExportedProperty(category = "launcher")
     private final boolean mHasVerticalHotseat;
 
+    @ViewDebug.ExportedProperty(category = "launcher")
+    private Rect mInsets = new Rect();
+
+    @ViewDebug.ExportedProperty(category = "launcher")
+    private int mBackgroundColor;
+    @ViewDebug.ExportedProperty(category = "launcher")
+    private ColorDrawable mBackground;
+
     public Hotseat(Context context) {
         this(context, null);
     }
@@ -56,6 +69,8 @@
         super(context, attrs, defStyle);
         mLauncher = (Launcher) context;
         mHasVerticalHotseat = mLauncher.getDeviceProfile().isVerticalBarLayout();
+        mBackground = new ColorDrawable();
+        setBackground(mBackground);
     }
 
     public CellLayout getLayout() {
@@ -166,4 +181,46 @@
         target.gridY = info.cellY;
         targetParent.containerType = LauncherLogProto.HOTSEAT;
     }
+
+    //Overridden so that the background color extends behind the navigation buttons.
+    @Override
+    public void setInsets(Rect insets) {
+        int rightInset = insets.right - mInsets.right;
+        int bottomInset = insets.bottom - mInsets.bottom;
+        mInsets.set(insets);
+        LayoutParams lp = (LayoutParams) getLayoutParams();
+        if (mHasVerticalHotseat) {
+            setPadding(getPaddingLeft(), getPaddingTop(),
+            getPaddingRight() + rightInset, getPaddingBottom());
+            if (lp.width != LayoutParams.MATCH_PARENT && lp.width != LayoutParams.WRAP_CONTENT) {
+                lp.width += rightInset;
+            }
+        } else {
+            setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
+            getPaddingBottom() + bottomInset);
+            if (lp.height != LayoutParams.MATCH_PARENT && lp.height != LayoutParams.WRAP_CONTENT) {
+                lp.height += bottomInset;
+            }
+        }
+    }
+
+    public void updateColor(ExtractedColors extractedColors, boolean animate) {
+        if (!mHasVerticalHotseat) {
+            int color = extractedColors.getColor(ExtractedColors.HOTSEAT_INDEX, Color.TRANSPARENT);
+            if (!animate) {
+                setBackgroundColor(color);
+            } else {
+                ValueAnimator animator = ValueAnimator.ofInt(mBackgroundColor, color);
+                animator.setEvaluator(new ArgbEvaluator());
+                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                    @Override
+                    public void onAnimationUpdate(ValueAnimator animation) {
+                        mBackground.setColor((Integer) animation.getAnimatedValue());
+                    }
+                });
+                animator.start();
+            }
+            mBackgroundColor = color;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 21adcb7..2a83299 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -496,9 +496,10 @@
     }
 
     private void loadExtractedColorsAndColorItems() {
-        if (mExtractedColors != null) {
+        // TODO: do this in pre-N as well, once the extraction part is complete.
+        if (mExtractedColors != null && Utilities.isNycOrAbove()) {
             mExtractedColors.load(this);
-            // TODO: pass mExtractedColors to interested items such as hotseat.
+            mHotseat.updateColor(mExtractedColors, !mPaused);
         }
     }
 
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 871f390..c5f601d 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -46,7 +46,6 @@
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.PaintDrawable;
 import android.os.Build;
-import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
 import android.os.PowerManager;
 import android.text.Spannable;
diff --git a/src/com/android/launcher3/dynamicui/ColorExtractionService.java b/src/com/android/launcher3/dynamicui/ColorExtractionService.java
index 95a62b9..89594f4 100644
--- a/src/com/android/launcher3/dynamicui/ColorExtractionService.java
+++ b/src/com/android/launcher3/dynamicui/ColorExtractionService.java
@@ -32,6 +32,9 @@
  */
 public class ColorExtractionService extends IntentService {
 
+    /** The fraction of the wallpaper to extract colors for use on the hotseat. */
+    private static final float HOTSEAT_FRACTION = 1f / 4;
+
     public ColorExtractionService() {
         super("ColorExtractionService");
     }
@@ -44,10 +47,21 @@
         if (wallpaperManager.getWallpaperInfo() != null) {
             // We can't extract colors from live wallpapers, so just use the default color always.
             extractedColors.updatePalette(null);
+            extractedColors.updateHotseatPalette(null);
         } else {
             Bitmap wallpaper = ((BitmapDrawable) wallpaperManager.getDrawable()).getBitmap();
             Palette palette = Palette.from(wallpaper).generate();
             extractedColors.updatePalette(palette);
+            // We extract colors for the hotseat separately,
+            // since it only considers the lower part of the wallpaper.
+            // TODO(twickham): update Palette library to 23.3.1 or higher,
+            // which fixes a bug with using regions (b/28349435).
+            Palette hotseatPalette = Palette.from(wallpaper)
+                    .setRegion(0, (int) (wallpaper.getHeight() * (1f - HOTSEAT_FRACTION)),
+                            wallpaper.getWidth(), wallpaper.getHeight())
+                    .clearFilters()
+                    .generate();
+            extractedColors.updateHotseatPalette(hotseatPalette);
         }
 
         // Save the extracted colors and wallpaper id to LauncherProvider.
diff --git a/src/com/android/launcher3/dynamicui/ExtractedColors.java b/src/com/android/launcher3/dynamicui/ExtractedColors.java
index 4d17ff7..e545288 100644
--- a/src/com/android/launcher3/dynamicui/ExtractedColors.java
+++ b/src/com/android/launcher3/dynamicui/ExtractedColors.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.graphics.Color;
+import android.support.v4.graphics.ColorUtils;
 import android.support.v7.graphics.Palette;
 import android.util.Log;
 
@@ -35,26 +36,30 @@
 
     // These color profile indices should NOT be changed, since they are used when saving and
     // loading extracted colors. New colors should always be added at the end.
-    public static final int DEFAULT_INDEX = 0;
-    public static final int VIBRANT_INDEX = 1;
-    public static final int VIBRANT_DARK_INDEX = 2;
-    public static final int VIBRANT_LIGHT_INDEX = 3;
-    public static final int MUTED_INDEX = 4;
-    public static final int MUTED_DARK_INDEX = 5;
-    public static final int MUTED_LIGHT_INDEX = 6;
+    public static final int VERSION_INDEX = 0;
+    public static final int HOTSEAT_INDEX = 1;
+    // public static final int VIBRANT_INDEX = 2;
+    // public static final int VIBRANT_DARK_INDEX = 3;
+    // public static final int VIBRANT_LIGHT_INDEX = 4;
+    // public static final int MUTED_INDEX = 5;
+    // public static final int MUTED_DARK_INDEX = 6;
+    // public static final int MUTED_LIGHT_INDEX = 7;
 
-    public static final int NUM_COLOR_PROFILES = 7;
+    public static final int NUM_COLOR_PROFILES = 1;
+    private static final int VERSION = 1;
 
     private static final String COLOR_SEPARATOR = ",";
 
     private int[] mColors;
 
     public ExtractedColors() {
-        mColors = new int[NUM_COLOR_PROFILES];
+        // The first entry is reserved for the version number.
+        mColors = new int[NUM_COLOR_PROFILES + 1];
+        mColors[VERSION_INDEX] = VERSION;
     }
 
     public void setColorAtIndex(int index, int color) {
-        if (index >= 0 && index < mColors.length) {
+        if (index > VERSION_INDEX && index < mColors.length) {
             mColors[index] = color;
         } else {
             Log.e(TAG, "Attempted to set a color at an invalid index " + index);
@@ -89,17 +94,21 @@
      */
     public void load(Context context) {
         String encodedString = Utilities.getPrefs(context).getString(
-                ExtractionUtils.EXTRACTED_COLORS_PREFERENCE_KEY, DEFAULT_COLOR + "");
+                ExtractionUtils.EXTRACTED_COLORS_PREFERENCE_KEY, VERSION + "");
 
         decodeFromString(encodedString);
+
+        if (mColors[VERSION_INDEX] != VERSION) {
+            ExtractionUtils.startColorExtractionService(context);
+        }
     }
 
     /** @param index must be one of the index values defined at the top of this class. */
-    public int getColor(int index) {
-        if (index >= 0 && index < mColors.length) {
+    public int getColor(int index, int defaultColor) {
+        if (index > VERSION_INDEX && index < mColors.length) {
             return mColors[index];
         }
-        return DEFAULT_COLOR;
+        return defaultColor;
     }
 
     /**
@@ -112,20 +121,39 @@
                 setColorAtIndex(i, ExtractedColors.DEFAULT_COLOR);
             }
         } else {
-            setColorAtIndex(ExtractedColors.DEFAULT_INDEX,
-                    ExtractedColors.DEFAULT_COLOR);
-            setColorAtIndex(ExtractedColors.VIBRANT_INDEX,
-                    palette.getVibrantColor(ExtractedColors.DEFAULT_COLOR));
-            setColorAtIndex(ExtractedColors.VIBRANT_DARK_INDEX,
-                    palette.getDarkVibrantColor(ExtractedColors.DEFAULT_DARK));
-            setColorAtIndex(ExtractedColors.VIBRANT_LIGHT_INDEX,
-                    palette.getLightVibrantColor(ExtractedColors.DEFAULT_LIGHT));
-            setColorAtIndex(ExtractedColors.MUTED_INDEX,
-                    palette.getMutedColor(ExtractedColors.DEFAULT_COLOR));
-            setColorAtIndex(ExtractedColors.MUTED_DARK_INDEX,
-                    palette.getDarkMutedColor(ExtractedColors.DEFAULT_DARK));
-            setColorAtIndex(ExtractedColors.MUTED_LIGHT_INDEX,
-                    palette.getLightVibrantColor(ExtractedColors.DEFAULT_LIGHT));
+            // We currently don't use any of the colors defined by the Palette API,
+            // but this is how we would add them if we ever need them.
+
+            // setColorAtIndex(ExtractedColors.VIBRANT_INDEX,
+                // palette.getVibrantColor(ExtractedColors.DEFAULT_COLOR));
+            // setColorAtIndex(ExtractedColors.VIBRANT_DARK_INDEX,
+                // palette.getDarkVibrantColor(ExtractedColors.DEFAULT_DARK));
+            // setColorAtIndex(ExtractedColors.VIBRANT_LIGHT_INDEX,
+                // palette.getLightVibrantColor(ExtractedColors.DEFAULT_LIGHT));
+            // setColorAtIndex(ExtractedColors.MUTED_INDEX,
+                // palette.getMutedColor(DEFAULT_COLOR));
+            // setColorAtIndex(ExtractedColors.MUTED_DARK_INDEX,
+                // palette.getDarkMutedColor(ExtractedColors.DEFAULT_DARK));
+            // setColorAtIndex(ExtractedColors.MUTED_LIGHT_INDEX,
+                // palette.getLightVibrantColor(ExtractedColors.DEFAULT_LIGHT));
         }
     }
+
+    /**
+     * The hotseat's color is defined as follows:
+     * - 12% black for super light wallpaper
+     * - 18% white for super dark
+     * - 25% white otherwise
+     */
+    public void updateHotseatPalette(Palette hotseatPalette) {
+        int hotseatColor;
+        if (hotseatPalette != null && ExtractionUtils.isSuperLight(hotseatPalette)) {
+            hotseatColor = ColorUtils.setAlphaComponent(Color.BLACK, (int) (0.12f * 255));
+        } else if (hotseatPalette != null && ExtractionUtils.isSuperDark(hotseatPalette)) {
+            hotseatColor = ColorUtils.setAlphaComponent(Color.WHITE, (int) (0.18f * 255));
+        } else {
+            hotseatColor = ColorUtils.setAlphaComponent(Color.WHITE, (int) (0.25f * 255));
+        }
+        setColorAtIndex(HOTSEAT_INDEX, hotseatColor);
+    }
 }
diff --git a/src/com/android/launcher3/dynamicui/ExtractionUtils.java b/src/com/android/launcher3/dynamicui/ExtractionUtils.java
index 0b28ba6..6dc0035 100644
--- a/src/com/android/launcher3/dynamicui/ExtractionUtils.java
+++ b/src/com/android/launcher3/dynamicui/ExtractionUtils.java
@@ -20,11 +20,15 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.support.v4.graphics.ColorUtils;
+import android.support.v7.graphics.Palette;
 
 import com.android.launcher3.Utilities;
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.util.List;
 
 /**
  * Contains helper fields and methods related to extracting colors from the wallpaper.
@@ -34,6 +38,7 @@
     public static final String WALLPAPER_ID_PREFERENCE_KEY = "pref_wallpaperId";
 
     private static final int FLAG_SET_SYSTEM = 1 << 0; // TODO: use WallpaperManager.FLAG_SET_SYSTEM
+    private static final float MIN_CONTRAST_RATIO = 2f;
 
     /**
      * Extract colors in the :wallpaper-chooser process, if the wallpaper id has changed.
@@ -46,12 +51,17 @@
             @Override
             public void run() {
                 if (hasWallpaperIdChanged(context)) {
-                    context.startService(new Intent(context, ColorExtractionService.class));
+                    startColorExtractionService(context);
                 }
             }
         });
     }
 
+    /** Starts the {@link ColorExtractionService} without checking the wallpaper id */
+    public static void startColorExtractionService(Context context) {
+        context.startService(new Intent(context, ColorExtractionService.class));
+    }
+
     private static boolean hasWallpaperIdChanged(Context context) {
         if (!Utilities.isNycOrAbove()) {
             // TODO: update an id in sharedprefs in onWallpaperChanged broadcast, and read it here.
@@ -72,4 +82,36 @@
             return -1;
         }
     }
+
+    public static boolean isSuperLight(Palette p) {
+        return !isLegibleOnWallpaper(Color.WHITE, p.getSwatches());
+    }
+
+    public static boolean isSuperDark(Palette p) {
+        return !isLegibleOnWallpaper(Color.BLACK, p.getSwatches());
+    }
+
+    /**
+     * Given a color, returns true if that color is legible on
+     * the given wallpaper color swatches, else returns false.
+     */
+    private static boolean isLegibleOnWallpaper(int color, List<Palette.Swatch> wallpaperSwatches) {
+        int legiblePopulation = 0;
+        int illegiblePopulation = 0;
+        for (Palette.Swatch swatch : wallpaperSwatches) {
+            if (isLegible(color, swatch.getRgb())) {
+                legiblePopulation += swatch.getPopulation();
+            } else {
+                illegiblePopulation += swatch.getPopulation();
+            }
+        }
+        return legiblePopulation > illegiblePopulation;
+    }
+
+    /** @return Whether the foreground color is legible on the background color. */
+    private static boolean isLegible(int foreground, int background) {
+        background = ColorUtils.setAlphaComponent(background, 255);
+        return ColorUtils.calculateContrast(foreground, background) >= MIN_CONTRAST_RATIO;
+    }
+
 }