Merge "Removing custom activity dump logic" into ub-launcher3-master
diff --git a/quickstep/recents_ui_overrides/res/layout/predicted_app_icon.xml b/quickstep/recents_ui_overrides/res/layout/predicted_app_icon.xml
new file mode 100644
index 0000000..70a765a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/layout/predicted_app_icon.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<com.android.launcher3.uioverrides.PredictedAppIcon style="@style/BaseIcon.Workspace" />
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
index fe9fd60..0254340 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 
 import android.animation.Animator;
+import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.app.prediction.AppPredictionContext;
 import android.app.prediction.AppPredictionManager;
@@ -40,10 +41,9 @@
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.touch.ItemLongClickListener;
+import com.android.launcher3.uioverrides.PredictedAppIcon;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.ComponentKey;
 
@@ -66,6 +66,9 @@
     //TODO: replace this with AppTargetEvent.ACTION_UNPIN (b/144119543)
     private static final int APPTARGET_ACTION_UNPIN = 4;
 
+    private static final String APP_LOCATION_HOTSEAT = "hotseat";
+    private static final String APP_LOCATION_WORKSPACE = "workspace";
+
     private static final String PREDICTION_CLIENT = "hotseat";
 
     private DropTarget.DragObject mDragObject;
@@ -81,12 +84,14 @@
     private AppPredictor mAppPredictor;
     private AllAppsStore mAllAppsStore;
 
+    private List<PredictedAppIcon.PredictedIconOutlineDrawing> mOutlineDrawings = new ArrayList<>();
+
     public HotseatPredictionController(Launcher launcher) {
         mLauncher = launcher;
         mHotseat = launcher.getHotseat();
         mAllAppsStore = mLauncher.getAppsView().getAppsStore();
         mAllAppsStore.addUpdateListener(this);
-        mDynamicItemCache = new DynamicItemCache(mLauncher, () -> fillGapsWithPrediction(false));
+        mDynamicItemCache = new DynamicItemCache(mLauncher, this::fillGapsWithPrediction);
         mHotSeatItemsCount = mLauncher.getDeviceProfile().inv.numHotseatIcons;
         launcher.getDeviceProfile().inv.addOnChangeListener(this);
         mHotseat.addOnAttachStateChangeListener(this);
@@ -102,16 +107,17 @@
         mLauncher.getDragController().removeDragListener(this);
     }
 
-    /**
-     * Fills gaps in the hotseat with predictions
-     */
-    public void fillGapsWithPrediction(boolean animate) {
+    private void fillGapsWithPrediction() {
+        fillGapsWithPrediction(false, null);
+    }
+
+    private void fillGapsWithPrediction(boolean animate, Runnable callback) {
         if (mDragObject != null) {
             return;
         }
         List<WorkspaceItemInfo> predictedApps = mapToWorkspaceItemInfo(mComponentKeyMappers);
         int predictionIndex = 0;
-        ArrayList<ItemInfo> newItemsToAdd = new ArrayList<>();
+        ArrayList<WorkspaceItemInfo> newItems = new ArrayList<>();
         for (int rank = 0; rank < mHotSeatItemsCount; rank++) {
             View child = mHotseat.getChildAt(
                     mHotseat.getCellXFromOrder(rank),
@@ -129,22 +135,38 @@
             }
 
             WorkspaceItemInfo predictedItem = predictedApps.get(predictionIndex++);
-            if (isPredictedIcon(child)) {
-                BubbleTextView icon = (BubbleTextView) child;
+            if (isPredictedIcon(child) && child.isEnabled()) {
+                PredictedAppIcon icon = (PredictedAppIcon) child;
                 icon.applyFromWorkspaceItem(predictedItem);
+                icon.finishBinding();
             } else {
-                newItemsToAdd.add(predictedItem);
+                newItems.add(predictedItem);
             }
             preparePredictionInfo(predictedItem, rank);
         }
-        mLauncher.bindItems(newItemsToAdd, animate);
-        for (BubbleTextView icon : getPredictedIcons()) {
-            icon.verifyHighRes();
-            icon.setOnLongClickListener((v) -> {
-                PopupContainerWithArrow.showForIcon((BubbleTextView) v);
-                return true;
+        bindItems(newItems, animate, callback);
+    }
+
+    private void bindItems(List<WorkspaceItemInfo> itemsToAdd, boolean animate, Runnable callback) {
+        AnimatorSet animationSet = new AnimatorSet();
+        for (WorkspaceItemInfo item : itemsToAdd) {
+            PredictedAppIcon icon = PredictedAppIcon.createIcon(mHotseat, item);
+            mLauncher.getWorkspace().addInScreenFromBind(icon, item);
+            icon.finishBinding();
+            if (animate) {
+                animationSet.play(ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0.2f, 1));
+            }
+        }
+        if (animate) {
+            animationSet.addListener(new AnimationSuccessListener() {
+                @Override
+                public void onAnimationSuccess(Animator animator) {
+                    if (callback != null) callback.run();
+                }
             });
-            icon.setBackgroundResource(R.drawable.predicted_icon_background);
+            animationSet.start();
+        } else {
+            if (callback != null) callback.run();
         }
     }
 
@@ -179,22 +201,30 @@
                         .build());
         mAppPredictor.registerPredictionUpdates(mLauncher.getMainExecutor(),
                 this::setPredictedApps);
+
         mAppPredictor.requestPredictionUpdate();
     }
 
     private Bundle getAppPredictionContextExtra() {
         Bundle bundle = new Bundle();
-        ViewGroup vg = mHotseat.getShortcutsAndWidgets();
+        bundle.putParcelableArrayList(APP_LOCATION_HOTSEAT,
+                getPinnedAppTargetsInViewGroup((mHotseat.getShortcutsAndWidgets())));
+        bundle.putParcelableArrayList(APP_LOCATION_WORKSPACE, getPinnedAppTargetsInViewGroup(
+                mLauncher.getWorkspace().getScreenWithId(
+                        Workspace.FIRST_SCREEN_ID).getShortcutsAndWidgets()));
+        return bundle;
+    }
+
+    private ArrayList<AppTarget> getPinnedAppTargetsInViewGroup(ViewGroup viewGroup) {
         ArrayList<AppTarget> pinnedApps = new ArrayList<>();
-        for (int i = 0; i < vg.getChildCount(); i++) {
-            View child = vg.getChildAt(i);
+        for (int i = 0; i < viewGroup.getChildCount(); i++) {
+            View child = viewGroup.getChildAt(i);
             if (isPinnedIcon(child)) {
                 WorkspaceItemInfo itemInfo = (WorkspaceItemInfo) child.getTag();
                 pinnedApps.add(getAppTargetFromItemInfo(itemInfo));
             }
         }
-        bundle.putParcelableArrayList("pinned_apps", pinnedApps);
-        return bundle;
+        return pinnedApps;
     }
 
     private void setPredictedApps(List<AppTarget> appTargets) {
@@ -210,7 +240,7 @@
             mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache));
         }
         updateDependencies();
-        fillGapsWithPrediction(false);
+        fillGapsWithPrediction();
     }
 
     private void updateDependencies() {
@@ -219,7 +249,7 @@
     }
 
     private void pinPrediction(ItemInfo info) {
-        BubbleTextView icon = (BubbleTextView) mHotseat.getChildAt(
+        PredictedAppIcon icon = (PredictedAppIcon) mHotseat.getChildAt(
                 mHotseat.getCellXFromOrder(info.rank),
                 mHotseat.getCellYFromOrder(info.rank));
         if (icon == null) {
@@ -230,11 +260,9 @@
                 LauncherSettings.Favorites.CONTAINER_HOTSEAT, workspaceItemInfo.screenId,
                 workspaceItemInfo.cellX, workspaceItemInfo.cellY);
         ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 1, 0.8f, 1).start();
-        icon.reset();
-        icon.applyFromWorkspaceItem(workspaceItemInfo);
-        icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
+        icon.pin(workspaceItemInfo);
         AppTarget appTarget = getAppTargetFromItemInfo(workspaceItemInfo);
-        notifyItemAction(appTarget, AppTargetEvent.ACTION_PIN);
+        notifyItemAction(appTarget, APP_LOCATION_HOTSEAT, AppTargetEvent.ACTION_PIN);
     }
 
     private List<WorkspaceItemInfo> mapToWorkspaceItemInfo(
@@ -265,47 +293,52 @@
         return predictedApps;
     }
 
