Adding support for themed calendar and clock icons

Bug: 183641907
Test: Manual
Change-Id: I6452a4b4096ef45e03768afbef3e1a3a4b58299b
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
index 845a8ec..06b39b8 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
@@ -22,6 +22,7 @@
 import android.graphics.Bitmap.Config;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.os.UserHandle;
 import android.util.Log;
@@ -146,14 +147,17 @@
         /**
          * Called for creating a custom BitmapInfo
          */
-        default BitmapInfo getExtendedInfo(Bitmap bitmap, int color,
-                BaseIconFactory iconFactory, float normalizationScale, UserHandle user) {
-            return BitmapInfo.of(bitmap, color);
-        }
+        BitmapInfo getExtendedInfo(Bitmap bitmap, int color,
+                BaseIconFactory iconFactory, float normalizationScale, UserHandle user);
 
         /**
          * Called to draw the UI independent of any runtime configurations like time or theme
          */
-        default void drawForPersistence(Canvas canvas) { }
+        void drawForPersistence(Canvas canvas);
+
+        /**
+         * Returns a new icon with theme applied
+         */
+        Drawable getThemedDrawable(Context context);
     }
 }
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java b/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java
index fe00428..ad9267a 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java
@@ -15,14 +15,25 @@
  */
 package com.android.launcher3.icons;
 
+import static com.android.launcher3.icons.ThemedIconDrawable.getColors;
+
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffColorFilter;
 import android.graphics.Rect;
 import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.LayerDrawable;
 import android.os.Build;
@@ -31,11 +42,15 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.util.Log;
+import android.util.TypedValue;
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.icons.ThemedIconDrawable.ThemeData;
+
 import java.util.Calendar;
 import java.util.concurrent.TimeUnit;
