Copies iconloaderlib to frameworks/libs/systemui.

Bug: 138964382
Test: N/A
Change-Id: Idf24deb2acd34b38182cc0dcacbfd4163e36b8be
Merged-In: Idf24deb2acd34b38182cc0dcacbfd4163e36b8be
diff --git a/iconloaderlib/Android.bp b/iconloaderlib/Android.bp
new file mode 100644
index 0000000..f12d16e
--- /dev/null
+++ b/iconloaderlib/Android.bp
@@ -0,0 +1,44 @@
+// 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.
+
+android_library {
+    name: "iconloader_base",
+    sdk_version: "28",
+    min_sdk_version: "21",
+    static_libs: [
+        "androidx.core_core",
+    ],
+    resource_dirs: [
+        "res",
+    ],
+    srcs: [
+        "src/**/*.java",
+    ],
+}
+
+android_library {
+    name: "iconloader",
+    sdk_version: "system_current",
+    min_sdk_version: "21",
+    static_libs: [
+        "androidx.core_core",
+    ],
+    resource_dirs: [
+        "res",
+    ],
+    srcs: [
+        "src/**/*.java",
+        "src_full_lib/**/*.java",
+    ],
+}
diff --git a/iconloaderlib/AndroidManifest.xml b/iconloaderlib/AndroidManifest.xml
new file mode 100644
index 0000000..b30258d
--- /dev/null
+++ b/iconloaderlib/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.launcher3.icons">
+</manifest>
diff --git a/iconloaderlib/IconLoader.iml b/iconloaderlib/IconLoader.iml
new file mode 100644
index 0000000..165abaa
--- /dev/null
+++ b/iconloaderlib/IconLoader.iml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.linked.project.id=":IconLoader" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/../../../../vendor/unbundled_google/packages/SystemUIGoogle/studio-dev/SysUIGradleProject" external.system.id="GRADLE" type="JAVA_MODULE" version="4">
+  <component name="FacetManager">
+    <facet type="android-gradle" name="Android-Gradle">
+      <configuration>
+        <option name="GRADLE_PROJECT_PATH" value=":IconLoader" />
+        <option name="LAST_SUCCESSFUL_SYNC_AGP_VERSION" value="3.3.0" />
+        <option name="LAST_KNOWN_AGP_VERSION" value="3.3.0" />
+      </configuration>
+    </facet>
+    <facet type="android" name="Android">
+      <configuration>
+        <option name="SELECTED_BUILD_VARIANT" value="debug" />
+        <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" />
+        <option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" />
+        <afterSyncTasks>
+          <task>generateDebugSources</task>
+        </afterSyncTasks>
+        <option name="ALLOW_USER_CONFIGURATION" value="false" />
+        <option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/res;file://$MODULE_DIR$/build/generated/res/rs/debug;file://$MODULE_DIR$/build/generated/res/resValues/debug" />
+        <option name="TEST_RES_FOLDERS_RELATIVE_PATH" value="" />
+        <option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
+        <option name="PROJECT_TYPE" value="1" />
+      </configuration>
+    </facet>
+  </component>
+  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
+    <output url="file://$MODULE_DIR$/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes" />
+    <output-test url="file://$MODULE_DIR$/build/intermediates/javac/debugUnitTest/compileDebugUnitTestJavaWithJavac/classes" />
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/debug" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debug/compileDebugAidl/out" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debug/compileDebugRenderscript/out" isTestSource="false" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/debug" type="java-resource" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/androidTest/debug" isTestSource="true" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/aidl_source_output_dir/debugAndroidTest/compileDebugAndroidTestAidl/out" isTestSource="true" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/debug" isTestSource="true" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/renderscript_source_output_dir/debugAndroidTest/compileDebugAndroidTestRenderscript/out" isTestSource="true" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/debug" type="java-test-resource" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/debug" type="java-test-resource" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/test/debug" isTestSource="true" generated="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/debug/shaders" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/res" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/resources" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/assets" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/aidl" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/java" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/rs" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTestDebug/shaders" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testDebug/res" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testDebug/resources" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testDebug/assets" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testDebug/aidl" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testDebug/java" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testDebug/rs" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/testDebug/shaders" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/res" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src_full_lib" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" />
+      <excludeFolder url="file://$MODULE_DIR$/build" />
+    </content>
+    <orderEntry type="jdk" jdkName="Android API 29 Platform" jdkType="Android SDK" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-common:2.0.0@jar" level="project" />
+    <orderEntry type="library" name="Gradle: androidx.arch.core:core-common:2.0.0@jar" level="project" />
+    <orderEntry type="library" name="Gradle: androidx.collection:collection:1.0.0@jar" level="project" />
+    <orderEntry type="library" name="Gradle: androidx.annotation:annotation:1.1.0@jar" level="project" />
+    <orderEntry type="library" name="Gradle: androidx.core:core:1.3.0-alpha02@aar" level="project" />
+    <orderEntry type="library" name="Gradle: androidx.lifecycle:lifecycle-runtime:2.0.0@aar" level="project" />
+    <orderEntry type="library" name="Gradle: androidx.versionedparcelable:versionedparcelable:1.1.0@aar" level="project" />
+  </component>
+</module>
\ No newline at end of file
diff --git a/iconloaderlib/build.gradle b/iconloaderlib/build.gradle
new file mode 100644
index 0000000..d7a62e1
--- /dev/null
+++ b/iconloaderlib/build.gradle
@@ -0,0 +1,38 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion COMPILE_SDK
+    buildToolsVersion BUILD_TOOLS_VERSION
+
+    defaultConfig {
+        minSdkVersion 25
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
+    }
+
+    sourceSets {
+        main {
+            java.srcDirs = ['src', 'src_full_lib']
+            manifest.srcFile 'AndroidManifest.xml'
+            res.srcDirs = ['res']
+        }
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+
+    tasks.withType(JavaCompile) {
+        options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    implementation "androidx.core:core:${ANDROID_X_VERSION}"
+}
diff --git a/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml b/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml
new file mode 100644
index 0000000..9f13cf5
--- /dev/null
+++ b/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2017 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.
+-->
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/legacy_icon_background"/>
+    <foreground>
+        <com.android.launcher3.icons.FixedScaleDrawable />
+    </foreground>
+</adaptive-icon>
diff --git a/iconloaderlib/res/drawable/ic_instant_app_badge.xml b/iconloaderlib/res/drawable/ic_instant_app_badge.xml
new file mode 100644
index 0000000..b74317e
--- /dev/null
+++ b/iconloaderlib/res/drawable/ic_instant_app_badge.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="@dimen/profile_badge_size"
+    android:height="@dimen/profile_badge_size"
+    android:viewportWidth="18"
+    android:viewportHeight="18">
+
+    <path
+        android:fillColor="@android:color/black"
+        android:strokeWidth="1"
+        android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:strokeWidth="1"
+        android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:strokeWidth="1"
+        android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" />
+    <path
+        android:fillColor="@android:color/black"
+        android:fillAlpha="0.87"
+        android:strokeWidth="1"
+        android:pathData="M 6 10.4123279 L 8.63934949 10.4123279 L 8.63934949 15.6 L 12.5577168 7.84517705 L 9.94547194 7.84517705 L 9.94547194 2 Z" />
+</vector>
diff --git a/iconloaderlib/res/values/colors.xml b/iconloaderlib/res/values/colors.xml
new file mode 100644
index 0000000..873b2fc
--- /dev/null
+++ b/iconloaderlib/res/values/colors.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+<resources>
+    <color name="legacy_icon_background">#FFFFFF</color>
+</resources>
diff --git a/iconloaderlib/res/values/config.xml b/iconloaderlib/res/values/config.xml
new file mode 100644
index 0000000..68c2d2e
--- /dev/null
+++ b/iconloaderlib/res/values/config.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+<resources>
+
+    <!-- Various configurations to control the simple cache implementation -->
+
+    <dimen name="default_icon_bitmap_size">56dp</dimen>
+    <bool name="simple_cache_enable_im_memory">false</bool>
+    <string name="cache_db_name" translatable="false">app_icons.db</string>
+
+</resources>
\ No newline at end of file
diff --git a/iconloaderlib/res/values/dimens.xml b/iconloaderlib/res/values/dimens.xml
new file mode 100644
index 0000000..e8c0c44
--- /dev/null
+++ b/iconloaderlib/res/values/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<resources>
+    <dimen name="profile_badge_size">24dp</dimen>
+</resources>
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
new file mode 100644
index 0000000..31a923e
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
@@ -0,0 +1,396 @@
+package com.android.launcher3.icons;
+
+import static android.graphics.Paint.DITHER_FLAG;
+import static android.graphics.Paint.FILTER_BITMAP_FLAG;
+
+import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.PaintFlagsDrawFilter;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Process;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+
+/**
+ * This class will be moved to androidx library. There shouldn't be any dependency outside
+ * this package.
+ */
+public class BaseIconFactory implements AutoCloseable {
+
+    private static final String TAG = "BaseIconFactory";
+    private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE;
+    static final boolean ATLEAST_OREO = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
+    static final boolean ATLEAST_P = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
+
+    private static final float ICON_BADGE_SCALE = 0.444f;
+
+    private final Rect mOldBounds = new Rect();
+    protected final Context mContext;
+    private final Canvas mCanvas;
+    private final PackageManager mPm;
+    private final ColorExtractor mColorExtractor;
+    private boolean mDisableColorExtractor;
+    private boolean mBadgeOnLeft = false;
+
+    protected final int mFillResIconDpi;
+    protected final int mIconBitmapSize;
+
+    private IconNormalizer mNormalizer;
+    private ShadowGenerator mShadowGenerator;
+    private final boolean mShapeDetection;
+
+    private Drawable mWrapperIcon;
+    private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
+
+    protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize,
+            boolean shapeDetection) {
+        mContext = context.getApplicationContext();
+        mShapeDetection = shapeDetection;
+        mFillResIconDpi = fillResIconDpi;
+        mIconBitmapSize = iconBitmapSize;
+
+        mPm = mContext.getPackageManager();
+        mColorExtractor = new ColorExtractor();
+
+        mCanvas = new Canvas();
+        mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG));
+        clear();
+    }
+
+    protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) {
+        this(context, fillResIconDpi, iconBitmapSize, false);
+    }
+
+    protected void clear() {
+        mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
+        mDisableColorExtractor = false;
+        mBadgeOnLeft = false;
+    }
+
+    public ShadowGenerator getShadowGenerator() {
+        if (mShadowGenerator == null) {
+            mShadowGenerator = new ShadowGenerator(mIconBitmapSize);
+        }
+        return mShadowGenerator;
+    }
+
+    public IconNormalizer getNormalizer() {
+        if (mNormalizer == null) {
+            mNormalizer = new IconNormalizer(mContext, mIconBitmapSize, mShapeDetection);
+        }
+        return mNormalizer;
+    }
+
+    @SuppressWarnings("deprecation")
+    public BitmapInfo createIconBitmap(Intent.ShortcutIconResource iconRes) {
+        try {
+            Resources resources = mPm.getResourcesForApplication(iconRes.packageName);
+            if (resources != null) {
+                final int id = resources.getIdentifier(iconRes.resourceName, null, null);
+                // do not stamp old legacy shortcuts as the app may have already forgotten about it
+                return createBadgedIconBitmap(
+                        resources.getDrawableForDensity(id, mFillResIconDpi),
+                        Process.myUserHandle() /* only available on primary user */,
+                        false /* do not apply legacy treatment */);
+            }
+        } catch (Exception e) {
+            // Icon not found.
+        }
+        return null;
+    }
+
+    public BitmapInfo createIconBitmap(Bitmap icon) {
+        if (mIconBitmapSize != icon.getWidth() || mIconBitmapSize != icon.getHeight()) {
+            icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f);
+        }
+
+        return BitmapInfo.of(icon, extractColor(icon));
+    }
+
+    public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
+            boolean shrinkNonAdaptiveIcons) {
+        return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, false, null);
+    }
+
+    public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
+            int iconAppTargetSdk) {
+        return createBadgedIconBitmap(icon, user, iconAppTargetSdk, false);
+    }
+
+    public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
+            int iconAppTargetSdk, boolean isInstantApp) {
+        return createBadgedIconBitmap(icon, user, iconAppTargetSdk, isInstantApp, null);
+    }
+
+    public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
+            int iconAppTargetSdk, boolean isInstantApp, float[] scale) {
+        boolean shrinkNonAdaptiveIcons = ATLEAST_P ||
+                (ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O);
+        return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, isInstantApp, scale);
+    }
+
+    public Bitmap createScaledBitmapWithoutShadow(Drawable icon, int iconAppTargetSdk) {
+        boolean shrinkNonAdaptiveIcons = ATLEAST_P ||
+                (ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O);
+        return  createScaledBitmapWithoutShadow(icon, shrinkNonAdaptiveIcons);
+    }
+
+    /**
+     * Creates bitmap using the source drawable and various parameters.
+     * The bitmap is visually normalized with other icons and has enough spacing to add shadow.
+     *
+     * @param icon                      source of the icon
+     * @param user                      info can be used for a badge
+     * @param shrinkNonAdaptiveIcons    {@code true} if non adaptive icons should be treated
+     * @param isInstantApp              info can be used for a badge
+     * @param scale                     returns the scale result from normalization
+     * @return a bitmap suitable for disaplaying as an icon at various system UIs.
+     */
+    public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon, UserHandle user,
+            boolean shrinkNonAdaptiveIcons, boolean isInstantApp, float[] scale) {
+        if (scale == null) {
+            scale = new float[1];
+        }
+        icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, null, scale);
+        Bitmap bitmap = createIconBitmap(icon, scale[0]);
+        if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) {
+            mCanvas.setBitmap(bitmap);
+            getShadowGenerator().recreateIcon(Bitmap.createBitmap(bitmap), mCanvas);
+            mCanvas.setBitmap(null);
+        }
+
+        if (isInstantApp) {
+            badgeWithDrawable(bitmap, mContext.getDrawable(R.drawable.ic_instant_app_badge));
+        }
+        if (user != null) {
+            BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap);
+            Drawable badged = mPm.getUserBadgedIcon(drawable, user);
+            if (badged instanceof BitmapDrawable) {
+                bitmap = ((BitmapDrawable) badged).getBitmap();
+            } else {
+                bitmap = createIconBitmap(badged, 1f);
+            }
+        }
+        int color = extractColor(bitmap);
+        return icon instanceof BitmapInfo.Extender
+                ? ((BitmapInfo.Extender) icon).getExtendedInfo(bitmap, color, this)
+                : BitmapInfo.of(bitmap, color);
+    }
+
+    public Bitmap createScaledBitmapWithoutShadow(Drawable icon, boolean shrinkNonAdaptiveIcons) {
+        RectF iconBounds = new RectF();
+        float[] scale = new float[1];
+        icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, iconBounds, scale);
+        return createIconBitmap(icon,
+                Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds)));
+    }
+
+    /**
+     * Switches badging to left/right
+     */
+    public void setBadgeOnLeft(boolean badgeOnLeft) {
+        mBadgeOnLeft = badgeOnLeft;
+    }
+
+    /**
+     * Sets the background color used for wrapped adaptive icon
+     */
+    public void setWrapperBackgroundColor(int color) {
+        mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color;
+    }
+
+    /**
+     * Disables the dominant color extraction for all icons loaded.
+     */
+    public void disableColorExtraction() {
+        mDisableColorExtractor = true;
+    }
+
+    private Drawable normalizeAndWrapToAdaptiveIcon(@NonNull Drawable icon,
+            boolean shrinkNonAdaptiveIcons, RectF outIconBounds, float[] outScale) {
+        if (icon == null) {
+            return null;
+        }
+        float scale = 1f;
+
+        if (shrinkNonAdaptiveIcons && ATLEAST_OREO) {
+            if (mWrapperIcon == null) {
+                mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper)
+                        .mutate();
+            }
+            AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon;
+            dr.setBounds(0, 0, 1, 1);
+            boolean[] outShape = new boolean[1];
+            scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape);
+            if (!(icon instanceof AdaptiveIconDrawable) && !outShape[0]) {
+                FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground());
+                fsd.setDrawable(icon);
+                fsd.setScale(scale);
+                icon = dr;
+                scale = getNormalizer().getScale(icon, outIconBounds, null, null);
+
+                ((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor);
+            }
+        } else {
+            scale = getNormalizer().getScale(icon, outIconBounds, null, null);
+        }
+
+        outScale[0] = scale;
+        return icon;
+    }
+
+    /**
+     * Adds the {@param badge} on top of {@param target} using the badge dimensions.
+     */
+    public void badgeWithDrawable(Bitmap target, Drawable badge) {
+        mCanvas.setBitmap(target);
+        badgeWithDrawable(mCanvas, badge);
+        mCanvas.setBitmap(null);
+    }
+
+    /**
+     * Adds the {@param badge} on top of {@param target} using the badge dimensions.
+     */
+    public void badgeWithDrawable(Canvas target, Drawable badge) {
+        int badgeSize = getBadgeSizeForIconSize(mIconBitmapSize);
+        if (mBadgeOnLeft) {
+            badge.setBounds(0, mIconBitmapSize - badgeSize, badgeSize, mIconBitmapSize);
+        } else {
+            badge.setBounds(mIconBitmapSize - badgeSize, mIconBitmapSize - badgeSize,
+                    mIconBitmapSize, mIconBitmapSize);
+        }
+        badge.draw(target);
+    }
+
+    private Bitmap createIconBitmap(Drawable icon, float scale) {
+        return createIconBitmap(icon, scale, mIconBitmapSize);
+    }
+
+    /**
+     * @param icon drawable that should be flattened to a bitmap
+     * @param scale the scale to apply before drawing {@param icon} on the canvas
+     */
+    public Bitmap createIconBitmap(@NonNull Drawable icon, float scale, int size) {
+        Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+        if (icon == null) {
+            return bitmap;
+        }
+        mCanvas.setBitmap(bitmap);
+        mOldBounds.set(icon.getBounds());
+
+        if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) {
+            int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size),
+                    Math.round(size * (1 - scale) / 2 ));
+            icon.setBounds(offset, offset, size - offset, size - offset);
+            icon.draw(mCanvas);
+        } else {
+            if (icon instanceof BitmapDrawable) {
+                BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
+                Bitmap b = bitmapDrawable.getBitmap();
+                if (bitmap != null && b.getDensity() == Bitmap.DENSITY_NONE) {
+                    bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics());
+                }
+            }
+            int width = size;
+            int height = size;
+
+            int intrinsicWidth = icon.getIntrinsicWidth();
+            int intrinsicHeight = icon.getIntrinsicHeight();
+            if (intrinsicWidth > 0 && intrinsicHeight > 0) {
+                // Scale the icon proportionally to the icon dimensions
+                final float ratio = (float) intrinsicWidth / intrinsicHeight;
+                if (intrinsicWidth > intrinsicHeight) {
+                    height = (int) (width / ratio);
+                } else if (intrinsicHeight > intrinsicWidth) {
+                    width = (int) (height * ratio);
+                }
+            }
+            final int left = (size - width) / 2;
+            final int top = (size - height) / 2;
+            icon.setBounds(left, top, left + width, top + height);
+            mCanvas.save();
+            mCanvas.scale(scale, scale, size / 2, size / 2);
+            icon.draw(mCanvas);
+            mCanvas.restore();
+
+        }
+        icon.setBounds(mOldBounds);
+        mCanvas.setBitmap(null);
+        return bitmap;
+    }
+
+    @Override
+    public void close() {
+        clear();
+    }
+
+    public BitmapInfo makeDefaultIcon(UserHandle user) {
+        return createBadgedIconBitmap(getFullResDefaultActivityIcon(mFillResIconDpi),
+                user, Build.VERSION.SDK_INT);
+    }
+
+    public static Drawable getFullResDefaultActivityIcon(int iconDpi) {
+        return Resources.getSystem().getDrawableForDensity(
+                Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+                        ? android.R.drawable.sym_def_app_icon : android.R.mipmap.sym_def_app_icon,
+                iconDpi);
+    }
+
+    /**
+     * Badges the provided source with the badge info
+     */
+    public BitmapInfo badgeBitmap(Bitmap source, BitmapInfo badgeInfo) {
+        Bitmap icon = BitmapRenderer.createHardwareBitmap(mIconBitmapSize, mIconBitmapSize, (c) -> {
+            getShadowGenerator().recreateIcon(source, c);
+            badgeWithDrawable(c, new FixedSizeBitmapDrawable(badgeInfo.icon));
+        });
+        return BitmapInfo.of(icon, badgeInfo.color);
+    }
+
+    private int extractColor(Bitmap bitmap) {
+        return mDisableColorExtractor ? 0 : mColorExtractor.findDominantColorByHue(bitmap);
+    }
+
+    /**
+     * Returns the correct badge size given an icon size
+     */
+    public static int getBadgeSizeForIconSize(int iconSize) {
+        return (int) (ICON_BADGE_SCALE * iconSize);
+    }
+
+    /**
+     * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
+     * This allows the badging to be done based on the action bitmap size rather than
+     * the scaled bitmap size.
+     */
+    private static class FixedSizeBitmapDrawable extends BitmapDrawable {
+
+        public FixedSizeBitmapDrawable(Bitmap bitmap) {
+            super(null, bitmap);
+        }
+
+        @Override
+        public int getIntrinsicHeight() {
+            return getBitmap().getWidth();
+        }
+
+        @Override
+        public int getIntrinsicWidth() {
+            return getBitmap().getWidth();
+        }
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
new file mode 100644
index 0000000..d33f9b1
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2017 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.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+
+import androidx.annotation.NonNull;
+
+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 final Bitmap icon;
+    public final int color;
+
+    public BitmapInfo(Bitmap icon, int color) {
+        this.icon = icon;
+        this.color = color;
+    }
+
+    /**
+     * Ideally icon should not be null, except in cases when generating hardware bitmap failed
+     */
+    public final boolean isNullOrLowRes() {
+        return icon == null || icon == LOW_RES_ICON;
+    }
+
+    public final boolean isLowRes() {
+        return LOW_RES_ICON == icon;
+    }
+
+    public static BitmapInfo fromBitmap(@NonNull Bitmap bitmap) {
+        return of(bitmap, 0);
+    }
+
+    public static BitmapInfo of(@NonNull Bitmap bitmap, int color) {
+        return new BitmapInfo(bitmap, color);
+    }
+
+    /**
+     * Interface to be implemented by drawables to provide a custom BitmapInfo
+     */
+    public interface Extender {
+
+        /**
+         * Called for creating a custom BitmapInfo
+         */
+        default BitmapInfo getExtendedInfo(Bitmap bitmap, int color, BaseIconFactory iconFactory) {
+            return BitmapInfo.of(bitmap, color);
+        }
+
+        /**
+         * Notifies the drawable that it will be drawn directly in the UI, without any preprocessing
+         */
+        default void prepareToDrawOnUi() { }
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapRenderer.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapRenderer.java
new file mode 100644
index 0000000..5751ed9
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/BitmapRenderer.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017 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.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Canvas;
+import android.graphics.Picture;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+
+/**
+ * Interface representing a bitmap draw operation.
+ */
+public interface BitmapRenderer {
+
+    boolean USE_HARDWARE_BITMAP = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
+
+    static Bitmap createSoftwareBitmap(int width, int height, BitmapRenderer renderer) {
+        GraphicsUtils.noteNewBitmapCreated();
+        Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        renderer.draw(new Canvas(result));
+        return result;
+    }
+
+    @TargetApi(Build.VERSION_CODES.P)
+    static Bitmap createHardwareBitmap(int width, int height, BitmapRenderer renderer) {
+        if (!USE_HARDWARE_BITMAP) {
+            return createSoftwareBitmap(width, height, renderer);
+        }
+
+        GraphicsUtils.noteNewBitmapCreated();
+        Picture picture = new Picture();
+        renderer.draw(picture.beginRecording(width, height));
+        picture.endRecording();
+        return Bitmap.createBitmap(picture);
+    }
+
+    /**
+     * Returns a bitmap from subset of the source bitmap. The new bitmap may be the
+     * same object as source, or a copy may have been made.
+     */
+    static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height) {
+        if (Build.VERSION.SDK_INT >= VERSION_CODES.O && source.getConfig() == Config.HARDWARE) {
+            return createHardwareBitmap(width, height, c -> c.drawBitmap(source,
+                    new Rect(x, y, x + width, y + height), new RectF(0, 0, width, height), null));
+        } else {
+            GraphicsUtils.noteNewBitmapCreated();
+            return Bitmap.createBitmap(source, x, y, width, height);
+        }
+    }
+
+    void draw(Canvas out);
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java b/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java
new file mode 100644
index 0000000..87bda82
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2017 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.graphics.Bitmap;
+import android.graphics.Color;
+import android.util.SparseArray;
+import java.util.Arrays;
+
+/**
+ * Utility class for extracting colors from a bitmap.
+ */
+public class ColorExtractor {
+
+    private final int NUM_SAMPLES = 20;
+    private final float[] mTmpHsv = new float[3];
+    private final float[] mTmpHueScoreHistogram = new float[360];
+    private final int[] mTmpPixels = new int[NUM_SAMPLES];
+    private final SparseArray<Float> mTmpRgbScores = new SparseArray<>();
+
+    /**
+     * This picks a dominant color, looking for high-saturation, high-value, repeated hues.
+     * @param bitmap The bitmap to scan
+     */
+    public int findDominantColorByHue(Bitmap bitmap) {
+        return findDominantColorByHue(bitmap, NUM_SAMPLES);
+    }
+
+    /**
+     * This picks a dominant color, looking for high-saturation, high-value, repeated hues.
+     * @param bitmap The bitmap to scan
+     */
+    public int findDominantColorByHue(Bitmap bitmap, int samples) {
+        final int height = bitmap.getHeight();
+        final int width = bitmap.getWidth();
+        int sampleStride = (int) Math.sqrt((height * width) / samples);
+        if (sampleStride < 1) {
+            sampleStride = 1;
+        }
+
+        // This is an out-param, for getting the hsv values for an rgb
+        float[] hsv = mTmpHsv;
+        Arrays.fill(hsv, 0);
+
+        // First get the best hue, by creating a histogram over 360 hue buckets,
+        // where each pixel contributes a score weighted by saturation, value, and alpha.
+        float[] hueScoreHistogram = mTmpHueScoreHistogram;
+        Arrays.fill(hueScoreHistogram, 0);
+        float highScore = -1;
+        int bestHue = -1;
+
+        int[] pixels = mTmpPixels;
+        Arrays.fill(pixels, 0);
+        int pixelCount = 0;
+
+        for (int y = 0; y < height; y += sampleStride) {
+            for (int x = 0; x < width; x += sampleStride) {
+                int argb = bitmap.getPixel(x, y);
+                int alpha = 0xFF & (argb >> 24);
+                if (alpha < 0x80) {
+                    // Drop mostly-transparent pixels.
+                    continue;
+                }
+                // Remove the alpha channel.
+                int rgb = argb | 0xFF000000;
+                Color.colorToHSV(rgb, hsv);
+                // Bucket colors by the 360 integer hues.
+                int hue = (int) hsv[0];
+                if (hue < 0 || hue >= hueScoreHistogram.length) {
+                    // Defensively avoid array bounds violations.
+                    continue;
+                }
+                if (pixelCount < samples) {
+                    pixels[pixelCount++] = rgb;
+                }
+                float score = hsv[1] * hsv[2];
+                hueScoreHistogram[hue] += score;
+                if (hueScoreHistogram[hue] > highScore) {
+                    highScore = hueScoreHistogram[hue];
+                    bestHue = hue;
+                }
+            }
+        }
+
+        SparseArray<Float> rgbScores = mTmpRgbScores;
+        rgbScores.clear();
+        int bestColor = 0xff000000;
+        highScore = -1;
+        // Go back over the RGB colors that match the winning hue,
+        // creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets.
+        // The highest-scoring RGB color wins.
+        for (int i = 0; i < pixelCount; i++) {
+            int rgb = pixels[i];
+            Color.colorToHSV(rgb, hsv);
+            int hue = (int) hsv[0];
+            if (hue == bestHue) {
+                float s = hsv[1];
+                float v = hsv[2];
+                int bucket = (int) (s * 100) + (int) (v * 10000);
+                // Score by cumulative saturation * value.
+                float score = s * v;
+                Float oldTotal = rgbScores.get(bucket);
+                float newTotal = oldTotal == null ? score : oldTotal + score;
+                rgbScores.put(bucket, newTotal);
+                if (newTotal > highScore) {
+                    highScore = newTotal;
+                    // All the colors in the winning bucket are very similar. Last in wins.
+                    bestColor = rgb;
+                }
+            }
+        }
+        return bestColor;
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java b/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java
new file mode 100644
index 0000000..97a0fd3
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/DotRenderer.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2017 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.graphics.Paint.ANTI_ALIAS_FLAG;
+import static android.graphics.Paint.FILTER_BITMAP_FLAG;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.util.Log;
+import android.view.ViewDebug;
+
+/**
+ * Used to draw a notification dot on top of an icon.
+ */
+public class DotRenderer {
+
+    private static final String TAG = "DotRenderer";
+
+    // The dot size is defined as a percentage of the app icon size.
+    private static final float SIZE_PERCENTAGE = 0.228f;
+
+    private final float mCircleRadius;
+    private final Paint mCirclePaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG);
+
+    private final Bitmap mBackgroundWithShadow;
+    private final float mBitmapOffset;
+
+    // Stores the center x and y position as a percentage (0 to 1) of the icon size
+    private final float[] mRightDotPosition;
+    private final float[] mLeftDotPosition;
+
+    public DotRenderer(int iconSizePx, Path iconShapePath, int pathSize) {
+        int size = Math.round(SIZE_PERCENTAGE * iconSizePx);
+        ShadowGenerator.Builder builder = new ShadowGenerator.Builder(Color.TRANSPARENT);
+        builder.ambientShadowAlpha = 88;
+        mBackgroundWithShadow = builder.setupBlurForSize(size).createPill(size, size);
+        mCircleRadius = builder.radius;
+
+        mBitmapOffset = -mBackgroundWithShadow.getHeight() * 0.5f; // Same as width.
+
+        // Find the points on the path that are closest to the top left and right corners.
+        mLeftDotPosition = getPathPoint(iconShapePath, pathSize, -1);
+        mRightDotPosition = getPathPoint(iconShapePath, pathSize, 1);
+    }
+
+    private static float[] getPathPoint(Path path, float size, float direction) {
+        float halfSize = size / 2;
+        // Small delta so that we don't get a zero size triangle
+        float delta = 1;
+
+        float x = halfSize + direction * halfSize;
+        Path trianglePath = new Path();
+        trianglePath.moveTo(halfSize, halfSize);
+        trianglePath.lineTo(x + delta * direction, 0);
+        trianglePath.lineTo(x, -delta);
+        trianglePath.close();
+
+        trianglePath.op(path, Path.Op.INTERSECT);
+        float[] pos = new float[2];
+        new PathMeasure(trianglePath, false).getPosTan(0, pos, null);
+
+        pos[0] = pos[0] / size;
+        pos[1] = pos[1] / size;
+        return pos;
+    }
+
+    public float[] getLeftDotPosition() {
+        return mLeftDotPosition;
+    }
+
+    public float[] getRightDotPosition() {
+        return mRightDotPosition;
+    }
+
+    /**
+     * Draw a circle on top of the canvas according to the given params.
+     */
+    public void draw(Canvas canvas, DrawParams params) {
+        if (params == null) {
+            Log.e(TAG, "Invalid null argument(s) passed in call to draw.");
+            return;
+        }
+        canvas.save();
+
+        Rect iconBounds = params.iconBounds;
+        float[] dotPosition = params.leftAlign ? mLeftDotPosition : mRightDotPosition;
+        float dotCenterX = iconBounds.left + iconBounds.width() * dotPosition[0];
+        float dotCenterY = iconBounds.top + iconBounds.height() * dotPosition[1];
+
+        // Ensure dot fits entirely in canvas clip bounds.
+        Rect canvasBounds = canvas.getClipBounds();
+        float offsetX = params.leftAlign
+                ? Math.max(0, canvasBounds.left - (dotCenterX + mBitmapOffset))
+                : Math.min(0, canvasBounds.right - (dotCenterX - mBitmapOffset));
+        float offsetY = Math.max(0, canvasBounds.top - (dotCenterY + mBitmapOffset));
+
+        // We draw the dot relative to its center.
+        canvas.translate(dotCenterX + offsetX, dotCenterY + offsetY);
+        canvas.scale(params.scale, params.scale);
+
+        mCirclePaint.setColor(Color.BLACK);
+        canvas.drawBitmap(mBackgroundWithShadow, mBitmapOffset, mBitmapOffset, mCirclePaint);
+        mCirclePaint.setColor(params.color);
+        canvas.drawCircle(0, 0, mCircleRadius, mCirclePaint);
+        canvas.restore();
+    }
+
+    public static class DrawParams {
+        /** The color (possibly based on the icon) to use for the dot. */
+        @ViewDebug.ExportedProperty(category = "notification dot", formatToHexString = true)
+        public int color;
+        /** The bounds of the icon that the dot is drawn on top of. */
+        @ViewDebug.ExportedProperty(category = "notification dot")
+        public Rect iconBounds = new Rect();
+        /** The progress of the animation, from 0 to 1. */
+        @ViewDebug.ExportedProperty(category = "notification dot")
+        public float scale;
+        /** Whether the dot should align to the top left of the icon rather than the top right. */
+        @ViewDebug.ExportedProperty(category = "notification dot")
+        public boolean leftAlign;
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java
new file mode 100644
index 0000000..516965e
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/FixedScaleDrawable.java
@@ -0,0 +1,53 @@
+package com.android.launcher3.icons;
+
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.graphics.Canvas;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.DrawableWrapper;
+import android.util.AttributeSet;
+
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Extension of {@link DrawableWrapper} which scales the child drawables by a fixed amount.
+ */
+public class FixedScaleDrawable extends DrawableWrapper {
+
+    // TODO b/33553066 use the constant defined in MaskableIconDrawable
+    private static final float LEGACY_ICON_SCALE = .7f * .6667f;
+    private float mScaleX, mScaleY;
+
+    public FixedScaleDrawable() {
+        super(new ColorDrawable());
+        mScaleX = LEGACY_ICON_SCALE;
+        mScaleY = LEGACY_ICON_SCALE;
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        int saveCount = canvas.save();
+        canvas.scale(mScaleX, mScaleY,
+                getBounds().exactCenterX(), getBounds().exactCenterY());
+        super.draw(canvas);
+        canvas.restoreToCount(saveCount);
+    }
+
+    @Override
+    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) { }
+
+    @Override
+    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) { }
+
+    public void setScale(float scale) {
+        float h = getIntrinsicHeight();
+        float w = getIntrinsicWidth();
+        mScaleX = scale * LEGACY_ICON_SCALE;
+        mScaleY = scale * LEGACY_ICON_SCALE;
+        if (h > w && w > 0) {
+            mScaleX *= w / h;
+        } else if (w > h && h > 0) {
+            mScaleY *= h / w;
+        }
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
new file mode 100644
index 0000000..22f1f23
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
@@ -0,0 +1,85 @@
+/*
+ * 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.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.RegionIterator;
+import android.util.Log;
+
+import androidx.annotation.ColorInt;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+public class GraphicsUtils {
+
+    private static final String TAG = "GraphicsUtils";
+
+    public static Runnable sOnNewBitmapRunnable = () -> { };
+
+    /**
+     * Set the alpha component of {@code color} to be {@code alpha}. Unlike the support lib version,
+     * it bounds the alpha in valid range instead of throwing an exception to allow for safer
+     * interpolation of color animations
+     */
+    @ColorInt
+    public static int setColorAlphaBound(int color, int alpha) {
+        if (alpha < 0) {
+            alpha = 0;
+        } else if (alpha > 255) {
+            alpha = 255;
+        }
+        return (color & 0x00ffffff) | (alpha << 24);
+    }
+
+    /**
+     * 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);
+        try {
+            bitmap.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;
+        }
+    }
+
+    public static int getArea(Region r) {
+        RegionIterator itr = new RegionIterator(r);
+        int area = 0;
+        Rect tempRect = new Rect();
+        while (itr.next(tempRect)) {
+            area += tempRect.width() * tempRect.height();
+        }
+        return area;
+    }
+
+    /**
+     * Utility method to track new bitmap creation
+     */
+    public static void noteNewBitmapCreated() {
+        sOnNewBitmapRunnable.run();
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/IconNormalizer.java b/iconloaderlib/src/com/android/launcher3/icons/IconNormalizer.java
new file mode 100644
index 0000000..de39e79
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/IconNormalizer.java
@@ -0,0 +1,411 @@
+/*
+ * Copyright (C) 2015 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.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.Log;
+
+import java.nio.ByteBuffer;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class IconNormalizer {
+
+    private static final String TAG = "IconNormalizer";
+    private static final boolean DEBUG = false;
+    // Ratio of icon visible area to full icon size for a square shaped icon
+    private static final float MAX_SQUARE_AREA_FACTOR = 375.0f / 576;
+    // Ratio of icon visible area to full icon size for a circular shaped icon
+    private static final float MAX_CIRCLE_AREA_FACTOR = 380.0f / 576;
+
+    private static final float CIRCLE_AREA_BY_RECT = (float) Math.PI / 4;
+
+    // Slope used to calculate icon visible area to full icon size for any generic shaped icon.
+    private static final float LINEAR_SCALE_SLOPE =
+            (MAX_CIRCLE_AREA_FACTOR - MAX_SQUARE_AREA_FACTOR) / (1 - CIRCLE_AREA_BY_RECT);
+
+    private static final int MIN_VISIBLE_ALPHA = 40;
+
+    // Shape detection related constants
+    private static final float BOUND_RATIO_MARGIN = .05f;
+    private static final float PIXEL_DIFF_PERCENTAGE_THRESHOLD = 0.005f;
+    private static final float SCALE_NOT_INITIALIZED = 0;
+
+    // Ratio of the diameter of an normalized circular icon to the actual icon size.
+    public static final float ICON_VISIBLE_AREA_FACTOR = 0.92f;
+
+    private final int mMaxSize;
+    private final Bitmap mBitmap;
+    private final Canvas mCanvas;
+    private final Paint mPaintMaskShape;
+    private final Paint mPaintMaskShapeOutline;
+    private final byte[] mPixels;
+
+    private final RectF mAdaptiveIconBounds;
+    private float mAdaptiveIconScale;
+
+    private boolean mEnableShapeDetection;
+
+    // for each y, stores the position of the leftmost x and the rightmost x
+    private final float[] mLeftBorder;
+    private final float[] mRightBorder;
+    private final Rect mBounds;
+    private final Path mShapePath;
+    private final Matrix mMatrix;
+
+    /** package private **/
+    IconNormalizer(Context context, int iconBitmapSize, boolean shapeDetection) {
+        // Use twice the icon size as maximum size to avoid scaling down twice.
+        mMaxSize = iconBitmapSize * 2;
+        mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8);
+        mCanvas = new Canvas(mBitmap);
+        mPixels = new byte[mMaxSize * mMaxSize];
+        mLeftBorder = new float[mMaxSize];
+        mRightBorder = new float[mMaxSize];
+        mBounds = new Rect();
+        mAdaptiveIconBounds = new RectF();
+
+        mPaintMaskShape = new Paint();
+        mPaintMaskShape.setColor(Color.RED);
+        mPaintMaskShape.setStyle(Paint.Style.FILL);
+        mPaintMaskShape.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR));
+
+        mPaintMaskShapeOutline = new Paint();
+        mPaintMaskShapeOutline.setStrokeWidth(
+                2 * context.getResources().getDisplayMetrics().density);
+        mPaintMaskShapeOutline.setStyle(Paint.Style.STROKE);
+        mPaintMaskShapeOutline.setColor(Color.BLACK);
+        mPaintMaskShapeOutline.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+
+        mShapePath = new Path();
+        mMatrix = new Matrix();
+        mAdaptiveIconScale = SCALE_NOT_INITIALIZED;
+        mEnableShapeDetection = shapeDetection;
+    }
+
+    private static float getScale(float hullArea, float boundingArea, float fullArea) {
+        float hullByRect = hullArea / boundingArea;
+        float scaleRequired;
+        if (hullByRect < CIRCLE_AREA_BY_RECT) {
+            scaleRequired = MAX_CIRCLE_AREA_FACTOR;
+        } else {
+            scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect);
+        }
+
+        float areaScale = hullArea / fullArea;
+        // Use sqrt of the final ratio as the images is scaled across both width and height.
+        return areaScale > scaleRequired ? (float) Math.sqrt(scaleRequired / areaScale) : 1;
+    }
+
+    /**
+     * @param d Should be AdaptiveIconDrawable
+     * @param size Canvas size to use
+     */
+    @TargetApi(Build.VERSION_CODES.O)
+    public static float normalizeAdaptiveIcon(Drawable d, int size, @Nullable RectF outBounds) {
+        Rect tmpBounds = new Rect(d.getBounds());
+        d.setBounds(0, 0, size, size);
+
+        Path path = ((AdaptiveIconDrawable) d).getIconMask();
+        Region region = new Region();
+        region.setPath(path, new Region(0, 0, size, size));
+
+        Rect hullBounds = region.getBounds();
+        int hullArea = GraphicsUtils.getArea(region);
+
+        if (outBounds != null) {
+            float sizeF = size;
+            outBounds.set(
+                    hullBounds.left / sizeF,
+                    hullBounds.top / sizeF,
+                    1 - (hullBounds.right / sizeF),
+                    1 - (hullBounds.bottom / sizeF));
+        }
+        d.setBounds(tmpBounds);
+        return getScale(hullArea, hullArea, size * size);
+    }
+
+    /**
+     * Returns if the shape of the icon is same as the path.
+     * For this method to work, the shape path bounds should be in [0,1]x[0,1] bounds.
+     */
+    private boolean isShape(Path maskPath) {
+        // Condition1:
+        // If width and height of the path not close to a square, then the icon shape is
+        // not same as the mask shape.
+        float iconRatio = ((float) mBounds.width()) / mBounds.height();
+        if (Math.abs(iconRatio - 1) > BOUND_RATIO_MARGIN) {
+            if (DEBUG) {
+                Log.d(TAG, "Not same as mask shape because width != height. " + iconRatio);
+            }
+            return false;
+        }
+
+        // Condition 2:
+        // Actual icon (white) and the fitted shape (e.g., circle)(red) XOR operation
+        // should generate transparent image, if the actual icon is equivalent to the shape.
+
+        // Fit the shape within the icon's bounding box
+        mMatrix.reset();
+        mMatrix.setScale(mBounds.width(), mBounds.height());
+        mMatrix.postTranslate(mBounds.left, mBounds.top);
+        maskPath.transform(mMatrix, mShapePath);
+
+        // XOR operation
+        mCanvas.drawPath(mShapePath, mPaintMaskShape);
+
+        // DST_OUT operation around the mask path outline
+        mCanvas.drawPath(mShapePath, mPaintMaskShapeOutline);
+
+        // Check if the result is almost transparent
+        return isTransparentBitmap();
+    }
+
+    /**
+     * Used to determine if certain the bitmap is transparent.
+     */
+    private boolean isTransparentBitmap() {
+        ByteBuffer buffer = ByteBuffer.wrap(mPixels);
+        buffer.rewind();
+        mBitmap.copyPixelsToBuffer(buffer);
+
+        int y = mBounds.top;
+        // buffer position
+        int index = y * mMaxSize;
+        // buffer shift after every row, width of buffer = mMaxSize
+        int rowSizeDiff = mMaxSize - mBounds.right;
+
+        int sum = 0;
+        for (; y < mBounds.bottom; y++) {
+            index += mBounds.left;
+            for (int x = mBounds.left; x < mBounds.right; x++) {
+                if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) {
+                    sum++;
+                }
+                index++;
+            }
+            index += rowSizeDiff;
+        }
+
+        float percentageDiffPixels = ((float) sum) / (mBounds.width() * mBounds.height());
+        return percentageDiffPixels < PIXEL_DIFF_PERCENTAGE_THRESHOLD;
+    }
+
+    /**
+     * Returns the amount by which the {@param d} should be scaled (in both dimensions) so that it
+     * matches the design guidelines for a launcher icon.
+     *
+     * We first calculate the convex hull of the visible portion of the icon.
+     * This hull then compared with the bounding rectangle of the hull to find how closely it
+     * resembles a circle and a square, by comparing the ratio of the areas. Note that this is not an
+     * ideal solution but it gives satisfactory result without affecting the performance.
+     *
+     * This closeness is used to determine the ratio of hull area to the full icon size.
+     * Refer {@link #MAX_CIRCLE_AREA_FACTOR} and {@link #MAX_SQUARE_AREA_FACTOR}
+     *
+     * @param outBounds optional rect to receive the fraction distance from each edge.
+     */
+    public synchronized float getScale(@NonNull Drawable d, @Nullable RectF outBounds,
+            @Nullable Path path, @Nullable boolean[] outMaskShape) {
+        if (BaseIconFactory.ATLEAST_OREO && d instanceof AdaptiveIconDrawable) {
+            if (mAdaptiveIconScale == SCALE_NOT_INITIALIZED) {
+                mAdaptiveIconScale = normalizeAdaptiveIcon(d, mMaxSize, mAdaptiveIconBounds);
+            }
+            if (outBounds != null) {
+                outBounds.set(mAdaptiveIconBounds);
+            }
+            return mAdaptiveIconScale;
+        }
+        int width = d.getIntrinsicWidth();
+        int height = d.getIntrinsicHeight();
+        if (width <= 0 || height <= 0) {
+            width = width <= 0 || width > mMaxSize ? mMaxSize : width;
+            height = height <= 0 || height > mMaxSize ? mMaxSize : height;
+        } else if (width > mMaxSize || height > mMaxSize) {
+            int max = Math.max(width, height);
+            width = mMaxSize * width / max;
+            height = mMaxSize * height / max;
+        }
+
+        mBitmap.eraseColor(Color.TRANSPARENT);
+        d.setBounds(0, 0, width, height);
+        d.draw(mCanvas);
+
+        ByteBuffer buffer = ByteBuffer.wrap(mPixels);
+        buffer.rewind();
+        mBitmap.copyPixelsToBuffer(buffer);
+
+        // Overall bounds of the visible icon.
+        int topY = -1;
+        int bottomY = -1;
+        int leftX = mMaxSize + 1;
+        int rightX = -1;
+
+        // Create border by going through all pixels one row at a time and for each row find
+        // the first and the last non-transparent pixel. Set those values to mLeftBorder and
+        // mRightBorder and use -1 if there are no visible pixel in the row.
+
+        // buffer position
+        int index = 0;
+        // buffer shift after every row, width of buffer = mMaxSize
+        int rowSizeDiff = mMaxSize - width;
+        // first and last position for any row.
+        int firstX, lastX;
+
+        for (int y = 0; y < height; y++) {
+            firstX = lastX = -1;
+            for (int x = 0; x < width; x++) {
+                if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) {
+                    if (firstX == -1) {
+                        firstX = x;
+                    }
+                    lastX = x;
+                }
+                index++;
+            }
+            index += rowSizeDiff;
+
+            mLeftBorder[y] = firstX;
+            mRightBorder[y] = lastX;
+
+            // If there is at least one visible pixel, update the overall bounds.
+            if (firstX != -1) {
+                bottomY = y;
+                if (topY == -1) {
+                    topY = y;
+                }
+
+                leftX = Math.min(leftX, firstX);
+                rightX = Math.max(rightX, lastX);
+            }
+        }
+
+        if (topY == -1 || rightX == -1) {
+            // No valid pixels found. Do not scale.
+            return 1;
+        }
+
+        convertToConvexArray(mLeftBorder, 1, topY, bottomY);
+        convertToConvexArray(mRightBorder, -1, topY, bottomY);
+
+        // Area of the convex hull
+        float area = 0;
+        for (int y = 0; y < height; y++) {
+            if (mLeftBorder[y] <= -1) {
+                continue;
+            }
+            area += mRightBorder[y] - mLeftBorder[y] + 1;
+        }
+
+        mBounds.left = leftX;
+        mBounds.right = rightX;
+
+        mBounds.top = topY;
+        mBounds.bottom = bottomY;
+
+        if (outBounds != null) {
+            outBounds.set(((float) mBounds.left) / width, ((float) mBounds.top) / height,
+                    1 - ((float) mBounds.right) / width,
+                    1 - ((float) mBounds.bottom) / height);
+        }
+        if (outMaskShape != null && mEnableShapeDetection && outMaskShape.length > 0) {
+            outMaskShape[0] = isShape(path);
+        }
+        // Area of the rectangle required to fit the convex hull
+        float rectArea = (bottomY + 1 - topY) * (rightX + 1 - leftX);
+        return getScale(area, rectArea, width * height);
+    }
+
+    /**
+     * Modifies {@param xCoordinates} to represent a convex border. Fills in all missing values
+     * (except on either ends) with appropriate values.
+     * @param xCoordinates map of x coordinate per y.
+     * @param direction 1 for left border and -1 for right border.
+     * @param topY the first Y position (inclusive) with a valid value.
+     * @param bottomY the last Y position (inclusive) with a valid value.
+     */
+    private static void convertToConvexArray(
+            float[] xCoordinates, int direction, int topY, int bottomY) {
+        int total = xCoordinates.length;
+        // The tangent at each pixel.
+        float[] angles = new float[total - 1];
+
+        int first = topY; // First valid y coordinate
+        int last = -1;    // Last valid y coordinate which didn't have a missing value
+
+        float lastAngle = Float.MAX_VALUE;
+
+        for (int i = topY + 1; i <= bottomY; i++) {
+            if (xCoordinates[i] <= -1) {
+                continue;
+            }
+            int start;
+
+            if (lastAngle == Float.MAX_VALUE) {
+                start = first;
+            } else {
+                float currentAngle = (xCoordinates[i] - xCoordinates[last]) / (i - last);
+                start = last;
+                // If this position creates a concave angle, keep moving up until we find a
+                // position which creates a convex angle.
+                if ((currentAngle - lastAngle) * direction < 0) {
+                    while (start > first) {
+                        start --;
+                        currentAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start);
+                        if ((currentAngle - angles[start]) * direction >= 0) {
+                            break;
+                        }
+                    }
+                }
+            }
+
+            // Reset from last check
+            lastAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start);
+            // Update all the points from start.
+            for (int j = start; j < i; j++) {
+                angles[j] = lastAngle;
+                xCoordinates[j] = xCoordinates[start] + lastAngle * (j - start);
+            }
+            last = i;
+        }
+    }
+
+    /**
+     * @return The diameter of the normalized circle that fits inside of the square (size x size).
+     */
+    public static int getNormalizedCircleSize(int size) {
+        float area = size * size * MAX_CIRCLE_AREA_FACTOR;
+        return (int) Math.round(Math.sqrt((4 * area) / Math.PI));
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java b/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java
new file mode 100644
index 0000000..7702727
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2016 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 com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
+
+import android.graphics.Bitmap;
+import android.graphics.BlurMaskFilter;
+import android.graphics.BlurMaskFilter.Blur;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.RectF;
+
+/**
+ * Utility class to add shadows to bitmaps.
+ */
+public class ShadowGenerator {
+    public static final float BLUR_FACTOR = 0.5f/48;
+
+    // Percent of actual icon size
+    public static final float KEY_SHADOW_DISTANCE = 1f/48;
+    private static final int KEY_SHADOW_ALPHA = 61;
+    // Percent of actual icon size
+    private static final float HALF_DISTANCE = 0.5f;
+    private static final int AMBIENT_SHADOW_ALPHA = 30;
+
+    private final int mIconSize;
+
+    private final Paint mBlurPaint;
+    private final Paint mDrawPaint;
+    private final BlurMaskFilter mDefaultBlurMaskFilter;
+
+    public ShadowGenerator(int iconSize) {
+        mIconSize = iconSize;
+        mBlurPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
+        mDrawPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
+        mDefaultBlurMaskFilter = new BlurMaskFilter(mIconSize * BLUR_FACTOR, Blur.NORMAL);
+    }
+
+    public synchronized void recreateIcon(Bitmap icon, Canvas out) {
+        recreateIcon(icon, mDefaultBlurMaskFilter, AMBIENT_SHADOW_ALPHA, KEY_SHADOW_ALPHA, out);
+    }
+
+    public synchronized void recreateIcon(Bitmap icon, BlurMaskFilter blurMaskFilter,
+            int ambientAlpha, int keyAlpha, Canvas out) {
+        int[] offset = new int[2];
+        mBlurPaint.setMaskFilter(blurMaskFilter);
+        Bitmap shadow = icon.extractAlpha(mBlurPaint, offset);
+
+        // Draw ambient shadow
+        mDrawPaint.setAlpha(ambientAlpha);
+        out.drawBitmap(shadow, offset[0], offset[1], mDrawPaint);
+
+        // Draw key shadow
+        mDrawPaint.setAlpha(keyAlpha);
+        out.drawBitmap(shadow, offset[0], offset[1] + KEY_SHADOW_DISTANCE * mIconSize, mDrawPaint);
+
+        // Draw the icon
+        mDrawPaint.setAlpha(255);
+        out.drawBitmap(icon, 0, 0, mDrawPaint);
+    }
+
+    /**
+     * Returns the minimum amount by which an icon with {@param bounds} should be scaled
+     * so that the shadows do not get clipped.
+     */
+    public static float getScaleForBounds(RectF bounds) {
+        float scale = 1;
+
+        // For top, left & right, we need same space.
+        float minSide = Math.min(Math.min(bounds.left, bounds.right), bounds.top);
+        if (minSide < BLUR_FACTOR) {
+            scale = (HALF_DISTANCE - BLUR_FACTOR) / (HALF_DISTANCE - minSide);
+        }
+
+        float bottomSpace = BLUR_FACTOR + KEY_SHADOW_DISTANCE;
+        if (bounds.bottom < bottomSpace) {
+            scale = Math.min(scale, (HALF_DISTANCE - bottomSpace) / (HALF_DISTANCE - bounds.bottom));
+        }
+        return scale;
+    }
+
+    public static class Builder {
+
+        public final RectF bounds = new RectF();
+        public final int color;
+
+        public int ambientShadowAlpha = AMBIENT_SHADOW_ALPHA;
+
+        public float shadowBlur;
+
+        public float keyShadowDistance;
+        public int keyShadowAlpha = KEY_SHADOW_ALPHA;
+        public float radius;
+
+        public Builder(int color) {
+            this.color = color;
+        }
+
+        public Builder setupBlurForSize(int height) {
+            shadowBlur = height * 1f / 24;
+            keyShadowDistance = height * 1f / 16;
+            return this;
+        }
+
+        public Bitmap createPill(int width, int height) {
+            return createPill(width, height, height / 2f);
+        }
+
+        public Bitmap createPill(int width, int height, float r) {
+            radius = r;
+
+            int centerX = Math.round(width / 2f + shadowBlur);
+            int centerY = Math.round(radius + shadowBlur + keyShadowDistance);
+            int center = Math.max(centerX, centerY);
+            bounds.set(0, 0, width, height);
+            bounds.offsetTo(center - width / 2f, center - height / 2f);
+
+            int size = center * 2;
+            return BitmapRenderer.createHardwareBitmap(size, size, this::drawShadow);
+        }
+
+        public void drawShadow(Canvas c) {
+            Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
+            p.setColor(color);
+
+            // Key shadow
+            p.setShadowLayer(shadowBlur, 0, keyShadowDistance,
+                    setColorAlphaBound(Color.BLACK, keyShadowAlpha));
+            c.drawRoundRect(bounds, radius, radius, p);
+
+            // Ambient shadow
+            p.setShadowLayer(shadowBlur, 0, 0,
+                    setColorAlphaBound(Color.BLACK, ambientShadowAlpha));
+            c.drawRoundRect(bounds, radius, radius, p);
+
+            if (Color.alpha(color) < 255) {
+                // Clear any content inside the pill-rect for translucent fill.
+                p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+                p.clearShadowLayer();
+                p.setColor(Color.BLACK);
+                c.drawRoundRect(bounds, radius, radius, p);
+
+                p.setXfermode(null);
+                p.setColor(color);
+                c.drawRoundRect(bounds, radius, radius, p);
+            }
+        }
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
new file mode 100644
index 0000000..4c634cb
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
@@ -0,0 +1,582 @@
+/*
+ * 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.cache;
+
+import static com.android.launcher3.icons.BaseIconFactory.getFullResDefaultActivityIcon;
+import static com.android.launcher3.icons.BitmapInfo.LOW_RES_ICON;
+import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
+
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Handler;
+import android.os.LocaleList;
+import android.os.Looper;
+import android.os.Process;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.icons.BaseIconFactory;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.BitmapRenderer;
+import com.android.launcher3.icons.GraphicsUtils;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.SQLiteCacheHelper;
+
+import java.util.AbstractMap;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Supplier;
+
+public abstract class BaseIconCache {
+
+    private static final String TAG = "BaseIconCache";
+    private static final boolean DEBUG = false;
+
+    private static final int INITIAL_ICON_CACHE_CAPACITY = 50;
+
+    // Empty class name is used for storing package default entry.
+    public static final String EMPTY_CLASS_NAME = ".";
+
+    public static class CacheEntry {
+
+        @NonNull
+        public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO;
+        public CharSequence title = "";
+        public CharSequence contentDescription = "";
+    }
+
+    private final HashMap<UserHandle, BitmapInfo> mDefaultIcons = new HashMap<>();
+
+    protected final Context mContext;
+    protected final PackageManager mPackageManager;
+
+    private final Map<ComponentKey, CacheEntry> mCache;
+    protected final Handler mWorkerHandler;
+
+    protected int mIconDpi;
+    protected IconDB mIconDb;
+    protected LocaleList mLocaleList = LocaleList.getEmptyLocaleList();
+    protected String mSystemState = "";
+
+    private final String mDbFileName;
+    private final BitmapFactory.Options mDecodeOptions;
+    private final Looper mBgLooper;
+
+    public BaseIconCache(Context context, String dbFileName, Looper bgLooper,
+            int iconDpi, int iconPixelSize, boolean inMemoryCache) {
+        mContext = context;
+        mDbFileName = dbFileName;
+        mPackageManager = context.getPackageManager();
+        mBgLooper = bgLooper;
+        mWorkerHandler = new Handler(mBgLooper);
+
+        if (inMemoryCache) {
+            mCache = new HashMap<>(INITIAL_ICON_CACHE_CAPACITY);
+        } else {
+            // Use a dummy cache
+            mCache = new AbstractMap<ComponentKey, CacheEntry>() {
+                @Override
+                public Set<Entry<ComponentKey, CacheEntry>> entrySet() {
+                    return Collections.emptySet();
+                }
+
+                @Override
+                public CacheEntry put(ComponentKey key, CacheEntry value) {
+                    return value;
+                }
+            };
+        }
+
+        if (BitmapRenderer.USE_HARDWARE_BITMAP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            mDecodeOptions = new BitmapFactory.Options();
+            mDecodeOptions.inPreferredConfig = Bitmap.Config.HARDWARE;
+        } else {
+            mDecodeOptions = null;
+        }
+
+        updateSystemState();
+        mIconDpi = iconDpi;
+        mIconDb = new IconDB(context, dbFileName, iconPixelSize);
+    }
+
+    /**
+     * Returns the persistable serial number for {@param user}. Subclass should implement proper
+     * caching strategy to avoid making binder call every time.
+     */
+    protected abstract long getSerialNumberForUser(UserHandle user);
+
+    /**
+     * Return true if the given app is an instant app and should be badged appropriately.
+     */
+    protected abstract boolean isInstantApp(ApplicationInfo info);
+
+    /**
+     * Opens and returns an icon factory. The factory is recycled by the caller.
+     */
+    protected abstract BaseIconFactory getIconFactory();
+
+    public void updateIconParams(int iconDpi, int iconPixelSize) {
+        mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize));
+    }
+
+    private synchronized void updateIconParamsBg(int iconDpi, int iconPixelSize) {
+        mIconDpi = iconDpi;
+        mDefaultIcons.clear();
+        mIconDb.clear();
+        mIconDb.close();
+        mIconDb = new IconDB(mContext, mDbFileName, iconPixelSize);
+        mCache.clear();
+    }
+
+    private Drawable getFullResIcon(Resources resources, int iconId) {
+        if (resources != null && iconId != 0) {
+            try {
+                return resources.getDrawableForDensity(iconId, mIconDpi);
+            } catch (Resources.NotFoundException e) { }
+        }
+        return getFullResDefaultActivityIcon(mIconDpi);
+    }
+
+    public Drawable getFullResIcon(String packageName, int iconId) {
+        try {
+            return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId);
+        } catch (PackageManager.NameNotFoundException e) { }
+        return getFullResDefaultActivityIcon(mIconDpi);
+    }
+
+    public Drawable getFullResIcon(ActivityInfo info) {
+        try {
+            return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo),
+                    info.getIconResource());
+        } catch (PackageManager.NameNotFoundException e) { }
+        return getFullResDefaultActivityIcon(mIconDpi);
+    }
+
+    private BitmapInfo makeDefaultIcon(UserHandle user) {
+        try (BaseIconFactory li = getIconFactory()) {
+            return li.makeDefaultIcon(user);
+        }
+    }
+
+    /**
+     * Remove any records for the supplied ComponentName.
+     */
+    public synchronized void remove(ComponentName componentName, UserHandle user) {
+        mCache.remove(new ComponentKey(componentName, user));
+    }
+
+    /**
+     * Remove any records for the supplied package name from memory.
+     */
+    private void removeFromMemCacheLocked(String packageName, UserHandle user) {
+        HashSet<ComponentKey> forDeletion = new HashSet<>();
+        for (ComponentKey key: mCache.keySet()) {
+            if (key.componentName.getPackageName().equals(packageName)
+                    && key.user.equals(user)) {
+                forDeletion.add(key);
+            }
+        }
+        for (ComponentKey condemned: forDeletion) {
+            mCache.remove(condemned);
+        }
+    }
+
+    /**
+     * Removes the entries related to the given package in memory and persistent DB.
+     */
+    public synchronized void removeIconsForPkg(String packageName, UserHandle user) {
+        removeFromMemCacheLocked(packageName, user);
+        long userSerial = getSerialNumberForUser(user);
+        mIconDb.delete(
+                IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?",
+                new String[]{packageName + "/%", Long.toString(userSerial)});
+    }
+
+    public IconCacheUpdateHandler getUpdateHandler() {
+        updateSystemState();
+        return new IconCacheUpdateHandler(this);
+    }
+
+    /**
+     * Refreshes the system state definition used to check the validity of the cache. It
+     * incorporates all the properties that can affect the cache like the list of enabled locale
+     * and system-version.
+     */
+    private void updateSystemState() {
+        mLocaleList = mContext.getResources().getConfiguration().getLocales();
+        mSystemState = mLocaleList.toLanguageTags() + "," + Build.VERSION.SDK_INT;
+    }
+
+    protected String getIconSystemState(String packageName) {
+        return mSystemState;
+    }
+
+    /**
+     * Adds an entry into the DB and the in-memory cache.
+     * @param replaceExisting if true, it will recreate the bitmap even if it already exists in
+     *                        the memory. This is useful then the previous bitmap was created using
+     *                        old data.
+     */
+    @VisibleForTesting
+    public synchronized <T> void addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic,
+            PackageInfo info, long userSerial, boolean replaceExisting) {
+        UserHandle user = cachingLogic.getUser(object);
+        ComponentName componentName = cachingLogic.getComponent(object);
+
+        final ComponentKey key = new ComponentKey(componentName, user);
+        CacheEntry entry = null;
+        if (!replaceExisting) {
+            entry = mCache.get(key);
+            // We can't reuse the entry if the high-res icon is not present.
+            if (entry == null || entry.bitmap.isNullOrLowRes()) {
+                entry = null;
+            }
+        }
+        if (entry == null) {
+            entry = new CacheEntry();
+            entry.bitmap = cachingLogic.loadIcon(mContext, object);
+        }
+        // Icon can't be loaded from cachingLogic, which implies alternative icon was loaded
+        // (e.g. fallback icon, default icon). So we drop here since there's no point in caching
+        // an empty entry.
+        if (entry.bitmap.isNullOrLowRes()) return;
+        entry.title = cachingLogic.getLabel(object);
+        entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
+        if (cachingLogic.addToMemCache()) mCache.put(key, entry);
+
+        ContentValues values = newContentValues(entry.bitmap, entry.title.toString(),
+                componentName.getPackageName(), cachingLogic.getKeywords(object, mLocaleList));
+        addIconToDB(values, componentName, info, userSerial,
+                cachingLogic.getLastUpdatedTime(object, info));
+    }
+
+    /**
+     * Updates {@param values} to contain versioning information and adds it to the DB.
+     * @param values {@link ContentValues} containing icon & title
+     */
+    private void addIconToDB(ContentValues values, ComponentName key,
+            PackageInfo info, long userSerial, long lastUpdateTime) {
+        values.put(IconDB.COLUMN_COMPONENT, key.flattenToString());
+        values.put(IconDB.COLUMN_USER, userSerial);
+        values.put(IconDB.COLUMN_LAST_UPDATED, lastUpdateTime);
+        values.put(IconDB.COLUMN_VERSION, info.versionCode);
+        mIconDb.insertOrReplace(values);
+    }
+
+    public synchronized BitmapInfo getDefaultIcon(UserHandle user) {
+        if (!mDefaultIcons.containsKey(user)) {
+            mDefaultIcons.put(user, makeDefaultIcon(user));
+        }
+        return mDefaultIcons.get(user);
+    }
+
+    public boolean isDefaultIcon(BitmapInfo icon, UserHandle user) {
+        return getDefaultIcon(user).icon == icon.icon;
+    }
+
+    /**
+     * Retrieves the entry from the cache. If the entry is not present, it creates a new entry.
+     * This method is not thread safe, it must be called from a synchronized method.
+     */
+    protected <T> CacheEntry cacheLocked(
+            @NonNull ComponentName componentName, @NonNull UserHandle user,
+            @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic,
+            boolean usePackageIcon, boolean useLowResIcon) {
+        assertWorkerThread();
+        ComponentKey cacheKey = new ComponentKey(componentName, user);
+        CacheEntry entry = mCache.get(cacheKey);
+        if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) {
+            entry = new CacheEntry();
+            if (cachingLogic.addToMemCache()) {
+                mCache.put(cacheKey, entry);
+            }
+
+            // Check the DB first.
+            T object = null;
+            boolean providerFetchedOnce = false;
+
+            if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
+                object = infoProvider.get();
+                providerFetchedOnce = true;
+
+                if (object != null) {
+                    entry.bitmap = cachingLogic.loadIcon(mContext, object);
+                } else {
+                    if (usePackageIcon) {
+                        CacheEntry packageEntry = getEntryForPackageLocked(
+                                componentName.getPackageName(), user, false);
+                        if (packageEntry != null) {
+                            if (DEBUG) Log.d(TAG, "using package default icon for " +
+                                    componentName.toShortString());
+                            entry.bitmap = packageEntry.bitmap;
+                            entry.title = packageEntry.title;
+                            entry.contentDescription = packageEntry.contentDescription;
+                        }
+                    }
+                    if (entry.bitmap == null) {
+                        if (DEBUG) Log.d(TAG, "using default icon for " +
+                                componentName.toShortString());
+                        entry.bitmap = getDefaultIcon(user);
+                    }
+                }
+            }
+
+            if (TextUtils.isEmpty(entry.title)) {
+                if (object == null && !providerFetchedOnce) {
+                    object = infoProvider.get();
+                    providerFetchedOnce = true;
+                }
+                if (object != null) {
+                    entry.title = cachingLogic.getLabel(object);
+                    entry.contentDescription = mPackageManager.getUserBadgedLabel(
+                            cachingLogic.getDescription(object, entry.title), user);
+                }
+            }
+        }
+        return entry;
+    }
+
+    public synchronized void clear() {
+        assertWorkerThread();
+        mIconDb.clear();
+    }
+
+    /**
+     * Adds a default package entry in the cache. This entry is not persisted and will be removed
+     * when the cache is flushed.
+     */
+    protected synchronized void cachePackageInstallInfo(String packageName, UserHandle user,
+            Bitmap icon, CharSequence title) {
+        removeFromMemCacheLocked(packageName, user);
+
+        ComponentKey cacheKey = getPackageKey(packageName, user);
+        CacheEntry entry = mCache.get(cacheKey);
+
+        // For icon caching, do not go through DB. Just update the in-memory entry.
+        if (entry == null) {
+            entry = new CacheEntry();
+        }
+        if (!TextUtils.isEmpty(title)) {
+            entry.title = title;
+        }
+        if (icon != null) {
+            BaseIconFactory li = getIconFactory();
+            entry.bitmap = li.createIconBitmap(icon);
+            li.close();
+        }
+        if (!TextUtils.isEmpty(title) && entry.bitmap.icon != null) {
+            mCache.put(cacheKey, entry);
+        }
+    }
+
+    private static ComponentKey getPackageKey(String packageName, UserHandle user) {
+        ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME);
+        return new ComponentKey(cn, user);
+    }
+
+    /**
+     * Gets an entry for the package, which can be used as a fallback entry for various components.
+     * This method is not thread safe, it must be called from a synchronized method.
+     */
+    protected CacheEntry getEntryForPackageLocked(String packageName, UserHandle user,
+            boolean useLowResIcon) {
+        assertWorkerThread();
+        ComponentKey cacheKey = getPackageKey(packageName, user);
+        CacheEntry entry = mCache.get(cacheKey);
+
+        if (entry == null || (entry.bitmap.isLowRes() && !useLowResIcon)) {
+            entry = new CacheEntry();
+            boolean entryUpdated = true;
+
+            // Check the DB first.
+            if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
+                try {
+                    int flags = Process.myUserHandle().equals(user) ? 0 :
+                            PackageManager.GET_UNINSTALLED_PACKAGES;
+                    PackageInfo info = mPackageManager.getPackageInfo(packageName, flags);
+                    ApplicationInfo appInfo = info.applicationInfo;
+                    if (appInfo == null) {
+                        throw new NameNotFoundException("ApplicationInfo is null");
+                    }
+
+                    BaseIconFactory li = getIconFactory();
+                    // Load the full res icon for the application, but if useLowResIcon is set, then
+                    // only keep the low resolution icon instead of the larger full-sized icon
+                    BitmapInfo iconInfo = li.createBadgedIconBitmap(
+                            appInfo.loadIcon(mPackageManager), user, appInfo.targetSdkVersion,
+                            isInstantApp(appInfo));
+                    li.close();
+
+                    entry.title = appInfo.loadLabel(mPackageManager);
+                    entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
+                    entry.bitmap = BitmapInfo.of(
+                            useLowResIcon ? LOW_RES_ICON : iconInfo.icon, iconInfo.color);
+
+                    // Add the icon in the DB here, since these do not get written during
+                    // package updates.
+                    ContentValues values = newContentValues(
+                            iconInfo, entry.title.toString(), packageName, null);
+                    addIconToDB(values, cacheKey.componentName, info, getSerialNumberForUser(user),
+                            info.lastUpdateTime);
+
+                } catch (NameNotFoundException e) {
+                    if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
+                    entryUpdated = false;
+                }
+            }
+
+            // Only add a filled-out entry to the cache
+            if (entryUpdated) {
+                mCache.put(cacheKey, entry);
+            }
+        }
+        return entry;
+    }
+
+    protected boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
+        Cursor c = null;
+        try {
+            c = mIconDb.query(
+                    lowRes ? IconDB.COLUMNS_LOW_RES : IconDB.COLUMNS_HIGH_RES,
+                    IconDB.COLUMN_COMPONENT + " = ? AND " + IconDB.COLUMN_USER + " = ?",
+                    new String[]{
+                            cacheKey.componentName.flattenToString(),
+                            Long.toString(getSerialNumberForUser(cacheKey.user))});
+            if (c.moveToNext()) {
+                // Set the alpha to be 255, so that we never have a wrong color
+                entry.bitmap = BitmapInfo.of(LOW_RES_ICON, setColorAlphaBound(c.getInt(0), 255));
+                entry.title = c.getString(1);
+                if (entry.title == null) {
+                    entry.title = "";
+                    entry.contentDescription = "";
+                } else {
+                    entry.contentDescription = mPackageManager.getUserBadgedLabel(
+                            entry.title, cacheKey.user);
+                }
+
+                if (!lowRes) {
+                    byte[] data = c.getBlob(2);
+                    try {
+                        entry.bitmap = BitmapInfo.of(
+                                BitmapFactory.decodeByteArray(data, 0, data.length, mDecodeOptions),
+                                entry.bitmap.color);
+                    } catch (Exception e) { }
+                }
+                return true;
+            }
+        } catch (SQLiteException e) {
+            Log.d(TAG, "Error reading icon cache", e);
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns a cursor for an arbitrary query to the cache db
+     */
+    public synchronized Cursor queryCacheDb(String[] columns, String selection,
+            String[] selectionArgs) {
+        return mIconDb.query(columns, selection, selectionArgs);
+    }
+
+    /**
+     * Cache class to store the actual entries on disk
+     */
+    public static final class IconDB extends SQLiteCacheHelper {
+        private static final int RELEASE_VERSION = 27;
+
+        public static final String TABLE_NAME = "icons";
+        public static final String COLUMN_ROWID = "rowid";
+        public static final String COLUMN_COMPONENT = "componentName";
+        public static final String COLUMN_USER = "profileId";
+        public static final String COLUMN_LAST_UPDATED = "lastUpdated";
+        public static final String COLUMN_VERSION = "version";
+        public static final String COLUMN_ICON = "icon";
+        public static final String COLUMN_ICON_COLOR = "icon_color";
+        public static final String COLUMN_LABEL = "label";
+        public static final String COLUMN_SYSTEM_STATE = "system_state";
+        public static final String COLUMN_KEYWORDS = "keywords";
+
+        public static final String[] COLUMNS_HIGH_RES = new String[] {
+                IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL, IconDB.COLUMN_ICON };
+        public static final String[] COLUMNS_LOW_RES = new String[] {
+                IconDB.COLUMN_ICON_COLOR, IconDB.COLUMN_LABEL };
+
+        public IconDB(Context context, String dbFileName, int iconPixelSize) {
+            super(context, dbFileName, (RELEASE_VERSION << 16) + iconPixelSize, TABLE_NAME);
+        }
+
+        @Override
+        protected void onCreateTable(SQLiteDatabase db) {
+            db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " ("
+                    + COLUMN_COMPONENT + " TEXT NOT NULL, "
+                    + COLUMN_USER + " INTEGER NOT NULL, "
+                    + COLUMN_LAST_UPDATED + " INTEGER NOT NULL DEFAULT 0, "
+                    + COLUMN_VERSION + " INTEGER NOT NULL DEFAULT 0, "
+                    + COLUMN_ICON + " BLOB, "
+                    + COLUMN_ICON_COLOR + " INTEGER NOT NULL DEFAULT 0, "
+                    + COLUMN_LABEL + " TEXT, "
+                    + COLUMN_SYSTEM_STATE + " TEXT, "
+                    + COLUMN_KEYWORDS + " TEXT, "
+                    + "PRIMARY KEY (" + COLUMN_COMPONENT + ", " + COLUMN_USER + ") "
+                    + ");");
+        }
+    }
+
+    private ContentValues newContentValues(BitmapInfo bitmapInfo, String label,
+            String packageName, @Nullable String keywords) {
+        ContentValues values = new ContentValues();
+        values.put(IconDB.COLUMN_ICON,
+                bitmapInfo.isLowRes() ? null : GraphicsUtils.flattenBitmap(bitmapInfo.icon));
+        values.put(IconDB.COLUMN_ICON_COLOR, bitmapInfo.color);
+
+        values.put(IconDB.COLUMN_LABEL, label);
+        values.put(IconDB.COLUMN_SYSTEM_STATE, getIconSystemState(packageName));
+        values.put(IconDB.COLUMN_KEYWORDS, keywords);
+        return values;
+    }
+
+    private void assertWorkerThread() {
+        if (Looper.myLooper() != mBgLooper) {
+            throw new IllegalStateException("Cache accessed on wrong thread " + Looper.myLooper());
+        }
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
new file mode 100644
index 0000000..c12e9dc
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
@@ -0,0 +1,65 @@
+/*
+ * 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.cache;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.os.LocaleList;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.icons.BitmapInfo;
+
+public interface CachingLogic<T> {
+
+    ComponentName getComponent(T object);
+
+    UserHandle getUser(T object);
+
+    CharSequence getLabel(T object);
+
+    default CharSequence getDescription(T object, CharSequence fallback) {
+        return fallback;
+    }
+
+    @NonNull
+    BitmapInfo loadIcon(Context context, T object);
+
+    /**
+     * Provides a option list of keywords to associate with this object
+     */
+    @Nullable
+    default String getKeywords(T object, LocaleList localeList) {
+        return null;
+    }
+
+    /**
+     * Returns the timestamp the entry was last updated in cache.
+     */
+    default long getLastUpdatedTime(T object, PackageInfo info) {
+        return info.lastUpdateTime;
+    }
+
+    /**
+     * Returns true the object should be added to mem cache; otherwise returns false.
+     */
+    default boolean addToMemCache() {
+        return true;
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java b/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java
new file mode 100644
index 0000000..ee52934
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/HandlerRunnable.java
@@ -0,0 +1,67 @@
+/*
+ * 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.cache;
+
+import android.os.Handler;
+
+/**
+ * A runnable that can be posted to a {@link Handler} which can be canceled.
+ */
+public abstract class HandlerRunnable implements Runnable {
+
+    private final Handler mHandler;
+    private final Runnable mEndRunnable;
+
+    private boolean mEnded = false;
+    private boolean mCanceled = false;
+
+    public HandlerRunnable(Handler handler, Runnable endRunnable) {
+        mHandler = handler;
+        mEndRunnable = endRunnable;
+    }
+
+    /**
+     * Cancels this runnable from being run, only if it has not already run.
+     */
+    public void cancel() {
+        mHandler.removeCallbacks(this);
+        // TODO: This can actually cause onEnd to be called twice if the handler is already running
+        //       this runnable
+        // NOTE: This is currently run on whichever thread the caller is run on.
+        mCanceled = true;
+        onEnd();
+    }
+
+    /**
+     * @return whether this runnable was canceled.
+     */
+    protected boolean isCanceled() {
+        return mCanceled;
+    }
+
+    /**
+     * To be called by the implemention of this runnable. The end callback is done on whichever
+     * thread the caller is calling from.
+     */
+    public void onEnd() {
+        if (!mEnded) {
+            mEnded = true;
+            if (mEndRunnable != null) {
+                mEndRunnable.run();
+            }
+        }
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
new file mode 100644
index 0000000..9e1ad7b
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
@@ -0,0 +1,313 @@
+/*
+ * 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.cache;
+
+import android.content.ComponentName;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+
+import com.android.launcher3.icons.cache.BaseIconCache.IconDB;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.Stack;
+
+/**
+ * Utility class to handle updating the Icon cache
+ */
+public class IconCacheUpdateHandler {
+
+    private static final String TAG = "IconCacheUpdateHandler";
+
+    /**
+     * In this mode, all invalid icons are marked as to-be-deleted in {@link #mItemsToDelete}.
+     * This mode is used for the first run.
+     */
+    private static final boolean MODE_SET_INVALID_ITEMS = true;
+
+    /**
+     * In this mode, any valid icon is removed from {@link #mItemsToDelete}. This is used for all
+     * subsequent runs, which essentially acts as set-union of all valid items.
+     */
+    private static final boolean MODE_CLEAR_VALID_ITEMS = false;
+
+    private static final Object ICON_UPDATE_TOKEN = new Object();
+
+    private final HashMap<String, PackageInfo> mPkgInfoMap;
+    private final BaseIconCache mIconCache;
+
+    private final ArrayMap<UserHandle, Set<String>> mPackagesToIgnore = new ArrayMap<>();
+
+    private final SparseBooleanArray mItemsToDelete = new SparseBooleanArray();
+    private boolean mFilterMode = MODE_SET_INVALID_ITEMS;
+
+    IconCacheUpdateHandler(BaseIconCache cache) {
+        mIconCache = cache;
+
+        mPkgInfoMap = new HashMap<>();
+
+        // Remove all active icon update tasks.
+        mIconCache.mWorkerHandler.removeCallbacksAndMessages(ICON_UPDATE_TOKEN);
+
+        createPackageInfoMap();
+    }
+
+    /**
+     * Sets a package to ignore for processing
+     */
+    public void addPackagesToIgnore(UserHandle userHandle, String packageName) {
+        Set<String> packages = mPackagesToIgnore.get(userHandle);
+        if (packages == null) {
+            packages = new HashSet<>();
+            mPackagesToIgnore.put(userHandle, packages);
+        }
+        packages.add(packageName);
+    }
+
+    private void createPackageInfoMap() {
+        PackageManager pm = mIconCache.mPackageManager;
+        for (PackageInfo info :
+                pm.getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES)) {
+            mPkgInfoMap.put(info.packageName, info);
+        }
+    }
+
+    /**
+     * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
+     * the DB and are updated.
+     * @return The set of packages for which icons have updated.
+     */
+    public <T> void updateIcons(List<T> apps, CachingLogic<T> cachingLogic,
+            OnUpdateCallback onUpdateCallback) {
+        // Filter the list per user
+        HashMap<UserHandle, HashMap<ComponentName, T>> userComponentMap = new HashMap<>();
+        int count = apps.size();
+        for (int i = 0; i < count; i++) {
+            T app = apps.get(i);
+            UserHandle userHandle = cachingLogic.getUser(app);
+            HashMap<ComponentName, T> componentMap = userComponentMap.get(userHandle);
+            if (componentMap == null) {
+                componentMap = new HashMap<>();
+                userComponentMap.put(userHandle, componentMap);
+            }
+            componentMap.put(cachingLogic.getComponent(app), app);
+        }
+
+        for (Entry<UserHandle, HashMap<ComponentName, T>> entry : userComponentMap.entrySet()) {
+            updateIconsPerUser(entry.getKey(), entry.getValue(), cachingLogic, onUpdateCallback);
+        }
+
+        // From now on, clear every valid item from the global valid map.
+        mFilterMode = MODE_CLEAR_VALID_ITEMS;
+    }
+
+    /**
+     * Updates the persistent DB, such that only entries corresponding to {@param apps} remain in
+     * the DB and are updated.
+     * @return The set of packages for which icons have updated.
+     */
+    @SuppressWarnings("unchecked")
+    private <T> void updateIconsPerUser(UserHandle user, HashMap<ComponentName, T> componentMap,
+            CachingLogic<T> cachingLogic, OnUpdateCallback onUpdateCallback) {
+        Set<String> ignorePackages = mPackagesToIgnore.get(user);
+        if (ignorePackages == null) {
+            ignorePackages = Collections.emptySet();
+        }
+        long userSerial = mIconCache.getSerialNumberForUser(user);
+
+        Stack<T> appsToUpdate = new Stack<>();
+
+        try (Cursor c = mIconCache.mIconDb.query(
+                new String[]{IconDB.COLUMN_ROWID, IconDB.COLUMN_COMPONENT,
+                        IconDB.COLUMN_LAST_UPDATED, IconDB.COLUMN_VERSION,
+                        IconDB.COLUMN_SYSTEM_STATE},
+                IconDB.COLUMN_USER + " = ? ",
+                new String[]{Long.toString(userSerial)})) {
+
+            final int indexComponent = c.getColumnIndex(IconDB.COLUMN_COMPONENT);
+            final int indexLastUpdate = c.getColumnIndex(IconDB.COLUMN_LAST_UPDATED);
+            final int indexVersion = c.getColumnIndex(IconDB.COLUMN_VERSION);
+            final int rowIndex = c.getColumnIndex(IconDB.COLUMN_ROWID);
+            final int systemStateIndex = c.getColumnIndex(IconDB.COLUMN_SYSTEM_STATE);
+
+            while (c.moveToNext()) {
+                String cn = c.getString(indexComponent);
+                ComponentName component = ComponentName.unflattenFromString(cn);
+                PackageInfo info = mPkgInfoMap.get(component.getPackageName());
+
+                int rowId = c.getInt(rowIndex);
+                if (info == null) {
+                    if (!ignorePackages.contains(component.getPackageName())) {
+
+                        if (mFilterMode == MODE_SET_INVALID_ITEMS) {
+                            mIconCache.remove(component, user);
+                            mItemsToDelete.put(rowId, true);
+                        }
+                    }
+                    continue;
+                }
+                if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_DATA_ONLY) != 0) {
+                    // Application is not present
+                    continue;
+                }
+
+                long updateTime = c.getLong(indexLastUpdate);
+                int version = c.getInt(indexVersion);
+                T app = componentMap.remove(component);
+                if (version == info.versionCode && updateTime == info.lastUpdateTime
+                        && TextUtils.equals(c.getString(systemStateIndex),
+                                mIconCache.getIconSystemState(info.packageName))) {
+
+                    if (mFilterMode == MODE_CLEAR_VALID_ITEMS) {
+                        mItemsToDelete.put(rowId, false);
+                    }
+                    continue;
+                }
+
+                if (app == null) {
+                    if (mFilterMode == MODE_SET_INVALID_ITEMS) {
+                        mIconCache.remove(component, user);
+                        mItemsToDelete.put(rowId, true);
+                    }
+                } else {
+                    appsToUpdate.add(app);
+                }
+            }
+        } catch (SQLiteException e) {
+            Log.d(TAG, "Error reading icon cache", e);
+            // Continue updating whatever we have read so far
+        }
+
+        // Insert remaining apps.
+        if (!componentMap.isEmpty() || !appsToUpdate.isEmpty()) {
+            Stack<T> appsToAdd = new Stack<>();
+            appsToAdd.addAll(componentMap.values());
+            new SerializedIconUpdateTask(userSerial, user, appsToAdd, appsToUpdate, cachingLogic,
+                    onUpdateCallback).scheduleNext();
+        }
+    }
+
+    /**
+     * Commits all updates as part of the update handler to disk. Not more calls should be made
+     * to this class after this.
+     */
+    public void finish() {
+        // Commit all deletes
+        int deleteCount = 0;
+        StringBuilder queryBuilder = new StringBuilder()
+                .append(IconDB.COLUMN_ROWID)
+                .append(" IN (");
+
+        int count = mItemsToDelete.size();
+        for (int i = 0;  i < count; i++) {
+            if (mItemsToDelete.valueAt(i)) {
+                if (deleteCount > 0) {
+                    queryBuilder.append(", ");
+                }
+                queryBuilder.append(mItemsToDelete.keyAt(i));
+                deleteCount++;
+            }
+        }
+        queryBuilder.append(')');
+
+        if (deleteCount > 0) {
+            mIconCache.mIconDb.delete(queryBuilder.toString(), null);
+        }
+    }
+
+    /**
+     * A runnable that updates invalid icons and adds missing icons in the DB for the provided
+     * LauncherActivityInfo list. Items are updated/added one at a time, so that the
+     * worker thread doesn't get blocked.
+     */
+    private class SerializedIconUpdateTask<T> implements Runnable {
+        private final long mUserSerial;
+        private final UserHandle mUserHandle;
+        private final Stack<T> mAppsToAdd;
+        private final Stack<T> mAppsToUpdate;
+        private final CachingLogic<T> mCachingLogic;
+        private final HashSet<String> mUpdatedPackages = new HashSet<>();
+        private final OnUpdateCallback mOnUpdateCallback;
+
+        SerializedIconUpdateTask(long userSerial, UserHandle userHandle,
+                Stack<T> appsToAdd, Stack<T> appsToUpdate, CachingLogic<T> cachingLogic,
+                OnUpdateCallback onUpdateCallback) {
+            mUserHandle = userHandle;
+            mUserSerial = userSerial;
+            mAppsToAdd = appsToAdd;
+            mAppsToUpdate = appsToUpdate;
+            mCachingLogic = cachingLogic;
+            mOnUpdateCallback = onUpdateCallback;
+        }
+
+        @Override
+        public void run() {
+            if (!mAppsToUpdate.isEmpty()) {
+                T app = mAppsToUpdate.pop();
+                String pkg = mCachingLogic.getComponent(app).getPackageName();
+                PackageInfo info = mPkgInfoMap.get(pkg);
+
+                mIconCache.addIconToDBAndMemCache(
+                        app, mCachingLogic, info, mUserSerial, true /*replace existing*/);
+                mUpdatedPackages.add(pkg);
+
+                if (mAppsToUpdate.isEmpty() && !mUpdatedPackages.isEmpty()) {
+                    // No more app to update. Notify callback.
+                    mOnUpdateCallback.onPackageIconsUpdated(mUpdatedPackages, mUserHandle);
+                }
+
+                // Let it run one more time.
+                scheduleNext();
+            } else if (!mAppsToAdd.isEmpty()) {
+                T app = mAppsToAdd.pop();
+                PackageInfo info = mPkgInfoMap.get(mCachingLogic.getComponent(app).getPackageName());
+                // We do not check the mPkgInfoMap when generating the mAppsToAdd. Although every
+                // app should have package info, this is not guaranteed by the api
+                if (info != null) {
+                    mIconCache.addIconToDBAndMemCache(app, mCachingLogic, info,
+                            mUserSerial, false /*replace existing*/);
+                }
+
+                if (!mAppsToAdd.isEmpty()) {
+                    scheduleNext();
+                }
+            }
+        }
+
+        public void scheduleNext() {
+            mIconCache.mWorkerHandler.postAtTime(this, ICON_UPDATE_TOKEN,
+                    SystemClock.uptimeMillis() + 1);
+        }
+    }
+
+    public interface OnUpdateCallback {
+
+        void onPackageIconsUpdated(HashSet<String> updatedPackages, UserHandle user);
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/util/ComponentKey.java b/iconloaderlib/src/com/android/launcher3/util/ComponentKey.java
new file mode 100644
index 0000000..34bed94
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/util/ComponentKey.java
@@ -0,0 +1,59 @@
+package com.android.launcher3.util;
+
+/**
+ * Copyright (C) 2015 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.
+ */
+
+import android.content.ComponentName;
+import android.os.UserHandle;
+
+import java.util.Arrays;
+
+public class ComponentKey {
+
+    public final ComponentName componentName;
+    public final UserHandle user;
+
+    private final int mHashCode;
+
+    public ComponentKey(ComponentName componentName, UserHandle user) {
+        if (componentName == null || user == null) {
+            throw new NullPointerException();
+        }
+        this.componentName = componentName;
+        this.user = user;
+        mHashCode = Arrays.hashCode(new Object[] {componentName, user});
+
+    }
+
+    @Override
+    public int hashCode() {
+        return mHashCode;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        ComponentKey other = (ComponentKey) o;
+        return other.componentName.equals(componentName) && other.user.equals(user);
+    }
+
+    /**
+     * Encodes a component key as a string of the form [flattenedComponentString#userId].
+     */
+    @Override
+    public String toString() {
+        return componentName.flattenToString() + "#" + user;
+    }
+}
\ No newline at end of file
diff --git a/iconloaderlib/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java b/iconloaderlib/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java
new file mode 100644
index 0000000..fe864a2
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/util/NoLocaleSQLiteHelper.java
@@ -0,0 +1,58 @@
+/*
+ * 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.util;
+
+import static android.database.sqlite.SQLiteDatabase.NO_LOCALIZED_COLLATORS;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.database.DatabaseErrorHandler;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.database.sqlite.SQLiteDatabase.OpenParams;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.os.Build;
+
+/**
+ * Extension of {@link SQLiteOpenHelper} which avoids creating default locale table by
+ * A context wrapper which creates databases without support for localized collators.
+ */
+public abstract class NoLocaleSQLiteHelper extends SQLiteOpenHelper {
+
+    private static final boolean ATLEAST_P =
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
+
+    public NoLocaleSQLiteHelper(Context context, String name, int version) {
+        super(ATLEAST_P ? context : new NoLocalContext(context), name, null, version);
+        if (ATLEAST_P) {
+            setOpenParams(new OpenParams.Builder().addOpenFlags(NO_LOCALIZED_COLLATORS).build());
+        }
+    }
+
+    private static class NoLocalContext extends ContextWrapper {
+        public NoLocalContext(Context base) {
+            super(base);
+        }
+
+        @Override
+        public SQLiteDatabase openOrCreateDatabase(
+                String name, int mode, CursorFactory factory, DatabaseErrorHandler errorHandler) {
+            return super.openOrCreateDatabase(
+                    name, mode | Context.MODE_NO_LOCALIZED_COLLATORS, factory, errorHandler);
+        }
+    }
+}
diff --git a/iconloaderlib/src/com/android/launcher3/util/SQLiteCacheHelper.java b/iconloaderlib/src/com/android/launcher3/util/SQLiteCacheHelper.java
new file mode 100644
index 0000000..49de4bd
--- /dev/null
+++ b/iconloaderlib/src/com/android/launcher3/util/SQLiteCacheHelper.java
@@ -0,0 +1,125 @@
+package com.android.launcher3.util;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteFullException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+/**
+ * An extension of {@link SQLiteOpenHelper} with utility methods for a single table cache DB.
+ * Any exception during write operations are ignored, and any version change causes a DB reset.
+ */
+public abstract class SQLiteCacheHelper {
+    private static final String TAG = "SQLiteCacheHelper";
+
+    private static final boolean IN_MEMORY_CACHE = false;
+
+    private final String mTableName;
+    private final MySQLiteOpenHelper mOpenHelper;
+
+    private boolean mIgnoreWrites;
+
+    public SQLiteCacheHelper(Context context, String name, int version, String tableName) {
+        if (IN_MEMORY_CACHE) {
+            name = null;
+        }
+        mTableName = tableName;
+        mOpenHelper = new MySQLiteOpenHelper(context, name, version);
+
+        mIgnoreWrites = false;
+    }
+
+    /**
+     * @see SQLiteDatabase#delete(String, String, String[])
+     */
+    public void delete(String whereClause, String[] whereArgs) {
+        if (mIgnoreWrites) {
+            return;
+        }
+        try {
+            mOpenHelper.getWritableDatabase().delete(mTableName, whereClause, whereArgs);
+        } catch (SQLiteFullException e) {
+            onDiskFull(e);
+        } catch (SQLiteException e) {
+            Log.d(TAG, "Ignoring sqlite exception", e);
+        }
+    }
+
+    /**
+     * @see SQLiteDatabase#insertWithOnConflict(String, String, ContentValues, int)
+     */
+    public void insertOrReplace(ContentValues values) {
+        if (mIgnoreWrites) {
+            return;
+        }
+        try {
+            mOpenHelper.getWritableDatabase().insertWithOnConflict(
+                    mTableName, null, values, SQLiteDatabase.CONFLICT_REPLACE);
+        } catch (SQLiteFullException e) {
+            onDiskFull(e);
+        } catch (SQLiteException e) {
+            Log.d(TAG, "Ignoring sqlite exception", e);
+        }
+    }
+
+    private void onDiskFull(SQLiteFullException e) {
+        Log.e(TAG, "Disk full, all write operations will be ignored", e);
+        mIgnoreWrites = true;
+    }
+
+    /**
+     * @see SQLiteDatabase#query(String, String[], String, String[], String, String, String)
+     */
+    public Cursor query(String[] columns, String selection, String[] selectionArgs) {
+        return mOpenHelper.getReadableDatabase().query(
+                mTableName, columns, selection, selectionArgs, null, null, null);
+    }
+
+    public void clear() {
+        mOpenHelper.clearDB(mOpenHelper.getWritableDatabase());
+    }
+
+    public void close() {
+        mOpenHelper.close();
+    }
+
+    protected abstract void onCreateTable(SQLiteDatabase db);
+
+    /**
+     * A private inner class to prevent direct DB access.
+     */
+    private class MySQLiteOpenHelper extends NoLocaleSQLiteHelper {
+
+        public MySQLiteOpenHelper(Context context, String name, int version) {
+            super(context, name, version);
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            onCreateTable(db);
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            if (oldVersion != newVersion) {
+                clearDB(db);
+            }
+        }
+
+        @Override
+        public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            if (oldVersion != newVersion) {
+                clearDB(db);
+            }
+        }
+
+        private void clearDB(SQLiteDatabase db) {
+            db.execSQL("DROP TABLE IF EXISTS " + mTableName);
+            onCreate(db);
+        }
+    }
+}
diff --git a/iconloaderlib/src_full_lib/com/android/launcher3/icons/IconFactory.java b/iconloaderlib/src_full_lib/com/android/launcher3/icons/IconFactory.java
new file mode 100644
index 0000000..48f11fd
--- /dev/null
+++ b/iconloaderlib/src_full_lib/com/android/launcher3/icons/IconFactory.java
@@ -0,0 +1,89 @@
+/*
+ * 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.content.Context;
+
+/**
+ * Wrapper class to provide access to {@link BaseIconFactory} and also to provide pool of this class
+ * that are threadsafe.
+ */
+public class IconFactory extends BaseIconFactory {
+
+    private static final Object sPoolSync = new Object();
+    private static IconFactory sPool;
+    private static int sPoolId = 0;
+
+    /**
+     * Return a new Message instance from the global pool. Allows us to
+     * avoid allocating new objects in many cases.
+     */
+    public static IconFactory obtain(Context context) {
+        int poolId;
+        synchronized (sPoolSync) {
+            if (sPool != null) {
+                IconFactory m = sPool;
+                sPool = m.next;
+                m.next = null;
+                return m;
+            }
+            poolId = sPoolId;
+        }
+
+        return new IconFactory(context,
+                context.getResources().getConfiguration().densityDpi,
+                context.getResources().getDimensionPixelSize(R.dimen.default_icon_bitmap_size),
+                poolId);
+    }
+
+    public static void clearPool() {
+        synchronized (sPoolSync) {
+            sPool = null;
+            sPoolId++;
+        }
+    }
+
+    private final int mPoolId;
+
+    private IconFactory next;
+
+    private IconFactory(Context context, int fillResIconDpi, int iconBitmapSize, int poolId) {
+        super(context, fillResIconDpi, iconBitmapSize);
+        mPoolId = poolId;
+    }
+
+    /**
+     * Recycles a LauncherIcons that may be in-use.
+     */
+    public void recycle() {
+        synchronized (sPoolSync) {
+            if (sPoolId != mPoolId) {
+                return;
+            }
+            // Clear any temporary state variables
+            clear();
+
+            next = sPool;
+            sPool = this;
+        }
+    }
+
+    @Override
+    public void close() {
+        recycle();
+    }
+}
diff --git a/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java b/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java
new file mode 100644
index 0000000..1337975
--- /dev/null
+++ b/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java
@@ -0,0 +1,114 @@
+/*
+ * 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 static android.content.Intent.ACTION_MANAGED_PROFILE_ADDED;
+import static android.content.Intent.ACTION_MANAGED_PROFILE_REMOVED;
+
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.SparseLongArray;
+
+import com.android.launcher3.icons.cache.BaseIconCache;
+
+/**
+ * Wrapper class to provide access to {@link BaseIconFactory} and also to provide pool of this class
+ * that are threadsafe.
+ */
+@TargetApi(Build.VERSION_CODES.P)
+public class SimpleIconCache extends BaseIconCache {
+
+    private static SimpleIconCache sIconCache = null;
+    private static final Object CACHE_LOCK = new Object();
+
+    private final SparseLongArray mUserSerialMap = new SparseLongArray(2);
+    private final UserManager mUserManager;
+
+    public SimpleIconCache(Context context, String dbFileName, Looper bgLooper, int iconDpi,
+            int iconPixelSize, boolean inMemoryCache) {
+        super(context, dbFileName, bgLooper, iconDpi, iconPixelSize, inMemoryCache);
+        mUserManager = context.getSystemService(UserManager.class);
+
+        // Listen for user cache changes.
+        IntentFilter filter = new IntentFilter(ACTION_MANAGED_PROFILE_ADDED);
+        filter.addAction(ACTION_MANAGED_PROFILE_REMOVED);
+        context.registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                resetUserCache();
+            }
+        }, filter, null, new Handler(bgLooper), 0);
+    }
+
+    @Override
+    protected long getSerialNumberForUser(UserHandle user) {
+        synchronized (mUserSerialMap) {
+            int index = mUserSerialMap.indexOfKey(user.getIdentifier());
+            if (index >= 0) {
+                return mUserSerialMap.valueAt(index);
+            }
+            long serial = mUserManager.getSerialNumberForUser(user);
+            mUserSerialMap.put(user.getIdentifier(), serial);
+            return serial;
+        }
+    }
+
+    private void resetUserCache() {
+        synchronized (mUserSerialMap) {
+            mUserSerialMap.clear();
+        }
+    }
+
+    @Override
+    protected boolean isInstantApp(ApplicationInfo info) {
+        return info.isInstantApp();
+    }
+
+    @Override
+    protected BaseIconFactory getIconFactory() {
+        return IconFactory.obtain(mContext);
+    }
+
+    public static SimpleIconCache getIconCache(Context context) {
+        synchronized (CACHE_LOCK) {
+            if (sIconCache != null) {
+                return sIconCache;
+            }
+            boolean inMemoryCache =
+                    context.getResources().getBoolean(R.bool.simple_cache_enable_im_memory);
+            String dbFileName = context.getString(R.string.cache_db_name);
+
+            HandlerThread bgThread = new HandlerThread("simple-icon-cache");
+            bgThread.start();
+
+            sIconCache = new SimpleIconCache(context.getApplicationContext(), dbFileName,
+                    bgThread.getLooper(), context.getResources().getConfiguration().densityDpi,
+                    context.getResources().getDimensionPixelSize(R.dimen.default_icon_bitmap_size),
+                    inMemoryCache);
+            return sIconCache;
+        }
+    }
+}