Moving some common functionality to iconlib

Bug: 183641907
Test: Manual
Change-Id: Id58171bf7cf19d56a702673f3d1f4a6647e6e5d1
diff --git a/iconloaderlib/build.gradle b/iconloaderlib/build.gradle
index d7a62e1..8410275 100644
--- a/iconloaderlib/build.gradle
+++ b/iconloaderlib/build.gradle
@@ -5,7 +5,7 @@
     buildToolsVersion BUILD_TOOLS_VERSION
 
     defaultConfig {
-        minSdkVersion 25
+        minSdkVersion 26
         targetSdkVersion 28
         versionCode 1
         versionName "1.0"
diff --git a/iconloaderlib/res/values/attrs.xml b/iconloaderlib/res/values/attrs.xml
new file mode 100644
index 0000000..8f0bd2c
--- /dev/null
+++ b/iconloaderlib/res/values/attrs.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+<resources>
+    <attr name="disabledIconAlpha" format="float" />
+    <attr name="loadingIconColor" format="color" />
+
+</resources>
\ No newline at end of file
diff --git a/iconloaderlib/res/values/config.xml b/iconloaderlib/res/values/config.xml
index 68c2d2e..893f955 100644
--- a/iconloaderlib/res/values/config.xml
+++ b/iconloaderlib/res/values/config.xml
@@ -24,4 +24,7 @@
     <bool name="simple_cache_enable_im_memory">false</bool>
     <string name="cache_db_name" translatable="false">app_icons.db</string>
 
+    <string name="calendar_component_name" translatable="false"></string>
+    <string name="clock_component_name" translatable="false"></string>
+
 </resources>
\ No newline at end of file
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
index d33f9b1..694343b 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
@@ -15,6 +15,7 @@
  */
 package com.android.launcher3.icons;
 
+import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap.Config;
 
@@ -44,6 +45,17 @@
         return LOW_RES_ICON == icon;
     }
 
