Adding support for loading system themed icons

Bug: 183641907
Test: Manual
Change-Id: Ic921f46659545316b5a1ead0b3ec5bb758f3fc37
diff --git a/iconloaderlib/Android.bp b/iconloaderlib/Android.bp
index 515aab6..4f4ea0c 100644
--- a/iconloaderlib/Android.bp
+++ b/iconloaderlib/Android.bp
@@ -18,7 +18,7 @@
 
 android_library {
     name: "iconloader_base",
-    sdk_version: "28",
+    sdk_version: "current",
     min_sdk_version: "21",
     static_libs: [
         "androidx.core_core",
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
index 844698c..7265cb9 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
@@ -59,6 +59,7 @@
 
     private Drawable mWrapperIcon;
     private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
+    private Bitmap mUserBadgeBitmap;
 
     private final Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
     private static final float PLACEHOLDER_TEXT_SIZE = 20f;
@@ -256,10 +257,27 @@
         }
         int color = extractColor(bitmap);
         return icon instanceof BitmapInfo.Extender
-                ? ((BitmapInfo.Extender) icon).getExtendedInfo(bitmap, color, this)
+                ? ((BitmapInfo.Extender) icon).getExtendedInfo(bitmap, color, this, scale[0], user)
                 : BitmapInfo.of(bitmap, color);
     }
 