-    private List<BubbleTextView> getPredictedIcons() {
-        List<BubbleTextView> icons = new ArrayList<>();
+    private List<PredictedAppIcon> getPredictedIcons() {
+        List<PredictedAppIcon> icons = new ArrayList<>();
         ViewGroup vg = mHotseat.getShortcutsAndWidgets();
         for (int i = 0; i < vg.getChildCount(); i++) {
             View child = vg.getChildAt(i);
             if (isPredictedIcon(child)) {
-                icons.add((BubbleTextView) child);
+                icons.add((PredictedAppIcon) child);
             }
         }
         return icons;
     }
 
-    private void removePredictedApps(boolean animate) {
-        for (BubbleTextView icon : getPredictedIcons()) {
-            if (animate) {
-                icon.animate().scaleY(0).scaleX(0).setListener(new AnimationSuccessListener() {
-                    @Override
-                    public void onAnimationSuccess(Animator animator) {
-                        if (icon.getParent() != null) {
-                            mHotseat.removeView(icon);
-                        }
+    private void removePredictedApps(List<PredictedAppIcon.PredictedIconOutlineDrawing> outlines) {
+        for (PredictedAppIcon icon : getPredictedIcons()) {
+            int rank = ((WorkspaceItemInfo) icon.getTag()).rank;
+            outlines.add(new PredictedAppIcon.PredictedIconOutlineDrawing(
+                    mHotseat.getCellXFromOrder(rank), mHotseat.getCellYFromOrder(rank), icon));
+            icon.setEnabled(false);
+            icon.animate().scaleY(0).scaleX(0).setListener(new AnimationSuccessListener() {
+                @Override
+                public void onAnimationSuccess(Animator animator) {
+                    if (icon.getParent() != null) {
+                        mHotseat.removeView(icon);
                     }
-                });
-            } else {
-                if (icon.getParent() != null) {
-                    mHotseat.removeView(icon);
                 }
-            }
+            });
         }
     }
 
-    private void notifyItemAction(AppTarget target, int action) {
+
+    private void notifyItemAction(AppTarget target, String location, int action) {
         if (mAppPredictor != null) {
-            mAppPredictor.notifyAppTargetEvent(new AppTargetEvent.Builder(target, action).build());
+            mAppPredictor.notifyAppTargetEvent(new AppTargetEvent.Builder(target,
+                    action).setLaunchLocation(location).build());
         }
     }
 
     @Override
     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
-        removePredictedApps(true);
+        removePredictedApps(mOutlineDrawings);
         mDragObject = dragObject;
+        if (mOutlineDrawings.isEmpty()) return;
+        for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
+            mHotseat.addDelegatedCellDrawing(outlineDrawing);
+        }
+        mHotseat.invalidate();
     }
 
     @Override
@@ -315,14 +348,29 @@
         }
         ItemInfo dragInfo = mDragObject.dragInfo;
         if (dragInfo instanceof WorkspaceItemInfo && dragInfo.getTargetComponent() != null) {
+            AppTarget appTarget = getAppTargetFromItemInfo(dragInfo);
+            if (!isInHotseat(dragInfo) && isInHotseat(mDragObject.originalDragInfo)) {
+                notifyItemAction(appTarget, APP_LOCATION_HOTSEAT, APPTARGET_ACTION_UNPIN);
+            }
+            if (!isInFirstPage(dragInfo) && isInFirstPage(mDragObject.originalDragInfo)) {
+                notifyItemAction(appTarget, APP_LOCATION_WORKSPACE, APPTARGET_ACTION_UNPIN);
+            }
             if (isInHotseat(dragInfo) && !isInHotseat(mDragObject.originalDragInfo)) {
-                notifyItemAction(getAppTargetFromItemInfo(dragInfo), AppTargetEvent.ACTION_PIN);
-            } else if (!isInHotseat(dragInfo) && isInHotseat(mDragObject.originalDragInfo)) {
-                notifyItemAction(getAppTargetFromItemInfo(dragInfo), APPTARGET_ACTION_UNPIN);
+                notifyItemAction(appTarget, APP_LOCATION_HOTSEAT, AppTargetEvent.ACTION_PIN);
+            }
+            if (isInFirstPage(dragInfo) && !isInFirstPage(mDragObject.originalDragInfo)) {
+                notifyItemAction(appTarget, APP_LOCATION_WORKSPACE, AppTargetEvent.ACTION_PIN);
             }
         }
         mDragObject = null;
-        fillGapsWithPrediction(true);
+        fillGapsWithPrediction(true, () -> {
+            if (mOutlineDrawings.isEmpty()) return;
+            for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
+                mHotseat.removeDelegatedCellDrawing(outlineDrawing);
+            }
+            mHotseat.invalidate();
+            mOutlineDrawings.clear();
+        });
     }
 
     @Nullable
@@ -351,8 +399,7 @@
 
     @Override
     public void onAppsUpdated() {
-        updateDependencies();
-        fillGapsWithPrediction(false);
+        fillGapsWithPrediction();
     }
 
     @Override
@@ -375,7 +422,7 @@
     }
 
     private static boolean isPredictedIcon(View view) {
-        return view instanceof BubbleTextView && view.getTag() instanceof WorkspaceItemInfo
+        return view instanceof PredictedAppIcon && view.getTag() instanceof WorkspaceItemInfo
                 && ((WorkspaceItemInfo) view.getTag()).container
                 == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
     }