+    /**
+     * Creates a drawable for the provided BitmapInfo
+     */
+    public FastBitmapDrawable newIcon(Context context) {
+        FastBitmapDrawable drawable = isLowRes()
+                ? new PlaceHolderIconDrawable(this, context)
+                : new FastBitmapDrawable(this);
+        drawable.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f);
+        return drawable;
+    }
+
     public static BitmapInfo fromBitmap(@NonNull Bitmap bitmap) {
         return of(bitmap, 0);
     }
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java b/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java
new file mode 100644
index 0000000..ed9d89c
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2019 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 android.annotation.TargetApi;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.util.Calendar;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Wrapper over {@link AdaptiveIconDrawable} to intercept icon flattening logic for dynamic
+ * clock icons
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public class ClockDrawableWrapper extends AdaptiveIconDrawable implements BitmapInfo.Extender {
+
+    private static final String TAG = "ClockDrawableWrapper";
+
+    private static final boolean DISABLE_SECONDS = true;
+
+    // Time after which the clock icon should check for an update. The actual invalidate
+    // will only happen in case of any change.
+    public static final long TICK_MS = DISABLE_SECONDS ? TimeUnit.MINUTES.toMillis(1) : 200L;
+
+    private static final String LAUNCHER_PACKAGE = "com.android.launcher3";
+    private static final String ROUND_ICON_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".LEVEL_PER_TICK_ICON_ROUND";
+    private static final String HOUR_INDEX_METADATA_KEY = LAUNCHER_PACKAGE + ".HOUR_LAYER_INDEX";
+    private static final String MINUTE_INDEX_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".MINUTE_LAYER_INDEX";
+    private static final String SECOND_INDEX_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".SECOND_LAYER_INDEX";
+    private static final String DEFAULT_HOUR_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".DEFAULT_HOUR";
+    private static final String DEFAULT_MINUTE_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".DEFAULT_MINUTE";
+    private static final String DEFAULT_SECOND_METADATA_KEY = LAUNCHER_PACKAGE
+            + ".DEFAULT_SECOND";
+
+    /* Number of levels to jump per second for the second hand */
+    private static final int LEVELS_PER_SECOND = 10;
+
+    public static final int INVALID_VALUE = -1;
+
+    private final AnimationInfo mAnimationInfo = new AnimationInfo();
+    private int mTargetSdkVersion;
+
+    public ClockDrawableWrapper(AdaptiveIconDrawable base) {
+        super(base.getBackground(), base.getForeground());
+    }
+
+    /**
+     * Loads and returns the wrapper from the provided package, or returns null
+     * if it is unable to load.
+     */
+    public static ClockDrawableWrapper forPackage(Context context, String pkg, int iconDpi) {
+        try {
+            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;
+            }
+            return wrapper;
+        } catch (Exception e) {
+            Log.d(TAG, "Unable to load clock drawable info", e);
+        }
+        return null;
+    }
+
+    @Override
+    public BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory) {
+        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);
+
+        return new ClockBitmapInfo(bitmap, color, scale[0], mAnimationInfo, bitmapInfo.icon);
+    }
+
+    @Override
+    public void prepareToDrawOnUi() {
+        mAnimationInfo.applyTime(Calendar.getInstance(), (LayerDrawable) getForeground());
+    }
+
+    private static class AnimationInfo {
+
+        public ConstantState baseDrawableState;
+
+        public int hourLayerIndex;
+        public int minuteLayerIndex;
+        public int secondLayerIndex;
+        public int defaultHour;
+        public int defaultMinute;
+        public int defaultSecond;
+
+        boolean applyTime(Calendar time, LayerDrawable foregroundDrawable) {
+            time.setTimeInMillis(System.currentTimeMillis());
+
+            // We need to rotate by the difference from the default time if one is specified.
+            int convertedHour = (time.get(Calendar.HOUR) + (12 - defaultHour)) % 12;
+            int convertedMinute = (time.get(Calendar.MINUTE) + (60 - defaultMinute)) % 60;
+            int convertedSecond = (time.get(Calendar.SECOND) + (60 - defaultSecond)) % 60;
+
+            boolean invalidate = false;
+            if (hourLayerIndex != INVALID_VALUE) {
+                final Drawable hour = foregroundDrawable.getDrawable(hourLayerIndex);
+                if (hour.setLevel(convertedHour * 60 + time.get(Calendar.MINUTE))) {
+                    invalidate = true;
+                }
+            }
+
+            if (minuteLayerIndex != INVALID_VALUE) {
+                final Drawable minute = foregroundDrawable.getDrawable(minuteLayerIndex);
+                if (minute.setLevel(time.get(Calendar.HOUR) * 60 + convertedMinute)) {
+                    invalidate = true;
+                }
+            }
+
+            if (secondLayerIndex != INVALID_VALUE) {
+                final Drawable second = foregroundDrawable.getDrawable(secondLayerIndex);
+                if (second.setLevel(convertedSecond * LEVELS_PER_SECOND)) {
+                    invalidate = true;
+                }
+            }
+
+            return invalidate;
+        }
+    }
+
+    private static class ClockBitmapInfo extends BitmapInfo {
+
+        public final float scale;
+        public final int offset;
+        public final AnimationInfo animInfo;
+        public final Bitmap mFlattenedBackground;
+
+        ClockBitmapInfo(Bitmap icon, int color, float scale, AnimationInfo animInfo,
+                Bitmap background) {
+            super(icon, color);
+            this.scale = scale;
+            this.animInfo = animInfo;
+            this.offset = (int) Math.ceil(ShadowGenerator.BLUR_FACTOR * icon.getWidth());
+            this.mFlattenedBackground = background;
+        }
+
+        @Override
+        public FastBitmapDrawable newIcon(Context context) {
+            ClockIconDrawable d = new ClockIconDrawable(this);
+            d.mDisabledAlpha = GraphicsUtils.getFloat(context, R.attr.disabledIconAlpha, 1f);
+            return d;
+        }
+    }
+
+    private static class ClockIconDrawable extends FastBitmapDrawable implements Runnable {
+
+        private final Calendar mTime = Calendar.getInstance();
+
+        private final ClockBitmapInfo mInfo;
+
+        private final AdaptiveIconDrawable mFullDrawable;
+        private final LayerDrawable mForeground;
+
+        ClockIconDrawable(ClockBitmapInfo clockInfo) {
+            super(clockInfo);
+
+            mInfo = clockInfo;
+
+            mFullDrawable = (AdaptiveIconDrawable) mInfo.animInfo.baseDrawableState.newDrawable();
+            mForeground = (LayerDrawable) mFullDrawable.getForeground();
+        }
+
+        @Override
+        protected void onBoundsChange(Rect bounds) {
+            super.onBoundsChange(bounds);
+            mFullDrawable.setBounds(bounds);
+        }
+
+        @Override
+        public void drawInternal(Canvas canvas, Rect bounds) {
+            if (mInfo == null) {
+                super.drawInternal(canvas, bounds);
+                return;
+            }
+            // draw the background that is already flattened to a bitmap
+            canvas.drawBitmap(mInfo.mFlattenedBackground, null, bounds, mPaint);
+
+            // prepare and draw the foreground
+            mInfo.animInfo.applyTime(mTime, mForeground);
+
+            canvas.scale(mInfo.scale, mInfo.scale,
+                    bounds.exactCenterX() + mInfo.offset, bounds.exactCenterY() + mInfo.offset);
+            canvas.clipPath(mFullDrawable.getIconMask());
+            mForeground.draw(canvas);
+
+            reschedule();
+        }
+
+        @Override
+        protected void updateFilter() {
+            super.updateFilter();
+            mFullDrawable.setColorFilter(mPaint.getColorFilter());
+        }
+
+        @Override
+        public void run() {
+            if (mInfo.animInfo.applyTime(mTime, mForeground)) {
+                invalidateSelf();
+            } else {
+                reschedule();
+            }
+        }
+
+        @Override
+        public boolean setVisible(boolean visible, boolean restart) {
+            boolean result = super.setVisible(visible, restart);
+            if (visible) {
+                reschedule();
+            } else {
+                unscheduleSelf(this);
+            }
+            return result;
+        }
+
+        private void reschedule() {
+            if (!isVisible()) {
+                return;
+            }
+
+            unscheduleSelf(this);
+            final long upTime = SystemClock.uptimeMillis();
+            final long step = TICK_MS; /* tick every 200 ms */
+            scheduleSelf(this, upTime - ((upTime % step)) + step);
+        }
+
+        @Override
+        public ConstantState getConstantState() {
+            return new ClockConstantState(mInfo, isDisabled());
+        }
+
+        private static class ClockConstantState extends FastBitmapConstantState {
+
+            private final ClockBitmapInfo mInfo;
+
+            ClockConstantState(ClockBitmapInfo info, boolean isDisabled) {
+                super(info.icon, info.color, isDisabled);
+                mInfo = info;
+            }
+
+            @Override
+            public FastBitmapDrawable newDrawable() {
+                ClockIconDrawable drawable = new ClockIconDrawable(mInfo);
+                drawable.setIsDisabled(mIsDisabled);
+                return drawable;
+            }
+        }
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java
new file mode 100644
index 0000000..ec930f8
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/FastBitmapDrawable.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2008 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 android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.Property;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.Nullable;
+
+
+public class FastBitmapDrawable extends Drawable {
+
+    private static final Interpolator ACCEL = new AccelerateInterpolator();
+    private static final Interpolator DEACCEL = new DecelerateInterpolator();
+
+    private static final float PRESSED_SCALE = 1.1f;
+
+    private static final float DISABLED_DESATURATION = 1f;
+    private static final float DISABLED_BRIGHTNESS = 0.5f;
+
+    public static final int CLICK_FEEDBACK_DURATION = 200;
+
+    private static ColorFilter sDisabledFColorFilter;
+
+    protected final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
+    protected Bitmap mBitmap;
+    protected final int mIconColor;
+
+    @Nullable private ColorFilter mColorFilter;
+
+    private boolean mIsPressed;
+    private boolean mIsDisabled;
+    float mDisabledAlpha = 1f;
+    private float mRoundedCornersRadius = 0f;
+    private final Path mClipPath = new Path();
+
+    // Animator and properties for the fast bitmap drawable's scale
+    private static final Property<FastBitmapDrawable, Float> SCALE
+            = new Property<FastBitmapDrawable, Float>(Float.TYPE, "scale") {
+        @Override
+        public Float get(FastBitmapDrawable fastBitmapDrawable) {
+            return fastBitmapDrawable.mScale;
+        }
+
+        @Override
+        public void set(FastBitmapDrawable fastBitmapDrawable, Float value) {
+            fastBitmapDrawable.mScale = value;
+            fastBitmapDrawable.invalidateSelf();
+        }
+    };
+    private ObjectAnimator mScaleAnimation;
+    private float mScale = 1;
+
+    private int mAlpha = 255;
+
+    public FastBitmapDrawable(Bitmap b) {
+        this(b, Color.TRANSPARENT);
+    }
+
+    public FastBitmapDrawable(BitmapInfo info) {
+        this(info.icon, info.color);
+    }
+
+    protected FastBitmapDrawable(Bitmap b, int iconColor) {
+        this(b, iconColor, false);
+    }
+
+    protected FastBitmapDrawable(Bitmap b, int iconColor, boolean isDisabled) {
+        mBitmap = b;
+        mIconColor = iconColor;
+        setFilterBitmap(true);
+        setIsDisabled(isDisabled);
+    }
+
+    @Override
+    public final void draw(Canvas canvas) {
+        if (mRoundedCornersRadius > 0) {
+            float radius = mRoundedCornersRadius * mScale;
+            mClipPath.reset();
+            mClipPath.addRoundRect(0, 0, getIntrinsicWidth(), getIntrinsicHeight(),
+                    radius, radius, Path.Direction.CCW);
+            canvas.clipPath(mClipPath);
+        }
+        if (mScale != 1f) {
+            int count = canvas.save();
+            Rect bounds = getBounds();
+            canvas.scale(mScale, mScale, bounds.exactCenterX(), bounds.exactCenterY());
+            drawInternal(canvas, bounds);
+            canvas.restoreToCount(count);
+        } else {
+            drawInternal(canvas, getBounds());
+        }
+    }
+
+    protected void drawInternal(Canvas canvas, Rect bounds) {
+        canvas.drawBitmap(mBitmap, null, bounds, mPaint);
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter cf) {
+        mColorFilter = cf;
+        updateFilter();
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        if (mAlpha != alpha) {
+            mAlpha = alpha;
+            mPaint.setAlpha(alpha);
+            invalidateSelf();
+        }
+    }
+
+    @Override
+    public void setFilterBitmap(boolean filterBitmap) {
+        mPaint.setFilterBitmap(filterBitmap);
+        mPaint.setAntiAlias(filterBitmap);
+    }
+
+    @Override
+    public int getAlpha() {
+        return mAlpha;
+    }
+
+    public void resetScale() {
+        if (mScaleAnimation != null) {
+            mScaleAnimation.cancel();
+            mScaleAnimation = null;
+        }
+        mScale = 1;
+        invalidateSelf();
+    }
+
+    public float getAnimatedScale() {
+        return mScaleAnimation == null ? 1 : mScale;
+    }
+
+    public void setRoundedCornersRadius(float radius) {
+        mRoundedCornersRadius = radius;
+    }
+
+    public float getRoundedCornersRadius() {
+        return mRoundedCornersRadius;
+    }
+
+    @Override
+    public int getIntrinsicWidth() {
+        return mBitmap.getWidth();
+    }
+
+    @Override
+    public int getIntrinsicHeight() {
+        return mBitmap.getHeight();
+    }
+
+    @Override
+    public int getMinimumWidth() {
+        return getBounds().width();
+    }
+
+    @Override
+    public int getMinimumHeight() {
+        return getBounds().height();
+    }
+
+    @Override
+    public boolean isStateful() {
+        return true;
+    }
+
+    @Override
+    public ColorFilter getColorFilter() {
+        return mPaint.getColorFilter();
+    }
+
+    @Override
+    protected boolean onStateChange(int[] state) {
+        boolean isPressed = false;
+        for (int s : state) {
+            if (s == android.R.attr.state_pressed) {
+                isPressed = true;
+                break;
+            }
+        }
+        if (mIsPressed != isPressed) {
+            mIsPressed = isPressed;
+
+            if (mScaleAnimation != null) {
+                mScaleAnimation.cancel();
+                mScaleAnimation = null;
+            }
+
+            if (mIsPressed) {
+                // Animate when going to pressed state
+                mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, PRESSED_SCALE);
+                mScaleAnimation.setDuration(CLICK_FEEDBACK_DURATION);
+                mScaleAnimation.setInterpolator(ACCEL);
+                mScaleAnimation.start();
+            } else {
+                if (isVisible()) {
+                    mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, 1f);
+                    mScaleAnimation.setDuration(CLICK_FEEDBACK_DURATION);
+                    mScaleAnimation.setInterpolator(DEACCEL);
+                    mScaleAnimation.start();
+                } else {
+                    mScale = 1f;
+                    invalidateSelf();
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    public void setIsDisabled(boolean isDisabled) {
+        if (mIsDisabled != isDisabled) {
+            mIsDisabled = isDisabled;
+            updateFilter();
+        }
+    }
+
+    protected boolean isDisabled() {
+        return mIsDisabled;
+    }
+
+    private ColorFilter getDisabledColorFilter() {
+        if (sDisabledFColorFilter == null) {
+            ColorMatrix tempBrightnessMatrix = new ColorMatrix();
+            ColorMatrix tempFilterMatrix = new ColorMatrix();
+
+            tempFilterMatrix.setSaturation(1f - DISABLED_DESATURATION);
+            float scale = 1 - DISABLED_BRIGHTNESS;
+            int brightnessI =   (int) (255 * DISABLED_BRIGHTNESS);
+            float[] mat = tempBrightnessMatrix.getArray();
+            mat[0] = scale;
+            mat[6] = scale;
+            mat[12] = scale;
+            mat[4] = brightnessI;
+            mat[9] = brightnessI;
+            mat[14] = brightnessI;
+            mat[18] = mDisabledAlpha;
+            tempFilterMatrix.preConcat(tempBrightnessMatrix);
+            sDisabledFColorFilter = new ColorMatrixColorFilter(tempFilterMatrix);
+        }
+        return sDisabledFColorFilter;
+    }
+
+    /**
+     * Updates the paint to reflect the current brightness and saturation.
+     */
+    protected void updateFilter() {
+        mPaint.setColorFilter(mIsDisabled ? getDisabledColorFilter() : mColorFilter);
+        invalidateSelf();
+    }
+
+    @Override
+    public ConstantState getConstantState() {
+        return new FastBitmapConstantState(mBitmap, mIconColor, mIsDisabled);
+    }
+
+    protected static class FastBitmapConstantState extends ConstantState {
+        protected final Bitmap mBitmap;
+        protected final int mIconColor;
+        protected final boolean mIsDisabled;
+
+        public FastBitmapConstantState(Bitmap bitmap, int color, boolean isDisabled) {
+            mBitmap = bitmap;
+            mIconColor = color;
+            mIsDisabled = isDisabled;
+        }
+
+        @Override
+        public FastBitmapDrawable newDrawable() {
+            return new FastBitmapDrawable(mBitmap, mIconColor, mIsDisabled);
+        }
+
+        @Override
+        public int getChangingConfigurations() {
+            return 0;
+        }
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
index 22f1f23..5d837fd 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
@@ -15,10 +15,16 @@
  */
 package com.android.launcher3.icons;
 
+import android.content.Context;
+import android.content.res.TypedArray;
 import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Path;
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.graphics.RegionIterator;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
 import android.util.Log;
 
 import androidx.annotation.ColorInt;
@@ -82,4 +88,35 @@
     public static void noteNewBitmapCreated() {
         sOnNewBitmapRunnable.run();
     }
+
+
+    /**
+     * Returns the default path to be used by an icon
+     */
+    public static Path getShapePath(int size) {
+        AdaptiveIconDrawable drawable = new AdaptiveIconDrawable(
+                new ColorDrawable(Color.BLACK), new ColorDrawable(Color.BLACK));
+        drawable.setBounds(0, 0, size, size);
+        return new Path(drawable.getIconMask());
+    }
+
+    /**
+     * Returns the color associated with the attribute
+     */
+    public static int getAttrColor(Context context, int attr) {
+        TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
+        int colorAccent = ta.getColor(0, 0);
+        ta.recycle();
+        return colorAccent;
+    }
+
+    /**
+     * Returns the alpha corresponding to the theme attribute {@param attr}
+     */
+    public static float getFloat(Context context, int attr, float defValue) {
+        TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
+        float value = ta.getFloat(0, defValue);
+        ta.recycle();
+        return value;
+    }
 }
diff --git a/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java b/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
new file mode 100644
index 0000000..7749308
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2019 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 android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.launcher3.icons.BitmapInfo.Extender;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.SafeCloseable;
+
+import java.util.Calendar;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+
+/**
+ * Class to handle icon loading from different packages
+ */
+public class IconProvider {
+
+    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 = " ";
+
+    // Default value returned if there are problems getting resources.
+    private static final int NO_ID = 0;
+
+    private static final BiFunction<LauncherActivityInfo, Integer, Drawable> LAI_LOADER =
+            LauncherActivityInfo::getIcon;
+
+    private static final BiFunction<ActivityInfo, PackageManager, Drawable> AI_LOADER =
+            ActivityInfo::loadUnbadgedIcon;
+
+
+    private final Context mContext;
+    private final ComponentName mCalendar;
+    private final ComponentName mClock;
+
+    public IconProvider(Context context) {
+        mContext = context;
+        mCalendar = parseComponentOrNull(context, R.string.calendar_component_name);
+        mClock = parseComponentOrNull(context, R.string.clock_component_name);
+    }
+
+    /**
+     * Adds any modification to the provided systemState for dynamic icons. This system state
+     * is used by caches to check for icon invalidation.
+     */
+    public String getSystemStateForPackage(String systemState, String packageName) {
+        if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
+            return systemState + SYSTEM_STATE_SEPARATOR + getDay();
+        } else {
+            return systemState;
+        }
+    }
+
+    /**
+     * Loads the icon for the provided LauncherActivityInfo such that it can be drawn directly
+     * on the UI
+     */
+    public Drawable getIconForUI(LauncherActivityInfo info, int iconDpi) {
+        Drawable icon = getIcon(info, iconDpi);
+        if (icon instanceof BitmapInfo.Extender) {
+            ((Extender) icon).prepareToDrawOnUi();
+        }
+        return icon;
+    }
+
+    /**
+     * Loads the icon for the provided ActivityInfo such that it can be drawn directly
+     * on the UI
+     */
+    public Drawable getIconForUI(ActivityInfo info, UserHandle user) {
+        Drawable icon = getIcon(info, user);
+        if (icon instanceof BitmapInfo.Extender) {
+            ((Extender) icon).prepareToDrawOnUi();
+        }
+        return icon;
+    }
+
+    /**
+     * Loads the icon for the provided LauncherActivityInfo
+     */
+    public Drawable getIcon(LauncherActivityInfo info, int iconDpi) {
+        return getIcon(info.getApplicationInfo().packageName, info.getUser(),
+                info, iconDpi, LAI_LOADER);
+    }
+
+    /**
+     * Loads the icon for the provided activity info
+     */
+    public Drawable getIcon(ActivityInfo info, UserHandle user) {
+        return getIcon(info.applicationInfo.packageName, user, info, mContext.getPackageManager(),
+                AI_LOADER);
+    }
+
+    private <T, P> Drawable getIcon(String packageName, UserHandle user, T obj, P param,
+            BiFunction<T, P, Drawable> loader) {
+        Drawable icon = null;
+        if (mCalendar != null && mCalendar.getPackageName().equals(packageName)) {
+            icon = loadCalendarDrawable(0);
+        } else if (mClock != null
+                && mClock.getPackageName().equals(packageName)
+                && Process.myUserHandle().equals(user)) {
+            icon = loadClockDrawable(0);
+        }
+        return icon == null ? loader.apply(obj, param) : icon;
+    }
+
+    private Drawable loadCalendarDrawable(int iconDpi) {
+        PackageManager pm = mContext.getPackageManager();
+        try {
+            final Bundle metadata = pm.getActivityInfo(
+                    mCalendar,
+                    PackageManager.GET_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA)
+                    .metaData;
+            final Resources resources = pm.getResourcesForApplication(mCalendar.getPackageName());
+            final int id = getDynamicIconId(metadata, resources);
+            if (id != NO_ID) {
+                if (DEBUG) Log.d(TAG, "Got icon #" + id);
+                return resources.getDrawableForDensity(id, iconDpi, null /* theme */);
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            if (DEBUG) {
+                Log.d(TAG, "Could not get activityinfo or resources for package: "
+                        + mCalendar.getPackageName());
+            }
+        }
+        return null;
+    }
+
+    private Drawable loadClockDrawable(int iconDpi) {
+        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
+     * @return the resource id for today's Calendar icon; 0 if resources cannot be found.
+     */
+    private int getDynamicIconId(Bundle metadata, Resources resources) {
+        if (metadata == null) {
+            return NO_ID;
+        }
+        String key = mCalendar.getPackageName() + ICON_METADATA_KEY_PREFIX;
+        final int arrayId = metadata.getInt(key, NO_ID);
+        if (arrayId == NO_ID) {
+            return NO_ID;
+        }
+        try {
+            return resources.obtainTypedArray(arrayId).getResourceId(getDay(), NO_ID);
+        } catch (Resources.NotFoundException e) {
+            if (DEBUG) {
+                Log.d(TAG, "package defines '" + key + "' but corresponding array not found");
+            }
+            return NO_ID;
+        }
+    }
+
+    /**
+     * @return Today's day of the month, zero-indexed.
+     */
+    private int getDay() {
+        return Calendar.getInstance().get(Calendar.DAY_OF_MONTH) - 1;
+    }
+
+
+    /**
+     * Registers a callback to listen for calendar icon changes.
+     * The callback receives the packageName for the calendar icon
+     */
+    public static SafeCloseable registerIconChangeListener(Context context,
+            BiConsumer<String, UserHandle> callback, Handler handler) {
+        ComponentName calendar = parseComponentOrNull(context, R.string.calendar_component_name);
+        ComponentName clock = parseComponentOrNull(context, R.string.clock_component_name);
+
+        if (calendar == null && clock == null) {
+            return () -> { };
+        }
+
+        BroadcastReceiver receiver = new DateTimeChangeReceiver(callback);
+        final IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
+        if (calendar != null) {
+            filter.addAction(Intent.ACTION_TIME_CHANGED);
+            filter.addAction(Intent.ACTION_DATE_CHANGED);
+        }
+        context.registerReceiver(receiver, filter, null, handler);
+
+        return () -> context.unregisterReceiver(receiver);
+    }
+
+    private static class DateTimeChangeReceiver extends BroadcastReceiver {
+
+        private final BiConsumer<String, UserHandle> mCallback;
+
+        DateTimeChangeReceiver(BiConsumer<String, UserHandle> callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
+                ComponentName clock = parseComponentOrNull(context, R.string.clock_component_name);
+                if (clock != null) {
+                    mCallback.accept(clock.getPackageName(), Process.myUserHandle());
+                }
+            }
+
+            ComponentName calendar =
+                    parseComponentOrNull(context, R.string.calendar_component_name);
+            if (calendar != null) {
+                for (UserHandle user
+                        : context.getSystemService(UserManager.class).getUserProfiles()) {
+                    mCallback.accept(calendar.getPackageName(), user);
+                }
+            }
+        }
+    }
+
+    private static ComponentName parseComponentOrNull(Context context, int resId) {
+        String cn = context.getString(resId);
+        return TextUtils.isEmpty(cn) ? null : ComponentName.unflattenFromString(cn);
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java
new file mode 100644
index 0000000..5f3343e
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2018 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 android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import androidx.core.graphics.ColorUtils;
+
+/**
+ * Subclass which draws a placeholder icon when the actual icon is not yet loaded
+ */
+public class PlaceHolderIconDrawable extends FastBitmapDrawable {
+
+    // Path in [0, 100] bounds.
+    private final Path mProgressPath;
+
+    public PlaceHolderIconDrawable(BitmapInfo info, Context context) {
+        super(info);
+
+        mProgressPath = GraphicsUtils.getShapePath(100);
+        mPaint.setColor(ColorUtils.compositeColors(
+                GraphicsUtils.getAttrColor(context, R.attr.loadingIconColor), info.color));
+    }
+
+    @Override
+    protected void drawInternal(Canvas canvas, Rect bounds) {
+        int saveCount = canvas.save();
+        canvas.translate(bounds.left, bounds.top);
+        canvas.scale(bounds.width() / 100f, bounds.height() / 100f);
+        canvas.drawPath(mProgressPath, mPaint);
+        canvas.restoreToCount(saveCount);
+    }
+
+    /** Updates this placeholder to {@code newIcon} with animation. */
+    public void animateIconUpdate(Drawable newIcon) {
+        int placeholderColor = mPaint.getColor();
+        int originalAlpha = Color.alpha(placeholderColor);
+
+        ValueAnimator iconUpdateAnimation = ValueAnimator.ofInt(originalAlpha, 0);
+        iconUpdateAnimation.setDuration(375);
+        iconUpdateAnimation.addUpdateListener(valueAnimator -> {
+            int newAlpha = (int) valueAnimator.getAnimatedValue();
+            int newColor = ColorUtils.setAlphaComponent(placeholderColor, newAlpha);
+
+            newIcon.setColorFilter(new PorterDuffColorFilter(newColor, PorterDuff.Mode.SRC_ATOP));
+        });
+        iconUpdateAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                newIcon.setColorFilter(null);
+            }
+        });
+        iconUpdateAnimation.start();
+    }
+
+}
diff --git a/iconloaderlib/src/com/android/launcher3/util/SafeCloseable.java b/iconloaderlib/src/com/android/launcher3/util/SafeCloseable.java
new file mode 100644
index 0000000..ba8ee04
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/util/SafeCloseable.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2019 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.util;
+
+/**
+ * Extension of closeable which does not throw an exception
+ */
+public interface SafeCloseable extends AutoCloseable {
+
+    @Override
+    void close();
+}