+    public Bitmap getUserBadgeBitmap(UserHandle user) {
+        if (mUserBadgeBitmap == null) {
+            Bitmap bitmap = Bitmap.createBitmap(
+                    mIconBitmapSize, mIconBitmapSize, Bitmap.Config.ARGB_8888);
+            Drawable badgedDrawable = mPm.getUserBadgedIcon(
+                    new FixedSizeBitmapDrawable(bitmap), user);
+            if (badgedDrawable instanceof BitmapDrawable) {
+                mUserBadgeBitmap = ((BitmapDrawable) badgedDrawable).getBitmap();
+            } else {
+                badgedDrawable.setBounds(0, 0, mIconBitmapSize, mIconBitmapSize);
+                mUserBadgeBitmap = BitmapRenderer.createSoftwareBitmap(
+                        mIconBitmapSize, mIconBitmapSize, badgedDrawable::draw);
+            }
+        }
+        return mUserBadgeBitmap;
+    }
+
     public Bitmap createScaledBitmapWithoutShadow(Drawable icon, boolean shrinkNonAdaptiveIcons) {
         RectF iconBounds = new RectF();
         float[] scale = new float[1];
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
index 2e20e8b..845a8ec 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
@@ -15,21 +15,36 @@
  */
 package com.android.launcher3.icons;
 
+import static com.android.launcher3.icons.GraphicsUtils.getExpectedBitmapSize;
+
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
 import android.os.Build;
+import android.os.UserHandle;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.icons.ThemedIconDrawable.ThemedBitmapInfo;
+import com.android.launcher3.icons.cache.BaseIconCache;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
 public class BitmapInfo {
 
     public static final Bitmap LOW_RES_ICON = Bitmap.createBitmap(1, 1, Config.ALPHA_8);
     public static final BitmapInfo LOW_RES_INFO = fromBitmap(LOW_RES_ICON);
 
+    public static final String TAG = "BitmapInfo";
+
+    protected static final byte TYPE_DEFAULT = 1;
+    protected static final byte TYPE_THEMED = 2;
+
     public final Bitmap icon;
     public final int color;
 
@@ -54,7 +69,27 @@
      */
     @Nullable
     public byte[] toByteArray() {
-        return isNullOrLowRes() ? null : GraphicsUtils.flattenBitmap(icon);
+        if (isNullOrLowRes()) {
+            return null;
+        }
+        ByteArrayOutputStream out = new ByteArrayOutputStream(getExpectedBitmapSize(icon) + 1);
+        try {
+            out.write(TYPE_DEFAULT);
+            icon.compress(Bitmap.CompressFormat.PNG, 100, out);
+            out.flush();
+            out.close();
+            return out.toByteArray();
+        } catch (IOException e) {
+            Log.w(TAG, "Could not write bitmap");
+            return null;
+        }
+    }
+
+    /**
+     * Returns a new icon based on the theme of the context
+     */
+    public FastBitmapDrawable newThemedIcon(Context context) {
+        return newIcon(context);
     }
 
     /**
@@ -72,7 +107,11 @@
      * Returns a BitmapInfo previously serialized using {@link #toByteArray()};
      */
     @NonNull
-    public static BitmapInfo fromByteArray(byte[] data, int color) {
+    public static BitmapInfo fromByteArray(byte[] data, int color, UserHandle user,
+            BaseIconCache iconCache, Context context) {
+        if (data == null) {
+            return null;
+        }
         BitmapFactory.Options decodeOptions;
         if (BitmapRenderer.USE_HARDWARE_BITMAP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             decodeOptions = new BitmapFactory.Options();
@@ -80,9 +119,15 @@
         } else {
             decodeOptions = null;
         }
-        return BitmapInfo.of(
-                BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions),
-                color);
+        if (data[0] == TYPE_DEFAULT) {
+            return BitmapInfo.of(
+                    BitmapFactory.decodeByteArray(data, 1, data.length - 1, decodeOptions),
+                    color);
+        } else if (data[0] == TYPE_THEMED) {
+            return ThemedBitmapInfo.decode(data, color, decodeOptions, user, iconCache, context);
+        } else {
+            return null;
+        }
     }
 
     public static BitmapInfo fromBitmap(@NonNull Bitmap bitmap) {
@@ -101,7 +146,8 @@
         /**
          * Called for creating a custom BitmapInfo
          */
-        default BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory) {
+        default BitmapInfo getExtendedInfo(Bitmap bitmap, int color,
+                BaseIconFactory iconFactory, float normalizationScale, UserHandle user) {
             return BitmapInfo.of(bitmap, color);
         }
 
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java b/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java
index 8581aa6..1a0fcfc 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java
@@ -29,8 +29,11 @@
 import android.os.Bundle;
 import android.os.Process;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.util.Log;
 
+import androidx.annotation.Nullable;
+
 import java.util.Calendar;
 import java.util.concurrent.TimeUnit;
 
@@ -138,15 +141,16 @@
     }
 
     @Override
-    public BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory) {
+    public BitmapInfo getExtendedInfo(Bitmap bitmap, int color,
+            BaseIconFactory iconFactory, float normalizationScale, UserHandle user) {
         iconFactory.disableColorExtraction();
-        float [] scale = new float[1];
         AdaptiveIconDrawable background = new AdaptiveIconDrawable(
                 getBackground().getConstantState().newDrawable(), null);
         BitmapInfo bitmapInfo = iconFactory.createBadgedIconBitmap(background,
-                Process.myUserHandle(), mTargetSdkVersion, false, scale);
+                Process.myUserHandle(), mTargetSdkVersion, false);
 
-        return new ClockBitmapInfo(bitmap, color, scale[0], mAnimationInfo, bitmapInfo.icon);
+        return new ClockBitmapInfo(bitmap, color, normalizationScale,
+                mAnimationInfo, bitmapInfo.icon);
     }
 
     @Override
@@ -232,6 +236,12 @@
             d.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f);
             return d;
         }