@@ -385,7 +432,7 @@
             return false;
         }
         ItemInfo info = (ItemInfo) view.getTag();
-        return info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT && (
+        return info.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION && (
                 info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
                         || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT);
     }
@@ -394,11 +441,15 @@
         return itemInfo.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT;
     }
 
+    private static boolean isInFirstPage(ItemInfo itemInfo) {
+        return itemInfo.container == LauncherSettings.Favorites.CONTAINER_DESKTOP
+                && itemInfo.screenId == Workspace.FIRST_SCREEN_ID;
+    }
+
     private static AppTarget getAppTargetFromItemInfo(ItemInfo info) {
         if (info.getTargetComponent() == null) return null;
-        return new AppTarget.Builder(
-                new AppTargetId("app:" + info.getTargetComponent().getPackageName()),
-                info.getTargetComponent().getPackageName(), info.user).setClassName(
-                info.getTargetComponent().getClassName()).build();
+        ComponentName cn = info.getTargetComponent();
+        return new AppTarget.Builder(new AppTargetId("app:" + cn.getPackageName()),
+                cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
new file mode 100644
index 0000000..1dcbffb
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.uioverrides;
+
+import static com.android.launcher3.graphics.IconShape.getShape;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.DashPathEffect;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.core.graphics.ColorUtils;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.graphics.IconPalette;
+import com.android.launcher3.icons.IconNormalizer;
+import com.android.launcher3.popup.PopupContainerWithArrow;
+import com.android.launcher3.touch.ItemClickHandler;
+import com.android.launcher3.touch.ItemLongClickListener;
+import com.android.launcher3.views.DoubleShadowBubbleTextView;
+
+/**
+ * A BubbleTextView with a ring around it's drawable
+ */
+public class PredictedAppIcon extends DoubleShadowBubbleTextView {
+
+    private static final float RING_EFFECT_RATIO = 0.12f;
+
+    private DeviceProfile mDeviceProfile;
+    private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private boolean mIsPinned = false;
+    private int mNormalizedIconRadius;
+
+
+    public PredictedAppIcon(Context context) {
+        this(context, null, 0);
+    }
+
+    public PredictedAppIcon(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        mDeviceProfile = Launcher.getLauncher(context).getDeviceProfile();
+        mNormalizedIconRadius = IconNormalizer.getNormalizedCircleSize(getIconSize()) / 2;
+        setOnClickListener(ItemClickHandler.INSTANCE);
+        setOnFocusChangeListener(Launcher.getLauncher(context).mFocusHandler);
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        int count = canvas.save();
+        if (!mIsPinned) {
+            drawEffect(canvas);
+            canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO);
+            canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO);
+        }
+        super.onDraw(canvas);
+        canvas.restoreToCount(count);
+    }
+
+    @Override
+    public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
+        super.applyFromWorkspaceItem(info);
+        int color = IconPalette.getMutedColor(info.bitmap.color, 0.54f);
+        mIconRingPaint.setColor(ColorUtils.setAlphaComponent(color, 200));
+    }
+
+    /**
+     * Removes prediction ring from app icon
+     */
+    public void pin(WorkspaceItemInfo info) {
+        if (mIsPinned) return;
+        applyFromWorkspaceItem(info);
+        setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
+        mIsPinned = true;
+        ((CellLayout.LayoutParams) getLayoutParams()).canReorder = true;
+        invalidate();
+    }
+
+    /**
+     * prepares prediction icon for usage after bind
+     */
+    public void finishBinding() {
+        setOnLongClickListener((v) -> {
+            PopupContainerWithArrow.showForIcon((BubbleTextView) v);
+            if (getParent() != null) {
+                getParent().requestDisallowInterceptTouchEvent(true);
+            }
+            return true;
+        });
+        ((CellLayout.LayoutParams) getLayoutParams()).canReorder = false;
+        setTextVisibility(false);
+        verifyHighRes();
+    }
+
+    @Override
+    public void getIconBounds(Rect outBounds) {
+        super.getIconBounds(outBounds);
+        if (!mIsPinned) {
+            int predictionInset = (int) (getIconSize() * RING_EFFECT_RATIO);
+            outBounds.inset(predictionInset, predictionInset);
+        }
+    }
+
+    private int getOutlineOffsetX() {
+        return (getMeasuredWidth() / 2) - mNormalizedIconRadius;
+    }
+
+    private int getOutlineOffsetY() {
+        return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
+    }
+
+    private void drawEffect(Canvas canvas) {
+        getShape().drawShape(canvas, getOutlineOffsetX(), getOutlineOffsetY(),
+                mNormalizedIconRadius, mIconRingPaint);
+    }
+
+    /**
+     * Creates and returns a new instance of PredictedAppIcon from WorkspaceItemInfo
+     */
+    public static PredictedAppIcon createIcon(ViewGroup parent, WorkspaceItemInfo info) {
+        PredictedAppIcon icon = (PredictedAppIcon) LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.predicted_app_icon, parent, false);
+        icon.applyFromWorkspaceItem(info);
+        return icon;
+    }
+
+    /**
+     * Draws Predicted Icon outline on cell layout
+     */
+    public static class PredictedIconOutlineDrawing extends CellLayout.DelegatedCellDrawing {
+
+        private int mOffsetX;
+        private int mOffsetY;
+        private int mIconRadius;
+        private Paint mOutlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+        public PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon) {
+            mDelegateCellX = cellX;
+            mDelegateCellY = cellY;
+            mOffsetX = icon.getOutlineOffsetX();
+            mOffsetY = icon.getOutlineOffsetY();
+            mIconRadius = icon.mNormalizedIconRadius;
+            mOutlinePaint.setStyle(Paint.Style.STROKE);
+            mOutlinePaint.setStrokeWidth(5);
+            mOutlinePaint.setPathEffect(new DashPathEffect(new float[]{15, 15}, 0));
+            mOutlinePaint.setColor(Color.argb(100, 245, 245, 245));
+        }
+
+        /**
+         * Draws predicted app icon outline under CellLayout
+         */
+        @Override
+        public void drawUnderItem(Canvas canvas) {
+            getShape().drawShape(canvas, mOffsetX, mOffsetY, mIconRadius, mOutlinePaint);
+        }
+
+        /**
+         * Draws PredictedAppIcon outline over CellLayout
+         */
+        @Override
+        public void drawOverItem(Canvas canvas) {
+            // Does nothing
+        }
+    }
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index 6e7214e..b602cea 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -177,6 +177,7 @@
     private final float mFastFlingVelocity;
     private final RecentsModel mModel;
     private final int mTaskTopMargin;