+import java.util.function.IntFunction;
 
 /**
  * Wrapper over {@link AdaptiveIconDrawable} to intercept icon flattening logic for dynamic
@@ -74,6 +89,7 @@
 
     private final AnimationInfo mAnimationInfo = new AnimationInfo();
     private int mTargetSdkVersion;
+    protected ThemeData mThemeData;
 
     public ClockDrawableWrapper(AdaptiveIconDrawable base) {
         super(base.getBackground(), base.getForeground());
@@ -88,60 +104,94 @@
             PackageManager pm = context.getPackageManager();
             ApplicationInfo appInfo =  pm.getApplicationInfo(pkg,
                     PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA);
-            final Bundle metadata = appInfo.metaData;
-            if (metadata == null) {
-                return null;
-            }
-            int drawableId = metadata.getInt(ROUND_ICON_METADATA_KEY, 0);
-            if (drawableId == 0) {
-                return null;
-            }
-
-            Drawable drawable = pm.getResourcesForApplication(appInfo).getDrawableForDensity(
-                    drawableId, iconDpi).mutate();
-            if (!(drawable instanceof AdaptiveIconDrawable)) {
-                return null;
-            }
-
-            ClockDrawableWrapper wrapper =
-                    new ClockDrawableWrapper((AdaptiveIconDrawable) drawable);
-            wrapper.mTargetSdkVersion = appInfo.targetSdkVersion;
-            AnimationInfo info = wrapper.mAnimationInfo;
-
-            info.baseDrawableState = drawable.getConstantState();
-
-            info.hourLayerIndex = metadata.getInt(HOUR_INDEX_METADATA_KEY, INVALID_VALUE);
-            info.minuteLayerIndex = metadata.getInt(MINUTE_INDEX_METADATA_KEY, INVALID_VALUE);
-            info.secondLayerIndex = metadata.getInt(SECOND_INDEX_METADATA_KEY, INVALID_VALUE);
-
-            info.defaultHour = metadata.getInt(DEFAULT_HOUR_METADATA_KEY, 0);
-            info.defaultMinute = metadata.getInt(DEFAULT_MINUTE_METADATA_KEY, 0);
-            info.defaultSecond = metadata.getInt(DEFAULT_SECOND_METADATA_KEY, 0);
-
-            LayerDrawable foreground = (LayerDrawable) wrapper.getForeground();
-            int layerCount = foreground.getNumberOfLayers();
-            if (info.hourLayerIndex < 0 || info.hourLayerIndex >= layerCount) {
-                info.hourLayerIndex = INVALID_VALUE;
-            }
-            if (info.minuteLayerIndex < 0 || info.minuteLayerIndex >= layerCount) {
-                info.minuteLayerIndex = INVALID_VALUE;
-            }
-            if (info.secondLayerIndex < 0 || info.secondLayerIndex >= layerCount) {
-                info.secondLayerIndex = INVALID_VALUE;
-            } else if (DISABLE_SECONDS) {
-                foreground.setDrawable(info.secondLayerIndex, null);
-                info.secondLayerIndex = INVALID_VALUE;
-            }
-            info.applyTime(Calendar.getInstance(), foreground);
-            return wrapper;
+            Resources res = pm.getResourcesForApplication(appInfo);
+            return forExtras(appInfo, appInfo.metaData,
+                    resId -> res.getDrawableForDensity(resId, iconDpi));
         } catch (Exception e) {
             Log.d(TAG, "Unable to load clock drawable info", e);
         }
         return null;
     }
 
+    private static ClockDrawableWrapper fromThemeData(Context context, ThemeData themeData) {
+        try {
+            TypedArray ta = themeData.mResources.obtainTypedArray(themeData.mResID);
+            int count = ta.length();
+            Bundle extras = new Bundle();
+            for (int i = 0; i < count; i += 2) {
+                TypedValue v = ta.peekValue(i + 1);
+                extras.putInt(ta.getString(i), v.type >= TypedValue.TYPE_FIRST_INT
+                        && v.type <= TypedValue.TYPE_LAST_INT
+                        ? v.data : v.resourceId);
+            }
+            ta.recycle();
+            ClockDrawableWrapper drawable = ClockDrawableWrapper.forExtras(
+                    context.getApplicationInfo(), extras, resId -> {
+                        int[] colors = getColors(context);
+                        Drawable bg = new ColorDrawable(colors[0]);
+                        Drawable fg = themeData.mResources.getDrawable(resId).mutate();
+                        fg.setTint(colors[1]);
+                        return new AdaptiveIconDrawable(bg, fg);
+                    });
+            if (drawable != null) {
+                return drawable;
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Error loading themed clock", e);
+        }
+        return null;
+    }
+
+    private static ClockDrawableWrapper forExtras(ApplicationInfo appInfo, Bundle metadata,
+            IntFunction<Drawable> drawableProvider) {
+        if (metadata == null) {
+            return null;
+        }
+        int drawableId = metadata.getInt(ROUND_ICON_METADATA_KEY, 0);
+        if (drawableId == 0) {
+            return null;
+        }
+
+        Drawable drawable = drawableProvider.apply(drawableId).mutate();
+        if (!(drawable instanceof AdaptiveIconDrawable)) {
+            return null;
+        }
+
+        ClockDrawableWrapper wrapper =
+                new ClockDrawableWrapper((AdaptiveIconDrawable) drawable);
+        wrapper.mTargetSdkVersion = appInfo.targetSdkVersion;
+        AnimationInfo info = wrapper.mAnimationInfo;
+
+        info.baseDrawableState = drawable.getConstantState();
+
+        info.hourLayerIndex = metadata.getInt(HOUR_INDEX_METADATA_KEY, INVALID_VALUE);
+        info.minuteLayerIndex = metadata.getInt(MINUTE_INDEX_METADATA_KEY, INVALID_VALUE);
+        info.secondLayerIndex = metadata.getInt(SECOND_INDEX_METADATA_KEY, INVALID_VALUE);
+
+        info.defaultHour = metadata.getInt(DEFAULT_HOUR_METADATA_KEY, 0);
+        info.defaultMinute = metadata.getInt(DEFAULT_MINUTE_METADATA_KEY, 0);
+        info.defaultSecond = metadata.getInt(DEFAULT_SECOND_METADATA_KEY, 0);
+
+        LayerDrawable foreground = (LayerDrawable) wrapper.getForeground();
+        int layerCount = foreground.getNumberOfLayers();
+        if (info.hourLayerIndex < 0 || info.hourLayerIndex >= layerCount) {
+            info.hourLayerIndex = INVALID_VALUE;
+        }
+        if (info.minuteLayerIndex < 0 || info.minuteLayerIndex >= layerCount) {
+            info.minuteLayerIndex = INVALID_VALUE;
+        }
+        if (info.secondLayerIndex < 0 || info.secondLayerIndex >= layerCount) {
+            info.secondLayerIndex = INVALID_VALUE;
+        } else if (DISABLE_SECONDS) {
+            foreground.setDrawable(info.secondLayerIndex, null);
+            info.secondLayerIndex = INVALID_VALUE;
+        }
+        info.applyTime(Calendar.getInstance(), foreground);
+        return wrapper;
+    }
+
     @Override
-    public BitmapInfo getExtendedInfo(Bitmap bitmap, int color,
+    public ClockBitmapInfo getExtendedInfo(Bitmap bitmap, int color,
             BaseIconFactory iconFactory, float normalizationScale, UserHandle user) {
         iconFactory.disableColorExtraction();
         AdaptiveIconDrawable background = new AdaptiveIconDrawable(
@@ -150,7 +200,7 @@
                 Process.myUserHandle(), mTargetSdkVersion, false);
 
         return new ClockBitmapInfo(bitmap, color, normalizationScale,
-                mAnimationInfo, bitmapInfo.icon);
+                mAnimationInfo, bitmapInfo.icon, mThemeData);
     }
 
     @Override
@@ -163,6 +213,15 @@
         mAnimationInfo.applyTime(Calendar.getInstance(), (LayerDrawable) getForeground());
     }
 
+    @Override
+    public Drawable getThemedDrawable(Context context) {
+        if (mThemeData != null) {
+            ClockDrawableWrapper drawable = fromThemeData(context, mThemeData);
+            return drawable == null ? this : drawable;
+        }
+        return this;
+    }
+
     private void resetLevel(LayerDrawable drawable, int index) {
         if (index != INVALID_VALUE) {
             drawable.getDrawable(index).setLevel(0);
@@ -214,20 +273,47 @@
         }
     }
 
-    private static class ClockBitmapInfo extends BitmapInfo {
+    static class ClockBitmapInfo extends BitmapInfo {
 
         public final float scale;
         public final int offset;
         public final AnimationInfo animInfo;
         public final Bitmap mFlattenedBackground;
 
+        public final ThemeData themeData;
+
         ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo,
-                Bitmap background) {
+                Bitmap background, ThemeData themeData) {
             super(icon, color);
             this.scale = scale;
             this.animInfo = animInfo;
             this.offset = (int) Math.ceil(ShadowGenerator.BLUR_FACTOR * icon.getWidth());
             this.mFlattenedBackground = background;
+            this.themeData = themeData;
+        }
+
+        @Override
+        public FastBitmapDrawable newThemedIcon(Context context) {
+            if (themeData != null) {
+                ClockDrawableWrapper wrapper = fromThemeData(context, themeData);
+                if (wrapper != null) {
+                    ColorFilter bgFilter = new PorterDuffColorFilter(
+                            getColors(context)[0], Mode.SRC_ATOP);
+                    ClockBitmapInfo bitmapInfo = new ClockBitmapInfo(icon, color, scale,
+                            wrapper.mAnimationInfo, mFlattenedBackground, themeData) {
+
+                        @Override
+                        void drawBackground(Canvas canvas, Rect bounds, Paint paint) {
+                            ColorFilter oldFilter = paint.getColorFilter();
+                            paint.setColorFilter(bgFilter);
+                            super.drawBackground(canvas, bounds, paint);
+                            paint.setColorFilter(oldFilter);
+                        }
+                    };
+                    return bitmapInfo.newIcon(context);
+                }
+            }
+            return super.newThemedIcon(context);
         }
 
         @Override
@@ -242,6 +328,11 @@
         public byte[] toByteArray() {
             return null;
         }
+
+        void drawBackground(Canvas canvas, Rect bounds, Paint paint) {
+            // draw the background that is already flattened to a bitmap
+            canvas.drawBitmap(mFlattenedBackground, null, bounds, paint);
+        }
     }
 
     private static class ClockIconDrawable extends FastBitmapDrawable implements Runnable {
@@ -274,8 +365,7 @@
                 super.drawInternal(canvas, bounds);
                 return;
             }
-            // draw the background that is already flattened to a bitmap
-            canvas.drawBitmap(mInfo.mFlattenedBackground, null, bounds, mPaint);
+            mInfo.drawBackground(canvas, bounds, mPaint);
 
             // prepare and draw the foreground
             mInfo.animInfo.applyTime(mTime, mForeground);
diff --git a/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java b/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
index 6c96967..449c0da 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
@@ -82,6 +82,10 @@
     private final ComponentName mCalendar;
     private final ComponentName mClock;
 
+    static final int ICON_TYPE_DEFAULT = 0;
+    static final int ICON_TYPE_CALENDAR = 1;
+    static final int ICON_TYPE_CLOCK = 2;
+
     public IconProvider(Context context) {
         this(context, false);
     }
@@ -143,17 +147,23 @@
             Supplier<Drawable> fallback) {
         Drawable icon = null;
 
+        int iconType = ICON_TYPE_DEFAULT;
         if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
             icon = loadCalendarDrawable(iconDpi);
+            iconType = ICON_TYPE_CALENDAR;
         } else if (mClock != null
                 && mClock.getPackageName().equals(packageName)
                 && Process.myUserHandle().equals(user)) {
             icon = loadClockDrawable(iconDpi);
+            iconType = ICON_TYPE_CLOCK;
         }
-        icon = icon == null ? fallback.get() : icon;
+        if (icon == null) {
+            icon = fallback.get();
+            iconType = ICON_TYPE_DEFAULT;
+        }
 
         ThemeData td = getThemedIconMap().get(packageName);
-        return td != null ? td.wrapDrawable(icon) : icon;
+        return td != null ? td.wrapDrawable(icon, iconType) : icon;
     }
 
     private Drawable loadActivityInfoIcon(ActivityInfo ai, int density) {
@@ -265,7 +275,7 @@
     /**
      * @return Today's day of the month, zero-indexed.
      */