+
+        @Nullable
+        @Override
+        public byte[] toByteArray() {
+            return null;
+        }
     }
 
     private static class ClockIconDrawable extends FastBitmapDrawable implements Runnable {
diff --git a/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java
index a3f38e0..cf510a1 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java
@@ -55,7 +55,7 @@
     @Nullable private ColorFilter mColorFilter;
 
     private boolean mIsPressed;
-    private boolean mIsDisabled;
+    protected boolean mIsDisabled;
     float mDisabledAlpha = 1f;
 
     // Animator and properties for the fast bitmap drawable's scale
diff --git a/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
index 5d837fd..17b0016 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
@@ -57,10 +57,7 @@
      * Compresses the bitmap to a byte array for serialization.
      */
     public static byte[] flattenBitmap(Bitmap bitmap) {
-        // Try go guesstimate how much space the icon will take when serialized
-        // to avoid unnecessary allocations/copies during the write (4 bytes per pixel).
-        int size = bitmap.getWidth() * bitmap.getHeight() * 4;
-        ByteArrayOutputStream out = new ByteArrayOutputStream(size);
+        ByteArrayOutputStream out = new ByteArrayOutputStream(getExpectedBitmapSize(bitmap));
         try {
             bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
             out.flush();
@@ -72,6 +69,14 @@
         }
     }
 
+    /**
+     * Try go guesstimate how much space the icon will take when serialized to avoid unnecessary
+     * allocations/copies during the write (4 bytes per pixel).
+     */
+    static int getExpectedBitmapSize(Bitmap bitmap) {
+        return bitmap.getWidth() * bitmap.getHeight() * 4;
+    }
+
     public static int getArea(Region r) {
         RegionIterator itr = new RegionIterator(r);
         int area = 0;
diff --git a/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java b/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
index dd6e63d..cc463ba 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.icons;
 
+import static android.content.res.Resources.ID_NULL;
+
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -26,6 +28,7 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.Handler;
@@ -33,11 +36,14 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.Log;
 
-import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.icons.ThemedIconDrawable.ThemeData;
 import com.android.launcher3.util.SafeCloseable;
 
+import org.xmlpull.v1.XmlPullParser;
+
 import java.util.Calendar;
 import java.util.function.BiConsumer;
 import java.util.function.Supplier;
@@ -47,24 +53,36 @@
  */
 public class IconProvider {
 
+    private static final String TAG_ICON = "icon";
+    private static final String ATTR_PACKAGE = "package";
+    private static final String ATTR_DRAWABLE = "drawable";
+
     private static final String TAG = "IconProvider";
     private static final boolean DEBUG = false;
 
     private static final String ICON_METADATA_KEY_PREFIX = ".dynamic_icons";
 
     private static final String SYSTEM_STATE_SEPARATOR = " ";
+    private static final String THEMED_ICON_MAP_FILE = "grayscale_icon_map";
 
-    // Default value returned if there are problems getting resources.
-    private static final int NO_ID = 0;
+    private ArrayMap<String, ThemeData> mThemedIconMap;
 
     private final Context mContext;
     private final ComponentName mCalendar;
     private final ComponentName mClock;
 
     public IconProvider(Context context) {
+        this(context, false);
+    }
+
+    public IconProvider(Context context, boolean supportsIconTheme) {
         mContext = context;
         mCalendar = parseComponentOrNull(context, R.string.calendar_component_name);
         mClock = parseComponentOrNull(context, R.string.clock_component_name);
+        if (!supportsIconTheme) {
+            // Initialize an empty map if theming is not supported
+            mThemedIconMap = new ArrayMap<>();
+        }
     }
 
     /**
@@ -115,6 +133,7 @@
     private Drawable getIconWithOverrides(String packageName, UserHandle user, int iconDpi,
             Supplier<Drawable> fallback) {
         Drawable icon = null;
+
         if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
             icon = loadCalendarDrawable(iconDpi);
         } else if (mClock != null
@@ -122,9 +141,11 @@
                 && Process.myUserHandle().equals(user)) {
             icon = loadClockDrawable(iconDpi);
         }
-        return icon == null ? fallback.get() : icon;
-    }
+        icon = icon == null ? fallback.get() : icon;
 
+        ThemeData td = getThemedIconMap().get(packageName);
+        return td != null ? td.wrapDrawable(icon) : icon;
+    }
 
     private Drawable loadActivityInfoIcon(ActivityInfo ai, int density) {
         final int iconRes = ai.getIconResource();
@@ -144,6 +165,44 @@
         return icon;
     }
 
+    private ArrayMap<String, ThemeData> getThemedIconMap() {
+        if (mThemedIconMap != null) {
+            return mThemedIconMap;
+        }
+        ArrayMap<String, ThemeData> map = new ArrayMap<>();
+        try {
+            Resources res = mContext.getResources();
+            int resID = res.getIdentifier(THEMED_ICON_MAP_FILE, "xml", mContext.getPackageName());
+            if (resID != 0) {
+                XmlResourceParser parser = res.getXml(resID);
+                final int depth = parser.getDepth();
+
+                int type;
+
+                while ((type = parser.next()) != XmlPullParser.START_TAG
+                        && type != XmlPullParser.END_DOCUMENT);
+
+                while (((type = parser.next()) != XmlPullParser.END_TAG ||
+                        parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+                    if (type != XmlPullParser.START_TAG) {
+                        continue;
+                    }
+                    if (TAG_ICON.equals(parser.getName())) {
+                        String pkg = parser.getAttributeValue(null, ATTR_PACKAGE);
+                        int iconId = parser.getAttributeResourceValue(null, ATTR_DRAWABLE, 0);
+                        if (iconId != 0 && !TextUtils.isEmpty(pkg)) {
+                            map.put(pkg, new ThemeData(res, iconId));
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to parse icon map", e);
+        }
+        mThemedIconMap = map;
+        return mThemedIconMap;
+    }
+
     private Drawable loadCalendarDrawable(int iconDpi) {
         PackageManager pm = mContext.getPackageManager();
         try {
@@ -153,7 +212,7 @@
                     .metaData;
             final Resources resources = pm.getResourcesForApplication(mCalendar.getPackageName());
             final int id = getDynamicIconId(metadata, resources);
-            if (id != NO_ID) {
+            if (id != ID_NULL) {
                 if (DEBUG) Log.d(TAG, "Got icon #" + id);
                 return resources.getDrawableForDensity(id, iconDpi, null /* theme */);
             }
@@ -170,11 +229,6 @@
         return ClockDrawableWrapper.forPackage(mContext, mClock.getPackageName(), iconDpi);
     }
 