+    private final int mTaskBottomMargin;
     private final ClearAllButton mClearAllButton;
     private final Rect mClearAllButtonDeadZoneRect = new Rect();
     private final Rect mTaskViewDeadZoneRect = new Rect();
@@ -343,6 +344,8 @@
         setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
         mTaskTopMargin = getResources()
                 .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
+        mTaskBottomMargin = getResources().getDimensionPixelSize(
+                R.dimen.task_thumbnail_bottom_margin);
         mSquaredTouchSlop = squaredTouchSlop(context);
 
         mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents);
@@ -696,6 +699,7 @@
         mTaskHeight = mTempRect.height();
 
         mTempRect.top -= mTaskTopMargin;
+        mTempRect.bottom += mTaskBottomMargin;
         setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top,
                 dp.widthPx - mInsets.right - mTempRect.right,
                 dp.heightPx - mInsets.bottom - mTempRect.bottom);
@@ -1467,7 +1471,7 @@
 
         // Set the pivot points to match the task preview center
         setPivotY(((mInsets.top + getPaddingTop() + mTaskTopMargin)
-                + (getHeight() - mInsets.bottom - getPaddingBottom())) / 2);
+                + (getHeight() - mInsets.bottom - getPaddingBottom() - mTaskBottomMargin)) / 2);
         setPivotX(((mInsets.left + getPaddingLeft())
                 + (getWidth() - mInsets.right - getPaddingRight())) / 2);
     }
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
index a1775f4..0bfde64 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
@@ -540,8 +540,11 @@
             }
 
             addView(view, indexToAdd);
-            ((LayoutParams) view.getLayoutParams()).gravity =
+            LayoutParams layoutParams = (LayoutParams) view.getLayoutParams();
+            layoutParams.gravity =
                     Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
+            layoutParams.bottomMargin =
+                    ((MarginLayoutParams) mSnapshotView.getLayoutParams()).bottomMargin;
             view.setAlpha(mFooterAlpha);
             mFooters[index] = new FooterWrapper(view);
             if (shouldAnimateEntry) {
@@ -618,10 +621,12 @@
     private static final class TaskOutlineProvider extends ViewOutlineProvider {
 
         private final int mMarginTop;
+        private final int mMarginBottom;
         private FullscreenDrawParams mFullscreenParams;
 
         TaskOutlineProvider(Resources res, FullscreenDrawParams fullscreenParams) {
             mMarginTop = res.getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
+            mMarginBottom = res.getDimensionPixelSize(R.dimen.task_thumbnail_bottom_margin);
             mFullscreenParams = fullscreenParams;
         }
 
@@ -636,7 +641,7 @@
             outline.setRoundRect(0,
                     (int) (mMarginTop * scale),
                     (int) ((insets.left + view.getWidth() + insets.right) * scale),
-                    (int) ((insets.top + view.getHeight() + insets.bottom) * scale),
+                    (int) ((insets.top + view.getHeight() + insets.bottom - mMarginBottom) * scale),
                     mFullscreenParams.mCurrentDrawnCornerRadius);
         }
     }
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index 60cfa0c..7a36416 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -25,7 +25,8 @@
         android:id="@+id/snapshot"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_marginTop="@dimen/task_thumbnail_top_margin"/>
+        android:layout_marginTop="@dimen/task_thumbnail_top_margin"
+        android:layout_marginBottom="@dimen/task_thumbnail_bottom_margin"/>
 
     <com.android.quickstep.views.IconView
         android:id="@+id/icon"
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 78424ca..82833ea 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -18,6 +18,8 @@
 
     <dimen name="task_thumbnail_top_margin">24dp</dimen>
     <dimen name="task_thumbnail_half_top_margin">12dp</dimen>
+    <!-- Can be overridden in overlays. -->
+    <dimen name="task_thumbnail_bottom_margin">0dp</dimen>
     <dimen name="task_thumbnail_icon_size">48dp</dimen>
     <!-- For screens without rounded corners -->
     <dimen name="task_corner_radius_small">2dp</dimen>
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index 2e118b4..c47bb4a 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -105,6 +105,7 @@
         }
 
         float topIconMargin = res.getDimension(R.dimen.task_thumbnail_top_margin);