-    private int getDay() {
+    static int getDay() {
         return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - 1;
     }
 
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java
index 7425c68..da62712 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java
@@ -20,9 +20,12 @@
 import static android.content.res.Resources.ID_NULL;
 
 import static com.android.launcher3.icons.GraphicsUtils.getExpectedBitmapSize;
+import static com.android.launcher3.icons.IconProvider.ICON_TYPE_CALENDAR;
+import static com.android.launcher3.icons.IconProvider.ICON_TYPE_CLOCK;
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
@@ -38,6 +41,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.icons.BitmapInfo.Extender;
+import com.android.launcher3.icons.ClockDrawableWrapper.ClockBitmapInfo;
 import com.android.launcher3.icons.cache.BaseIconCache;
 
 import java.io.ByteArrayInputStream;
@@ -72,6 +76,7 @@
         mBgWrapper = new AdaptiveIconDrawable(new ColorDrawable(colorBg), null);
         mBadgeBounds = bitmapInfo.mUserBadge == null ? null :
                 new Rect(0, 0, bitmapInfo.mUserBadge.getWidth(), bitmapInfo.mUserBadge.getHeight());
+
     }
 
     @Override
@@ -147,7 +152,7 @@
             if (isNullOrLowRes()) {
                 return null;
             }
-            String resName = mThemeData.mResources.getResourceName(mThemeData.mIconID);
+            String resName = mThemeData.mResources.getResourceName(mThemeData.mResID);
             ByteArrayOutputStream out = new ByteArrayOutputStream(
                     getExpectedBitmapSize(icon) + 3 + resName.length());
             try {
@@ -200,30 +205,46 @@
     public static class ThemeData {
 
         final Resources mResources;
-        final int mIconID;
+        final int mResID;
 
-        public ThemeData(Resources resources, int iconID) {
+        public ThemeData(Resources resources, int resID) {
             mResources = resources;
-            mIconID = iconID;
+            mResID = resID;
         }
 
         Drawable loadMonochromeDrawable(int accentColor) {
-            Drawable d = mResources.getDrawable(mIconID).mutate();
+            Drawable d = mResources.getDrawable(mResID).mutate();
             d.setTint(accentColor);
-            d = new InsetDrawable(d, .15f);
+            d = new InsetDrawable(d, .2f);
             return d;
         }
 
-        public Drawable wrapDrawable(Drawable original) {
-            return (original instanceof AdaptiveIconDrawable)
-                    ? new ThemedAdaptiveIcon((AdaptiveIconDrawable) original, this)
-                    : original;
+        public Drawable wrapDrawable(Drawable original, int iconType) {
+            if (!(original instanceof AdaptiveIconDrawable)) {
+                return original;
+            }
+            AdaptiveIconDrawable aid = (AdaptiveIconDrawable) original;
+            String resourceType = mResources.getResourceTypeName(mResID);
+            if (iconType == ICON_TYPE_CALENDAR && "array".equals(resourceType)) {
+                TypedArray ta = mResources.obtainTypedArray(mResID);
+                int id = ta.getResourceId(IconProvider.getDay(), ID_NULL);
+                ta.recycle();
+                return id == ID_NULL ? original
+                        : new ThemedAdaptiveIcon(aid, new ThemeData(mResources, id));
+            } else if (iconType == ICON_TYPE_CLOCK && "array".equals(resourceType)) {
+                ((ClockDrawableWrapper) original).mThemeData = this;
+                return original;
+            } else if ("drawable".equals(resourceType)) {
+                return new ThemedAdaptiveIcon(aid, this);
+            } else {
+                return original;
+            }
         }
     }
 
-    public static class ThemedAdaptiveIcon extends AdaptiveIconDrawable implements Extender {
+    static class ThemedAdaptiveIcon extends AdaptiveIconDrawable implements Extender {
 
-        private final ThemeData mThemeData;
+        protected final ThemeData mThemeData;
 
         public ThemedAdaptiveIcon(AdaptiveIconDrawable parent, ThemeData themeData) {
             super(parent.getBackground(), parent.getForeground());
@@ -243,9 +264,7 @@
             draw(canvas);
         }
 
-        /**
-         * Returns a new icon with theme applied
-         */
+        @Override
         public Drawable getThemedDrawable(Context context) {
             int[] colors = getColors(context);
             Drawable bg = new ColorDrawable(colors[0]);