-    protected boolean isClockIcon(ComponentKey key) {
-        return mClock != null && mClock.equals(key.componentName)
-                && Process.myUserHandle().equals(key.user);
-    }
-
     /**
      * @param metadata metadata of the default activity of Calendar
      * @param resources from the Calendar package
@@ -182,20 +236,20 @@
      */
     private int getDynamicIconId(Bundle metadata, Resources resources) {
         if (metadata == null) {
-            return NO_ID;
+            return ID_NULL;
         }
         String key = mCalendar.getPackageName() + ICON_METADATA_KEY_PREFIX;
-        final int arrayId = metadata.getInt(key, NO_ID);
-        if (arrayId == NO_ID) {
-            return NO_ID;
+        final int arrayId = metadata.getInt(key, ID_NULL);
+        if (arrayId == ID_NULL) {
+            return ID_NULL;
         }
         try {
-            return resources.obtainTypedArray(arrayId).getResourceId(getDay(), NO_ID);
+            return resources.obtainTypedArray(arrayId).getResourceId(getDay(), ID_NULL);
         } catch (Resources.NotFoundException e) {
             if (DEBUG) {
                 Log.d(TAG, "package defines '" + key + "' but corresponding array not found");
             }
-            return NO_ID;
+            return ID_NULL;
         }
     }
 
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java
new file mode 100644
index 0000000..efbabc7
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.icons;
+
+import static android.content.res.Configuration.UI_MODE_NIGHT_MASK;
+import static android.content.res.Configuration.UI_MODE_NIGHT_YES;
+import static android.content.res.Resources.ID_NULL;
+
+import static com.android.launcher3.icons.GraphicsUtils.getExpectedBitmapSize;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.os.Process;
+import android.os.UserHandle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.icons.BitmapInfo.Extender;
+import com.android.launcher3.icons.cache.BaseIconCache;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+/**
+ * Class to handle monochrome themed app icons
+ */
+@SuppressWarnings("NewApi")
+public class ThemedIconDrawable extends FastBitmapDrawable {
+
+    public static final String TAG = "ThemedIconDrawable";
+
+    final ThemedBitmapInfo bitmapInfo;
+    final int colorFg, colorBg;
+
+    // The foreground/monochrome icon for the app
+    private final Drawable mMonochromeIcon;
+    private final AdaptiveIconDrawable mBgWrapper;
+    private final Rect mBadgeBounds;
+
+    protected ThemedIconDrawable(ThemedConstantState constantState) {
+        super(constantState.mBitmap, constantState.mIconColor, constantState.mIsDisabled);
+        bitmapInfo = constantState.bitmapInfo;
+        colorBg = constantState.colorBg;
+        colorFg = constantState.colorFg;
+
+        mMonochromeIcon = bitmapInfo.mThemeData.loadMonochromeDrawable(colorFg);
+        mBgWrapper = new AdaptiveIconDrawable(new ColorDrawable(colorBg), null);
+        mBadgeBounds = bitmapInfo.mUserBadge == null ? null :
+                new Rect(0, 0, bitmapInfo.mUserBadge.getWidth(), bitmapInfo.mUserBadge.getHeight());
+    }
+
+    @Override
+    protected void onBoundsChange(Rect bounds) {
+        super.onBoundsChange(bounds);
+        mBgWrapper.setBounds(bounds);
+        mMonochromeIcon.setBounds(bounds);
+    }
+
+    @Override
+    protected void drawInternal(Canvas canvas, Rect bounds) {
+        int count = canvas.save();
+        canvas.scale(bitmapInfo.mNormalizationScale, bitmapInfo.mNormalizationScale,
+                bounds.exactCenterX(), bounds.exactCenterY());
+        mPaint.setColor(colorBg);
+        canvas.drawPath(mBgWrapper.getIconMask(), mPaint);
+        mMonochromeIcon.draw(canvas);
+        canvas.restoreToCount(count);
+        if (mBadgeBounds != null) {
+            canvas.drawBitmap(bitmapInfo.mUserBadge, mBadgeBounds, getBounds(), mPaint);
+        }
+    }
+
+    @Override
+    public ConstantState getConstantState() {
+        return new ThemedConstantState(bitmapInfo, colorBg, colorFg, mIsDisabled);
+    }
+
+    static class ThemedConstantState extends FastBitmapConstantState {
+
+        final ThemedBitmapInfo bitmapInfo;
+        final int colorFg, colorBg;
+
+        public ThemedConstantState(ThemedBitmapInfo bitmapInfo,
+                int colorBg, int colorFg, boolean isDisabled) {
+            super(bitmapInfo.icon, bitmapInfo.color, isDisabled);
+            this.bitmapInfo = bitmapInfo;
+            this.colorBg = colorBg;
+            this.colorFg = colorFg;
+        }
+
+        @Override
+        public FastBitmapDrawable newDrawable() {
+            return new ThemedIconDrawable(this);
+        }
+    }
+
+    public static class ThemedBitmapInfo extends BitmapInfo {
+
+        final ThemeData mThemeData;
+        final float mNormalizationScale;
+        final Bitmap mUserBadge;
+
+        public ThemedBitmapInfo(Bitmap icon, int color, ThemeData themeData,
+                float normalizationScale, Bitmap userBadge) {
+            super(icon, color);
+            mThemeData = themeData;
+            mNormalizationScale = normalizationScale;
+            mUserBadge = userBadge;
+        }
+
+        @Override
+        public FastBitmapDrawable newThemedIcon(Context context) {
+            int[] colors = getColors(context);
+            FastBitmapDrawable drawable = new ThemedConstantState(this, colors[0], colors[1], false)
+                    .newDrawable();
+            drawable.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f);
+            return drawable;
+        }
+
+        @Nullable
+        public byte[] toByteArray() {
+            if (isNullOrLowRes()) {
+                return null;
+            }
+            String resName = mThemeData.mResources.getResourceName(mThemeData.mIconID);
+            ByteArrayOutputStream out = new ByteArrayOutputStream(
+                    getExpectedBitmapSize(icon) + 3 + resName.length());
+            try {
+                DataOutputStream dos = new DataOutputStream(out);
+                dos.writeByte(TYPE_THEMED);
+                dos.writeFloat(mNormalizationScale);
+                dos.writeUTF(resName);
+                icon.compress(Bitmap.CompressFormat.PNG, 100, dos);
+
+                dos.flush();
+                dos.close();
+                return out.toByteArray();
+            } catch (IOException e) {
+                Log.w(TAG, "Could not write bitmap");
+                return null;
+            }
+        }
+
+        static ThemedBitmapInfo decode(byte[] data, int color,
+                BitmapFactory.Options decodeOptions, UserHandle user, BaseIconCache iconCache,
+                Context context) {
+            try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data))) {
+                dis.readByte(); // type
+                float normalizationScale = dis.readFloat();
+
+                String resName = dis.readUTF();
+                int resId = context.getResources()
+                        .getIdentifier(resName, "drawable", context.getPackageName());
+                if (resId == ID_NULL) {
+                    return null;
+                }
+
+                Bitmap userBadgeBitmap = null;
+                if (!Process.myUserHandle().equals(user)) {
+                    try (BaseIconFactory iconFactory = iconCache.getIconFactory()) {
+                        userBadgeBitmap = iconFactory.getUserBadgeBitmap(user);
+                    }
+                }
+
+                ThemeData themeData = new ThemeData(context.getResources(), resId);
+                Bitmap icon = BitmapFactory.decodeStream(dis, null, decodeOptions);
+                return new ThemedBitmapInfo(icon, color, themeData, normalizationScale,
+                        userBadgeBitmap);
+            } catch (IOException e) {
+                return null;
+            }
+        }
+    }
+
+    public static class ThemeData {
+
+        final Resources mResources;
+        final int mIconID;
+
+        public ThemeData(Resources resources, int iconID) {
+            mResources = resources;
+            mIconID = iconID;
+        }
+
+        Drawable loadMonochromeDrawable(int accentColor) {
+            Drawable d = mResources.getDrawable(mIconID).mutate();
+            d.setTint(accentColor);
+            d = new InsetDrawable(d, .15f);
+            return d;
+        }
+
+        public Drawable wrapDrawable(Drawable original) {
+            return (original instanceof AdaptiveIconDrawable)
+                    ? new ThemedAdaptiveIcon((AdaptiveIconDrawable) original, this)
+                    : original;
+        }
+    }
+
+    public static class ThemedAdaptiveIcon extends AdaptiveIconDrawable implements Extender {
+
+        private final ThemeData mThemeData;
+
+        public ThemedAdaptiveIcon(AdaptiveIconDrawable parent, ThemeData themeData) {
+            super(parent.getBackground(), parent.getForeground());
+            mThemeData = themeData;
+        }
+
+        @Override
+        public BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory,
+                float normalizationScale, UserHandle user) {
+            Bitmap userBadge = Process.myUserHandle().equals(user)
+                    ? null : iconFactory.getUserBadgeBitmap(user);
+            return new ThemedBitmapInfo(bitmap, color, mThemeData, normalizationScale, userBadge);
+        }
+
+        @Override
+        public void drawForPersistence(Canvas canvas) {
+            draw(canvas);
+        }
+
+        /**
+         * Returns a new icon with theme applied
+         */
+        public Drawable getThemedDrawable(Context context) {
+            int[] colors = getColors(context);
+            Drawable bg = new ColorDrawable(colors[0]);
+            float inset = getExtraInsetFraction() / (1 + 2 * getExtraInsetFraction());
+            Drawable fg = new InsetDrawable(mThemeData.loadMonochromeDrawable(colors[1]), inset);
+            return new AdaptiveIconDrawable(bg, fg);
+        }
+    }
+
+    private static int[] getColors(Context context) {
+        Resources res = context.getResources();
+        int[] colors = new int[2];
+        if ((res.getConfiguration().uiMode & UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES) {
+            colors[0] = res.getColor(android.R.color.system_neutral1_800);
+            colors[1] = res.getColor(android.R.color.system_neutral2_200);
+        } else {
+            colors[0] = res.getColor(android.R.color.system_accent1_100);
+            colors[1] = res.getColor(android.R.color.system_neutral2_700);
+        }
+        return colors;
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
index 863e8c8..89dfa18 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
@@ -137,7 +137,7 @@
     /**
      * Opens and returns an icon factory. The factory is recycled by the caller.
      */
-    protected abstract BaseIconFactory getIconFactory();
+    public abstract BaseIconFactory getIconFactory();
 
     public void updateIconParams(int iconDpi, int iconPixelSize) {
         mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize));
@@ -479,14 +479,14 @@
                 }
 
                 if (!lowRes) {
-                    byte[] data = c.getBlob(2);
                     try {
-                        entry.bitmap = BitmapInfo.fromByteArray(data, entry.bitmap.color);
+                        entry.bitmap = BitmapInfo.fromByteArray(
+                                c.getBlob(2), entry.bitmap.color, cacheKey.user, this, mContext);
                     } catch (Exception e) {
                         return false;
                     }
                 }
-                return true;
+                return entry.bitmap != null;
             }
         } catch (SQLiteException e) {
             Log.d(TAG, "Error reading icon cache", e);
diff --git a/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java b/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java
index 1337975..cc4ad7b 100644
--- a/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java
+++ b/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java
@@ -88,7 +88,7 @@
     }
 
     @Override
-    protected BaseIconFactory getIconFactory() {
+    public BaseIconFactory getIconFactory() {
         return IconFactory.obtain(mContext);
     }