+        float bottomMargin = res.getDimension(R.dimen.task_thumbnail_bottom_margin);
         float paddingVert = res.getDimension(R.dimen.task_card_vert_space);
 
         // Note this should be same as dp.availableWidthPx and dp.availableHeightPx unless
@@ -113,7 +114,7 @@
         int launcherVisibleHeight = dp.heightPx - insets.top - insets.bottom;
 
         float availableHeight = launcherVisibleHeight
-                - topIconMargin - extraVerticalSpace - paddingVert;
+                - topIconMargin - extraVerticalSpace - paddingVert - bottomMargin;
         float availableWidth = launcherVisibleWidth - paddingHorz;
 
         float scale = Math.min(availableWidth / taskWidth, availableHeight / taskHeight);
diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml
index bc658e4..662b86e 100644
--- a/res/values-land/dimens.xml
+++ b/res/values-land/dimens.xml
@@ -15,9 +15,6 @@
 -->
 
 <resources>
-    <!-- Container -->
-    <item name="container_margin" format="fraction" type="fraction">12%</item>
-
     <!-- Fast scroll -->
     <dimen name="fastscroll_popup_width">58dp</dimen>
     <dimen name="fastscroll_popup_height">48dp</dimen>
diff --git a/res/values/config.xml b/res/values/config.xml
index 2a1f6f7..0dfed97 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -114,4 +114,7 @@
 
     <!-- Recents -->
     <item type="id" name="overview_panel"/>
+
+    <!-- Configuration resources -->
+    <array name="dynamic_resources"> </array>
 </resources>
diff --git a/res/xml/dynamic_resources.xml b/res/xml/dynamic_resources.xml
new file mode 100644
index 0000000..f5d2628
--- /dev/null
+++ b/res/xml/dynamic_resources.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<DynamicResources>
+    <entry id="@color/delete_target_hover_tint" />
+    <entry id="@color/delete_target_hover_tint" />
+    <entry id="@color/delete_target_hover_tint" />
+    <entry id="@color/delete_target_hover_tint" />
+
+
+</DynamicResources>
\ No newline at end of file
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 976ccd5..89bec98 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -110,7 +110,7 @@
 
     private OnTouchListener mInterceptTouchListener;
 
-    private final ArrayList<PreviewBackground> mFolderBackgrounds = new ArrayList<>();
+    private final ArrayList<DelegatedCellDrawing> mDelegatedCellDrawings = new ArrayList<>();
     final PreviewBackground mFolderLeaveBehind = new PreviewBackground();
 
     private static final int[] BACKGROUND_STATE_ACTIVE = new int[] { android.R.attr.state_active };
@@ -219,8 +219,8 @@
         mPreviousReorderDirection[0] = INVALID_DIRECTION;
         mPreviousReorderDirection[1] = INVALID_DIRECTION;
 
-        mFolderLeaveBehind.delegateCellX = -1;
-        mFolderLeaveBehind.delegateCellY = -1;
+        mFolderLeaveBehind.mDelegateCellX = -1;
+        mFolderLeaveBehind.mDelegateCellY = -1;
 
         setAlwaysDrawnWithCacheEnabled(false);
         final Resources res = getResources();
@@ -466,21 +466,18 @@
             }
         }
 
-        for (int i = 0; i < mFolderBackgrounds.size(); i++) {
-            PreviewBackground bg = mFolderBackgrounds.get(i);
-            cellToPoint(bg.delegateCellX, bg.delegateCellY, mTempLocation);
+        for (int i = 0; i < mDelegatedCellDrawings.size(); i++) {
+            DelegatedCellDrawing cellDrawing = mDelegatedCellDrawings.get(i);
+            cellToPoint(cellDrawing.mDelegateCellX, cellDrawing.mDelegateCellY, mTempLocation);
             canvas.save();
             canvas.translate(mTempLocation[0], mTempLocation[1]);
-            bg.drawBackground(canvas);
-            if (!bg.isClipping) {
-                bg.drawBackgroundStroke(canvas);
-            }
+            cellDrawing.drawUnderItem(canvas);
             canvas.restore();
         }
 
-        if (mFolderLeaveBehind.delegateCellX >= 0 && mFolderLeaveBehind.delegateCellY >= 0) {
-            cellToPoint(mFolderLeaveBehind.delegateCellX,
-                    mFolderLeaveBehind.delegateCellY, mTempLocation);
+        if (mFolderLeaveBehind.mDelegateCellX >= 0 && mFolderLeaveBehind.mDelegateCellY >= 0) {
+            cellToPoint(mFolderLeaveBehind.mDelegateCellX,
+                    mFolderLeaveBehind.mDelegateCellY, mTempLocation);
             canvas.save();
             canvas.translate(mTempLocation[0], mTempLocation[1]);
             mFolderLeaveBehind.drawLeaveBehind(canvas);
@@ -492,23 +489,28 @@
     protected void dispatchDraw(Canvas canvas) {
         super.dispatchDraw(canvas);
 
-        for (int i = 0; i < mFolderBackgrounds.size(); i++) {
-            PreviewBackground bg = mFolderBackgrounds.get(i);
-            if (bg.isClipping) {
-                cellToPoint(bg.delegateCellX, bg.delegateCellY, mTempLocation);
-                canvas.save();
-                canvas.translate(mTempLocation[0], mTempLocation[1]);
-                bg.drawBackgroundStroke(canvas);
-                canvas.restore();
-            }
+        for (int i = 0; i < mDelegatedCellDrawings.size(); i++) {
+            DelegatedCellDrawing bg = mDelegatedCellDrawings.get(i);
+            cellToPoint(bg.mDelegateCellX, bg.mDelegateCellY, mTempLocation);
+            canvas.save();
+            canvas.translate(mTempLocation[0], mTempLocation[1]);
+            bg.drawOverItem(canvas);
+            canvas.restore();
         }
     }
 
-    public void addFolderBackground(PreviewBackground bg) {
-        mFolderBackgrounds.add(bg);
+    /**
+     * Add Delegated cell drawing
+     */
+    public void addDelegatedCellDrawing(DelegatedCellDrawing bg) {
+        mDelegatedCellDrawings.add(bg);
     }
-    public void removeFolderBackground(PreviewBackground bg) {
-        mFolderBackgrounds.remove(bg);
+
+    /**
+     * Remove item from DelegatedCellDrawings
+     */
+    public void removeDelegatedCellDrawing(DelegatedCellDrawing bg) {
+        mDelegatedCellDrawings.remove(bg);
     }
 
     public void setFolderLeaveBehindCell(int x, int y) {
@@ -516,14 +518,14 @@
         mFolderLeaveBehind.setup(getContext(), mActivity, null,
                 child.getMeasuredWidth(), child.getPaddingTop());
 
-        mFolderLeaveBehind.delegateCellX = x;
-        mFolderLeaveBehind.delegateCellY = y;
+        mFolderLeaveBehind.mDelegateCellX = x;
+        mFolderLeaveBehind.mDelegateCellY = y;
         invalidate();
     }
 
     public void clearFolderLeaveBehind() {
-        mFolderLeaveBehind.delegateCellX = -1;
-        mFolderLeaveBehind.delegateCellY = -1;
+        mFolderLeaveBehind.mDelegateCellX = -1;
+        mFolderLeaveBehind.mDelegateCellY = -1;
         invalidate();
     }
 
@@ -2744,6 +2746,24 @@
     }
 
     /**
+     * A Delegated cell Drawing for drawing on CellLayout
+     */
+    public abstract static class DelegatedCellDrawing {
+        public int mDelegateCellX;
+        public int mDelegateCellY;
+
+        /**
+         * Draw under CellLayout
+         */
+        public abstract void drawUnderItem(Canvas canvas);
+
+        /**
+         * Draw over CellLayout
+         */
+        public abstract void drawOverItem(Canvas canvas);
+    }
+
+    /**
      * Returns whether an item can be placed in this CellLayout (after rearranging and/or resizing
      * if necessary).
      */
diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java
index 3347b2a..bd48aec 100644
--- a/src/com/android/launcher3/DeleteDropTarget.java
+++ b/src/com/android/launcher3/DeleteDropTarget.java
@@ -113,6 +113,7 @@
     public void onDrop(DragObject d, DragOptions options) {
         if (canRemove(d.dragInfo)) {
             mLauncher.getModelWriter().prepareToUndoDelete();
+            d.dragInfo.container = NO_ID;
         }
         super.onDrop(d, options);
     }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index d445bc9..816d710 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -1641,7 +1641,9 @@
             return false;
         }
 
-        boolean aboveShortcut = (dropOverView.getTag() instanceof WorkspaceItemInfo);
+        boolean aboveShortcut = (dropOverView.getTag() instanceof WorkspaceItemInfo
+                && ((WorkspaceItemInfo) dropOverView.getTag()).container
+                != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION);
         boolean willBecomeShortcut =
                 (info.itemType == ITEM_TYPE_APPLICATION ||
                         info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT ||
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index f4b705e..ae30380 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -69,16 +69,8 @@
      *
      * To add a new flag that can be toggled through the flags UI:
      *
-     * 1. Declare a new ToggleableFlag below. Give it a unique key (e.g. "QSB_ON_FIRST_SCREEN"),
+     * Declare a new ToggleableFlag below. Give it a unique key (e.g. "QSB_ON_FIRST_SCREEN"),
      *    and set a default value for the flag. This will be the default value on Debug builds.
-     *
-     * 2. Add your flag to mTogglableFlags.
-     *
-     * 3. Create a getter method (an 'is' method) for the flag by copying an existing one.
-     *
-     * 4. Create a getter method with the same name in the release flags copy of FeatureFlags.java.
-     *    This should returns a constant (true/false). This will be the value of the flag used on
-     *    release builds.
      */
     // When enabled the promise icon is visible in all apps while installation an app.
     public static final TogglableFlag PROMISE_APPS_IN_ALL_APPS = new TogglableFlag(
diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java
index b2c0ca7..2d177d2 100644
--- a/src/com/android/launcher3/folder/PreviewBackground.java
+++ b/src/com/android/launcher3/folder/PreviewBackground.java
@@ -48,7 +48,7 @@
  * This object represents a FolderIcon preview background. It stores drawing / measurement
  * information, handles drawing, and animation (accept state <--> rest state).
  */
-public class PreviewBackground {
+public class PreviewBackground extends CellLayout.DelegatedCellDrawing {
 
     private static final int CONSUMPTION_ANIMATION_DURATION = 100;
 
@@ -76,8 +76,6 @@
     int basePreviewOffsetY;
 
     private CellLayout mDrawingDelegate;
-    public int delegateCellX;
-    public int delegateCellY;
 
     // When the PreviewBackground is drawn under an icon (for creating a folder) the border
     // should not occlude the icon
@@ -124,6 +122,27 @@
                 }
             };
 
+    /**
+     * Draws folder background under cell layout
+     */
+    @Override
+    public void drawUnderItem(Canvas canvas) {
+        drawBackground(canvas);
+        if (!isClipping) {
+            drawBackgroundStroke(canvas);
+        }
+    }
+
+    /**
+     * Draws folder background on cell layout
+     */
+    @Override
+    public void drawOverItem(Canvas canvas) {
+        if (isClipping) {
+            drawBackgroundStroke(canvas);
+        }
+    }
+
     public void setup(Context context, ActivityContext activity, View invalidateDelegate,
                       int availableSpaceX, int topPadding) {
         mInvalidateDelegate = invalidateDelegate;
@@ -317,19 +336,19 @@
 
     private void delegateDrawing(CellLayout delegate, int cellX, int cellY) {
         if (mDrawingDelegate != delegate) {
-            delegate.addFolderBackground(this);
+            delegate.addDelegatedCellDrawing(this);
         }
 
         mDrawingDelegate = delegate;
-        delegateCellX = cellX;
-        delegateCellY = cellY;
+        mDelegateCellX = cellX;
+        mDelegateCellY = cellY;
 
         invalidate();
     }
 
     private void clearDrawingDelegate() {
         if (mDrawingDelegate != null) {
-            mDrawingDelegate.removeFolderBackground(this);
+            mDrawingDelegate.removeDelegatedCellDrawing(this);
         }
 
         mDrawingDelegate = null;
@@ -395,8 +414,8 @@
         // is saved and restored at the beginning of the animation, since cancelling the
         // existing animation can clear the delgate.
         CellLayout cl = mDrawingDelegate;
-        int cellX = delegateCellX;
-        int cellY = delegateCellY;
+        int cellX = mDelegateCellX;
+        int cellY = mDelegateCellY;
         animateScale(1f, 1f, () -> delegateDrawing(cl, cellX, cellY), this::clearDrawingDelegate);
     }
 
diff --git a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
index a9242f9..3668313 100644
--- a/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
+++ b/src/com/android/launcher3/settings/DeveloperOptionsFragment.java
@@ -15,6 +15,8 @@
  */
 package com.android.launcher3.settings;
 
+import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
+
 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.PLUGIN_CHANGED;
 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.pluginEnabledKey;
 
@@ -24,28 +26,21 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.PackageInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.provider.Settings;
 import android.util.ArrayMap;
-import android.util.ArraySet;
+import android.util.Pair;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 
-import com.android.launcher3.R;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.config.FlagTogglerPrefUi;
-import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
-
-import java.util.List;
-import java.util.Set;
-
 import androidx.preference.Preference;
 import androidx.preference.PreferenceCategory;
 import androidx.preference.PreferenceDataStore;
@@ -54,6 +49,16 @@
 import androidx.preference.PreferenceViewHolder;
 import androidx.preference.SwitchPreference;
 
+import com.android.launcher3.R;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.config.FlagTogglerPrefUi;
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
 /**
  * Dev-build only UI allowing developers to toggle flag settings and plugins.
  * See {@link FeatureFlags}.
@@ -154,44 +159,53 @@
         PackageManager pm = getContext().getPackageManager();
 
         Set<String> pluginActions = manager.getPluginActions();
-        ArrayMap<String, ArraySet<String>> plugins = new ArrayMap<>();
+
+        ArrayMap<Pair<String, String>, ArrayList<Pair<String, ServiceInfo>>> plugins =
+                new ArrayMap<>();
+
+        Set<String> pluginPermissionApps = pm.getPackagesHoldingPermissions(
+                new String[]{PLUGIN_PERMISSION}, MATCH_DISABLED_COMPONENTS)
+                .stream()
+                .map(pi -> pi.packageName)
+                .collect(Collectors.toSet());
+
         for (String action : pluginActions) {
             String name = toName(action);
             List<ResolveInfo> result = pm.queryIntentServices(
-                    new Intent(action), PackageManager.MATCH_DISABLED_COMPONENTS);
+                    new Intent(action), MATCH_DISABLED_COMPONENTS);
             for (ResolveInfo info : result) {
                 String packageName = info.serviceInfo.packageName;
-                if (!plugins.containsKey(packageName)) {
-                    plugins.put(packageName, new ArraySet<>());
+                if (!pluginPermissionApps.contains(packageName)) {
+                    continue;
                 }
-                plugins.get(packageName).add(name);
+
+                Pair<String, String> key = Pair.create(packageName, info.serviceInfo.processName);
+                if (!plugins.containsKey(key)) {
+                    plugins.put(key, new ArrayList<>());
+                }
+                plugins.get(key).add(Pair.create(name, info.serviceInfo));
             }
         }
 
-        List<PackageInfo> apps = pm.getPackagesHoldingPermissions(new String[]{PLUGIN_PERMISSION},
-                PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.GET_SERVICES);
-        PreferenceDataStore enabled = manager.getPluginEnabler();
-        apps.forEach(app -> {
-            if (!plugins.containsKey(app.packageName)) return;
-            SwitchPreference pref = new PluginPreference(prefContext, app, enabled);
-            pref.setSummary("Plugins: " + toString(plugins.get(app.packageName)));
-            mPluginsCategory.addPreference(pref);
+        PreferenceDataStore enabler = manager.getPluginEnabler();
+        plugins.forEach((key, si) -> {
+            String packageName = key.first;
+            List<ComponentName> componentNames = si.stream()
+                    .map(p -> new ComponentName(packageName, p.second.name))
+                    .collect(Collectors.toList());
+            if (!componentNames.isEmpty()) {
+                SwitchPreference pref = new PluginPreference(
+                        prefContext, si.get(0).second.applicationInfo, enabler, componentNames);
+                pref.setSummary("Plugins: "
+                        + si.stream().map(p -> p.first).collect(Collectors.joining(", ")));
+                mPluginsCategory.addPreference(pref);
+            }
         });
     }
 
-    private String toString(ArraySet<String> plugins) {
-        StringBuilder b = new StringBuilder();
-        for (String string : plugins) {
-            if (b.length() != 0) {
-                b.append(", ");
-            }
-            b.append(string);
-        }
-        return b.toString();
-    }
-
     private String toName(String action) {
-        String str = action.replace("com.android.systemui.action.PLUGIN_", "");
+        String str = action.replace("com.android.systemui.action.PLUGIN_", "")
+                .replace("com.android.launcher3.action.PLUGIN_", "");
         StringBuilder b = new StringBuilder();
         for (String s : str.split("_")) {
             if (b.length() != 0) {
@@ -205,18 +219,20 @@
 
     private static class PluginPreference extends SwitchPreference {
         private final boolean mHasSettings;
-        private final PackageInfo mInfo;
         private final PreferenceDataStore mPluginEnabler;
+        private final String mPackageName;
+        private final List<ComponentName> mComponentNames;
 
-        public PluginPreference(Context prefContext, PackageInfo info,
-                PreferenceDataStore pluginEnabler) {
+        PluginPreference(Context prefContext, ApplicationInfo info,
+                PreferenceDataStore pluginEnabler, List<ComponentName> componentNames) {
             super(prefContext);
             PackageManager pm = prefContext.getPackageManager();
             mHasSettings = pm.resolveActivity(new Intent(ACTION_PLUGIN_SETTINGS)
                     .setPackage(info.packageName), 0) != null;
-            mInfo = info;
+            mPackageName = info.packageName;
+            mComponentNames = componentNames;
             mPluginEnabler = pluginEnabler;
-            setTitle(info.applicationInfo.loadLabel(pm));
+            setTitle(info.loadLabel(pm));
             setChecked(isPluginEnabled());
             setWidgetLayoutResource(R.layout.switch_preference_with_settings);
         }
@@ -227,9 +243,7 @@
         }
 
         private boolean isPluginEnabled() {
-            for (int i = 0; i < mInfo.services.length; i++) {
-                ComponentName componentName = new ComponentName(mInfo.packageName,
-                        mInfo.services[i].name);
+            for (ComponentName componentName : mComponentNames) {
                 if (!isEnabled(componentName)) {
                     return false;
                 }
@@ -240,17 +254,14 @@
         @Override
         protected boolean persistBoolean(boolean isEnabled) {
             boolean shouldSendBroadcast = false;
-            for (int i = 0; i < mInfo.services.length; i++) {
-                ComponentName componentName = new ComponentName(mInfo.packageName,
-                        mInfo.services[i].name);
-
+            for (ComponentName componentName : mComponentNames) {
                 if (isEnabled(componentName) != isEnabled) {
                     mPluginEnabler.putBoolean(pluginEnabledKey(componentName), isEnabled);
                     shouldSendBroadcast = true;
                 }
             }
             if (shouldSendBroadcast) {
-                final String pkg = mInfo.packageName;
+                final String pkg = mPackageName;
                 final Intent intent = new Intent(PLUGIN_CHANGED,
                         pkg != null ? Uri.fromParts("package", pkg, null) : null);
                 getContext().sendBroadcast(intent);
@@ -268,8 +279,7 @@
                     : View.GONE);
             holder.findViewById(R.id.settings).setOnClickListener(v -> {
                 ResolveInfo result = v.getContext().getPackageManager().resolveActivity(
-                        new Intent(ACTION_PLUGIN_SETTINGS).setPackage(
-                                mInfo.packageName), 0);
+                        new Intent(ACTION_PLUGIN_SETTINGS).setPackage(mPackageName), 0);
                 if (result != null) {
                     v.getContext().startActivity(new Intent().setComponent(
                             new ComponentName(result.activityInfo.packageName,
@@ -278,7 +288,7 @@
             });
             holder.itemView.setOnLongClickListener(v -> {
                 Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
-                intent.setData(Uri.fromParts("package", mInfo.packageName, null));
+                intent.setData(Uri.fromParts("package", mPackageName, null));
                 getContext().startActivity(intent);
                 return true;
             });
diff --git a/src/com/android/launcher3/util/DynamicResource.java b/src/com/android/launcher3/util/DynamicResource.java
new file mode 100644
index 0000000..8a75767
--- /dev/null
+++ b/src/com/android/launcher3/util/DynamicResource.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import android.content.Context;
+
+import androidx.annotation.ColorRes;
+import androidx.annotation.DimenRes;
+import androidx.annotation.FractionRes;
+import androidx.annotation.IntegerRes;
+
+import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
+import com.android.systemui.plugins.PluginListener;
+import com.android.systemui.plugins.ResourceProvider;
+
+/**
+ * Utility class to support customizing resource values using plugins
+ *
+ * To load resources, call
+ *    DynamicResource.provider(context).getInt(resId) or any other supported methods
+ *
+ * To allow customization for a particular resource, add them to dynamic_resources.xml
+ */
+public class DynamicResource implements ResourceProvider, PluginListener<ResourceProvider> {
+
+    private static final MainThreadInitializedObject<DynamicResource> INSTANCE =
+            new MainThreadInitializedObject<>(DynamicResource::new);
+
+    private final Context mContext;
+    private ResourceProvider mPlugin;
+
+    private DynamicResource(Context context) {
+        mContext = context;
+        PluginManagerWrapper.INSTANCE.get(context).addPluginListener(this,
+                ResourceProvider.class, false /* allowedMultiple */);
+    }
+
+    @Override
+    public int getInt(@IntegerRes int resId) {
+        return mContext.getResources().getInteger(resId);
+    }
+
+    @Override
+    public float getFraction(@FractionRes int resId) {
+        return mContext.getResources().getFraction(resId, 1, 1);
+    }
+
+    @Override
+    public float getDimension(@DimenRes int resId) {
+        return mContext.getResources().getDimension(resId);
+    }
+
+    @Override
+    public int getColor(@ColorRes int resId) {
+        return mContext.getResources().getColor(resId, null);
+    }
+
+    @Override
+    public void onPluginConnected(ResourceProvider plugin, Context context) {
+        mPlugin = plugin;
+    }
+
+    @Override
+    public void onPluginDisconnected(ResourceProvider plugin) {
+        mPlugin = null;
+    }
+
+    /**
+     * Returns the currently active or default provider
+     */
+    public static ResourceProvider provider(Context context) {
+        DynamicResource dr = DynamicResource.INSTANCE.get(context);
+        ResourceProvider plugin = dr.mPlugin;
+        return plugin == null ? dr : plugin;
+    }
+}
diff --git a/src_plugins/com/android/systemui/plugins/ResourceProvider.java b/src_plugins/com/android/systemui/plugins/ResourceProvider.java
new file mode 100644
index 0000000..eaed9e7
--- /dev/null
+++ b/src_plugins/com/android/systemui/plugins/ResourceProvider.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.plugins;
+
+import com.android.systemui.plugins.annotations.ProvidesInterface;
+
+/**
+ * Plugin to support customizing resource
+ */
+@ProvidesInterface(action = ResourceProvider.ACTION, version = ResourceProvider.VERSION)
+public interface ResourceProvider extends Plugin {
+    String ACTION = "com.android.launcher3.action.PLUGIN_DYNAMIC_RESOURCE";
+    int VERSION = 1;
+
+    /**
+     * @see android.content.res.Resources#getInteger(int)
+     */
+    int getInt(int resId);
+
+    /**
+     * @see android.content.res.Resources#getFraction(int, int, int)
+     */
+    float getFraction(int resId);
+
+    /**
+     * @see android.content.res.Resources#getDimension(int)
+     */
+    float getDimension(int resId);
+
+    /**
+     * @see android.content.res.Resources#getColor(int)
+     */
+    int getColor(int resId);
+}