Merge "Allows a plugin Settings activity to designate plugins it supports." into sc-dev
diff --git a/quickstep/res/layout/taskbar.xml b/quickstep/res/layout/taskbar.xml
index 5f1046d..b124b33 100644
--- a/quickstep/res/layout/taskbar.xml
+++ b/quickstep/res/layout/taskbar.xml
@@ -24,6 +24,8 @@
         android:id="@+id/taskbar_view"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:background="@color/taskbar_background"/>
+        android:background="@color/taskbar_background"
+        android:gravity="center"
+        android:animateLayoutChanges="true"/>
 
 </com.android.launcher3.taskbar.TaskbarContainerView>
\ No newline at end of file
diff --git a/quickstep/res/layout/taskbar_app_icon.xml b/quickstep/res/layout/taskbar_app_icon.xml
new file mode 100644
index 0000000..6fefdb6
--- /dev/null
+++ b/quickstep/res/layout/taskbar_app_icon.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<com.android.launcher3.views.DoubleShadowBubbleTextView style="@style/BaseIcon.Workspace.Taskbar" />
diff --git a/quickstep/res/layout/taskbar_predicted_app_icon.xml b/quickstep/res/layout/taskbar_predicted_app_icon.xml
new file mode 100644
index 0000000..211ebc8
--- /dev/null
+++ b/quickstep/res/layout/taskbar_predicted_app_icon.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<com.android.launcher3.uioverrides.PredictedAppIcon style="@style/BaseIcon.Workspace.Taskbar" />
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 4272f50..39cc0b8 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -122,4 +122,9 @@
 
     <!-- Taskbar -->
     <dimen name="taskbar_size">48dp</dimen>
+    <dimen name="taskbar_icon_size">32dp</dimen>
+    <dimen name="taskbar_icon_touch_size">48dp</dimen>
+    <dimen name="taskbar_icon_drag_icon_size">54dp</dimen>
+    <!-- Note that this applies to both sides of all icons, so visible space is double this. -->
+    <dimen name="taskbar_icon_spacing">14dp</dimen>
 </resources>
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index 8d054b4..5a353f0 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -85,4 +85,10 @@
         <item name="android:drawablePadding">8dp</item>
         <item name="android:textAllCaps">false</item>
     </style>
+
+    <!-- Icon displayed on the taskbar -->
+    <style name="BaseIcon.Workspace.Taskbar" >
+        <item name="iconDisplay">taskbar</item>
+        <item name="iconSizeOverride">@dimen/taskbar_icon_size</item>
+    </style>
 </resources>
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 09a3cfd..cbe0eb0 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -265,6 +265,11 @@
     }
 
     @Override
+    public boolean isViewInTaskbar(View v) {
+        return mTaskbarController != null && mTaskbarController.isViewInTaskbar(v);
+    }
+
+    @Override
     public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) {
         QuickstepAppTransitionManagerImpl appTransitionManager =
                 (QuickstepAppTransitionManagerImpl) getAppTransitionManager();
@@ -350,6 +355,10 @@
         // populating workspace.
         // TODO: Find a better place for this
         WellbeingModel.INSTANCE.get(this);
+
+        if (mTaskbarController != null) {
+            mTaskbarController.onHotseatUpdated();
+        }
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
index 36c8bb8..876cabc 100644
--- a/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/QuickstepAppTransitionManagerImpl.java
@@ -202,9 +202,10 @@
     }
 
     @Override
-    public boolean supportsAdaptiveIconAnimation() {
+    public boolean supportsAdaptiveIconAnimation(View clickedView) {
         return hasControlRemoteAppTransitionPermission()
-                && FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM.get();
+                && FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM.get()
+                && !mLauncher.isViewInTaskbar(clickedView);
     }
 
     /**
@@ -972,9 +973,12 @@
                     launcherIsATargetWithMode(appTargets, MODE_CLOSING);
 
             final boolean launchingFromRecents = isLaunchingFromRecents(mV, appTargets);
+            final boolean launchingFromTaskbar = mLauncher.isViewInTaskbar(mV);
             if (launchingFromRecents) {
                 composeRecentsLaunchAnimator(anim, mV, appTargets, wallpaperTargets,
                         launcherClosing);
+            } else if (launchingFromTaskbar) {
+                // TODO
             } else {
                 composeIconLaunchAnimator(anim, mV, appTargets, wallpaperTargets,
                         launcherClosing);
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index b2de4c9..aa6601b 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -37,7 +37,6 @@
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
@@ -75,7 +74,7 @@
 
     private int mHotSeatItemsCount;
 
-    private Launcher mLauncher;
+    private QuickstepLauncher mLauncher;
     private final Hotseat mHotseat;
 
     private List<ItemInfo> mPredictedItems = Collections.emptyList();
@@ -108,7 +107,7 @@
         return true;
     };
 
-    public HotseatPredictionController(Launcher launcher) {
+    public HotseatPredictionController(QuickstepLauncher launcher) {
         mLauncher = launcher;
         mHotseat = launcher.getHotseat();
         mHotSeatItemsCount = mLauncher.getDeviceProfile().inv.numHotseatIcons;
@@ -229,6 +228,10 @@
         } else {
             if (callback != null) callback.run();
         }
+
+        if (mLauncher.getTaskbarController() != null) {
+            mLauncher.getTaskbarController().onHotseatUpdated();
+        }
     }
 
     /**
@@ -242,6 +245,10 @@
      * start and pauses predicted apps update on the hotseat
      */
     public void setPauseUIUpdate(boolean paused) {
+        if (mLauncher.getTaskbarController() != null) {
+            // Taskbar is present, always allow updates since hotseat is still visible.
+            return;
+        }
         mUIUpdatePaused = paused;
         if (!paused) {
             fillGapsWithPrediction();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
index bdf7f8a..7608645 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarController.java
@@ -26,6 +26,8 @@
 import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
 import android.view.WindowManager;
 
 import androidx.annotation.Nullable;
@@ -36,7 +38,9 @@
 import com.android.launcher3.R;
 import com.android.launcher3.anim.AlphaUpdateListener;
 import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.touch.ItemClickHandler;
 import com.android.quickstep.AnimatedFloat;
 import com.android.systemui.shared.system.WindowManagerWrapper;
 
@@ -55,6 +59,8 @@
     private final Point mTaskbarSize;
     private final TaskbarStateHandler mTaskbarStateHandler;
     private final TaskbarVisibilityController mTaskbarVisibilityController;
+    private final TaskbarHotseatController mHotseatController;
+    private final TaskbarDragController mDragController;
 
     // Initialized in init().
     private WindowManager.LayoutParams mWindowLayoutParams;
@@ -64,12 +70,16 @@
         mLauncher = launcher;
         mTaskbarContainerView = taskbarContainerView;
         mTaskbarView = mTaskbarContainerView.findViewById(R.id.taskbar_view);
+        mTaskbarView.setCallbacks(createTaskbarViewCallbacks());
         mWindowManager = mLauncher.getWindowManager();
         mTaskbarSize = new Point(MATCH_PARENT,
                 mLauncher.getResources().getDimensionPixelSize(R.dimen.taskbar_size));
         mTaskbarStateHandler = mLauncher.getTaskbarStateHandler();
         mTaskbarVisibilityController = new TaskbarVisibilityController(mLauncher,
                 createTaskbarVisibilityControllerCallbacks());
+        mHotseatController = new TaskbarHotseatController(mLauncher,
+                createTaskbarHotseatControllerCallbacks());
+        mDragController = new TaskbarDragController(mLauncher);
     }
 
     private TaskbarVisibilityControllerCallbacks createTaskbarVisibilityControllerCallbacks() {
@@ -87,13 +97,38 @@
         };
     }
 
+    private TaskbarViewCallbacks createTaskbarViewCallbacks() {
+        return new TaskbarViewCallbacks() {
+            @Override
+            public View.OnClickListener getItemOnClickListener() {
+                return ItemClickHandler.INSTANCE;
+            }
+
+            @Override
+            public View.OnLongClickListener getItemOnLongClickListener() {
+                return mDragController::startDragOnLongClick;
+            }
+        };
+    }
+
+    private TaskbarHotseatControllerCallbacks createTaskbarHotseatControllerCallbacks() {
+        return new TaskbarHotseatControllerCallbacks() {
+            @Override
+            public void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
+                mTaskbarView.updateHotseatItems(hotseatItemInfos);
+            }
+        };
+    }
+
     /**
      * Initializes the Taskbar, including adding it to the screen.
      */
     public void init() {
+        mTaskbarView.init(mHotseatController.getNumHotseatIcons());
         addToWindowManager();
         mTaskbarStateHandler.setTaskbarCallbacks(createTaskbarStateHandlerCallbacks());
         mTaskbarVisibilityController.init();
+        mHotseatController.init();
     }
 
     private TaskbarStateHandlerCallbacks createTaskbarStateHandlerCallbacks() {
@@ -109,9 +144,11 @@
      * Removes the Taskbar from the screen, and removes any obsolete listeners etc.
      */
     public void cleanup() {
+        mTaskbarView.cleanup();
         removeFromWindowManager();
         mTaskbarStateHandler.setTaskbarCallbacks(null);
         mTaskbarVisibilityController.cleanup();
+        mHotseatController.cleanup();
     }
 
     private void removeFromWindowManager() {
@@ -191,6 +228,32 @@
     }
 
     /**
+     * Should be called when one or more items in the Hotseat have changed.
+     */
+    public void onHotseatUpdated() {
+        mHotseatController.onHotseatUpdated();
+    }
+
+    /**
+     * @param ev MotionEvent in screen coordinates.
+     * @return Whether any Taskbar item could handle the given MotionEvent if given the chance.
+     */
+    public boolean isEventOverAnyTaskbarItem(MotionEvent ev) {
+        return mTaskbarView.isEventOverAnyItem(ev);
+    }
+
+    public boolean isDraggingItem() {
+        return mTaskbarView.isDraggingItem();
+    }
+
+    /**
+     * @return Whether the given View is in the same window as Taskbar.
+     */
+    public boolean isViewInTaskbar(View v) {
+        return mTaskbarContainerView.getWindowId().equals(v.getWindowId());
+    }
+
+    /**
      * Contains methods that TaskbarStateHandler can call to interface with TaskbarController.
      */
     protected interface TaskbarStateHandlerCallbacks {
@@ -205,4 +268,19 @@
         void updateTaskbarBackgroundAlpha(float alpha);
         void updateTaskbarVisibilityAlpha(float alpha);
     }
+
+    /**
+     * Contains methods that TaskbarView can call to interface with TaskbarController.
+     */
+    protected interface TaskbarViewCallbacks {
+        View.OnClickListener getItemOnClickListener();
+        View.OnLongClickListener getItemOnLongClickListener();
+    }
+
+    /**
+     * Contains methods that TaskbarHotseatController can call to interface with TaskbarController.
+     */
+    protected interface TaskbarHotseatControllerCallbacks {
+        void updateHotseatItems(ItemInfo[] hotseatItemInfos);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
new file mode 100644
index 0000000..2318ff9
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.Intent;
+import android.content.pm.LauncherApps;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.view.DragEvent;
+import android.view.View;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.LauncherSettings;
+import com.android.launcher3.R;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.systemui.shared.system.ClipDescriptionCompat;
+import com.android.systemui.shared.system.LauncherAppsCompat;
+
+/**
+ * Handles long click on Taskbar items to start a system drag and drop operation.
+ */
+public class TaskbarDragController {
+
+    private final BaseQuickstepLauncher mLauncher;
+    private final int mDragIconSize;
+
+    public TaskbarDragController(BaseQuickstepLauncher launcher) {
+        mLauncher = launcher;
+        Resources resources = mLauncher.getResources();
+        mDragIconSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_drag_icon_size);
+    }
+
+    /**
+     * Attempts to start a system drag and drop operation for the given View, using its tag to
+     * generate the ClipDescription and Intent.
+     * @return Whether {@link View#startDragAndDrop} started successfully.
+     */
+    protected boolean startDragOnLongClick(View view) {
+        if (!(view instanceof BubbleTextView)) {
+            return false;
+        }
+
+        BubbleTextView btv = (BubbleTextView) view;
+
+        View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view) {
+            @Override
+            public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
+                shadowSize.set(mDragIconSize, mDragIconSize);
+                // TODO: should be based on last touch point on the icon.
+                shadowTouchPoint.set(shadowSize.x / 2, shadowSize.y / 2);
+            }
+
+            @Override
+            public void onDrawShadow(Canvas canvas) {
+                canvas.save();
+                float scale = (float) mDragIconSize / btv.getIconSize();
+                canvas.scale(scale, scale);
+                btv.getIcon().draw(canvas);
+                canvas.restore();
+            }
+        };
+
+        Object tag = view.getTag();
+        ClipDescription clipDescription = null;
+        Intent intent = null;
+        if (tag instanceof WorkspaceItemInfo) {
+            WorkspaceItemInfo item = (WorkspaceItemInfo) tag;
+            LauncherApps launcherApps = mLauncher.getSystemService(LauncherApps.class);
+            clipDescription = new ClipDescription(item.title,
+                    new String[] {
+                            item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+                                    ? ClipDescriptionCompat.MIMETYPE_APPLICATION_SHORTCUT
+                                    : ClipDescriptionCompat.MIMETYPE_APPLICATION_ACTIVITY
+                    });
+            intent = new Intent();
+            if (item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+                intent.putExtra(Intent.EXTRA_PACKAGE_NAME, item.getIntent().getPackage());
+                intent.putExtra(Intent.EXTRA_SHORTCUT_ID, item.getDeepShortcutId());
+            } else {
+                intent.putExtra(ClipDescriptionCompat.EXTRA_PENDING_INTENT,
+                        LauncherAppsCompat.getMainActivityLaunchIntent(launcherApps,
+                                item.getIntent().getComponent(), null, item.user));
+            }
+            intent.putExtra(Intent.EXTRA_USER, item.user);
+        }
+
+        if (clipDescription != null && intent != null) {
+            ClipData clipData = new ClipData(clipDescription, new ClipData.Item(intent));
+            view.setOnDragListener(getDraggedViewDragListener());
+            return view.startDragAndDrop(clipData, shadowBuilder, null /* localState */,
+                    View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_OPAQUE);
+        }
+        return false;
+    }
+
+    /**
+     * Hide the original Taskbar item while it is being dragged.
+     */
+    private View.OnDragListener getDraggedViewDragListener() {
+        return (view, dragEvent) -> {
+            switch (dragEvent.getAction()) {
+                case DragEvent.ACTION_DRAG_STARTED:
+                    view.setVisibility(INVISIBLE);
+                    return true;
+                case DragEvent.ACTION_DRAG_ENDED:
+                    view.setVisibility(VISIBLE);
+                    view.setOnDragListener(null);
+                    return true;
+            }
+            return false;
+        };
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java
new file mode 100644
index 0000000..4dc051a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.taskbar;
+
+import android.view.View;
+
+import com.android.launcher3.BaseQuickstepLauncher;
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.ShortcutAndWidgetContainer;
+import com.android.launcher3.dragndrop.DragController;
+import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.model.data.ItemInfo;
+
+/**
+ * Works with TaskbarController to update the TaskbarView's Hotseat items.
+ */
+public class TaskbarHotseatController {
+
+    private final BaseQuickstepLauncher mLauncher;
+    private final Hotseat mHotseat;
+    private final TaskbarController.TaskbarHotseatControllerCallbacks mTaskbarCallbacks;
+    private final int mNumHotseatIcons;
+
+    private final DragController.DragListener mDragListener = new DragController.DragListener() {
+        @Override
+        public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
+        }
+
+        @Override
+        public void onDragEnd() {
+            onHotseatUpdated();
+        }
+    };
+
+    public TaskbarHotseatController(BaseQuickstepLauncher launcher,
+            TaskbarController.TaskbarHotseatControllerCallbacks taskbarCallbacks) {
+        mLauncher = launcher;
+        mHotseat = mLauncher.getHotseat();
+        mTaskbarCallbacks = taskbarCallbacks;
+        mNumHotseatIcons = mLauncher.getDeviceProfile().inv.numHotseatIcons;
+    }
+
+    protected void init() {
+        mLauncher.getDragController().addDragListener(mDragListener);
+    }
+
+    protected void cleanup() {
+        mLauncher.getDragController().removeDragListener(mDragListener);
+    }
+
+    /**
+     * Called when any Hotseat item changes, and reports the new list of items to TaskbarController.
+     */
+    protected void onHotseatUpdated() {
+        ShortcutAndWidgetContainer shortcutsAndWidgets = mHotseat.getShortcutsAndWidgets();
+        ItemInfo[] hotseatItemInfos = new ItemInfo[mNumHotseatIcons];
+        for (int i = 0; i < shortcutsAndWidgets.getChildCount(); i++) {
+            View child = shortcutsAndWidgets.getChildAt(i);
+            Object tag = shortcutsAndWidgets.getChildAt(i).getTag();
+            if (tag instanceof ItemInfo) {
+                ItemInfo itemInfo = (ItemInfo) tag;
+                CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
+                // Since the hotseat might be laid out vertically or horizontally, use whichever
+                // index is higher.
+                hotseatItemInfos[Math.max(lp.cellX, lp.cellY)] = itemInfo;
+            }
+        }
+
+        mTaskbarCallbacks.updateHotseatItems(hotseatItemInfos);
+    }
+
+    protected int getNumHotseatIcons() {
+        return mNumHotseatIcons;
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index bf6e946..c98f09c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -16,19 +16,50 @@
 package com.android.launcher3.taskbar;
 
 import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.RectF;
 import android.graphics.drawable.ColorDrawable;
 import android.util.AttributeSet;
+import android.view.DragEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
 import android.widget.LinearLayout;
 
+import androidx.annotation.LayoutRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.R;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+
 /**
  * Hosts the Taskbar content such as Hotseat and Recent Apps. Drawn on top of other apps.
  */
 public class TaskbarView extends LinearLayout {
 
     private final ColorDrawable mBackgroundDrawable;
+    private final int mItemMarginLeftRight;
+    private final int mIconTouchSize;
+    private final int mTouchSlop;
+    private final RectF mTempDelegateBounds = new RectF();
+    private final RectF mDelegateSlopBounds = new RectF();
+    private final int[] mTempOutLocation = new int[2];
+
+    // Initialized in init().
+    private int mHotseatStartIndex;
+    private int mHotseatEndIndex;
+
+    private TaskbarController.TaskbarViewCallbacks mControllerCallbacks;
+
+    // Delegate touches to the closest view if within mIconTouchSize.
+    private boolean mDelegateTargeted;
+    private View mDelegateView;
+
+    private boolean mIsDraggingItem;
 
     public TaskbarView(@NonNull Context context) {
         this(context, null);
@@ -46,7 +77,26 @@
     public TaskbarView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
+
+        Resources resources = getResources();
         mBackgroundDrawable = (ColorDrawable) getBackground();
+        mItemMarginLeftRight = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing);
+        mIconTouchSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_touch_size);
+        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+    }
+
+    protected void setCallbacks(TaskbarController.TaskbarViewCallbacks taskbarViewCallbacks) {
+        mControllerCallbacks = taskbarViewCallbacks;
+    }
+
+    protected void init(int numHotseatIcons) {
+        mHotseatStartIndex = 0;
+        mHotseatEndIndex = mHotseatStartIndex + numHotseatIcons - 1;
+        updateHotseatItems(new ItemInfo[numHotseatIcons]);
+    }
+
+    protected void cleanup() {
+        removeAllViews();
     }
 
     /**
@@ -56,4 +106,159 @@
     public void setBackgroundAlpha(float alpha) {
         mBackgroundDrawable.setAlpha((int) (alpha * 255));
     }
+
+    /**
+     * Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos.
+     */
+    protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
+        for (int i = 0; i < hotseatItemInfos.length; i++) {
+            ItemInfo hotseatItemInfo = hotseatItemInfos[i];
+            int hotseatIndex = mHotseatStartIndex + i;
+            View hotseatView = getChildAt(hotseatIndex);
+
+            // Replace any Hotseat views with the appropriate type if it's not already that type.
+            final int expectedLayoutResId;
+            if (hotseatItemInfo != null && hotseatItemInfo.isPredictedItem()) {
+                expectedLayoutResId = R.layout.taskbar_predicted_app_icon;
+            } else {
+                expectedLayoutResId = R.layout.taskbar_app_icon;
+            }
+            if (hotseatView == null || hotseatView.getSourceLayoutResId() != expectedLayoutResId) {
+                removeView(hotseatView);
+                BubbleTextView btv = (BubbleTextView) inflate(expectedLayoutResId);
+                LayoutParams lp = new LayoutParams(btv.getIconSize(), btv.getIconSize());
+                lp.setMargins(mItemMarginLeftRight, 0, mItemMarginLeftRight, 0);
+                hotseatView = btv;
+                addView(hotseatView, hotseatIndex, lp);
+            }
+
+            // Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
+            if (hotseatView instanceof BubbleTextView
+                    && hotseatItemInfo instanceof WorkspaceItemInfo) {
+                ((BubbleTextView) hotseatView).applyFromWorkspaceItem(
+                        (WorkspaceItemInfo) hotseatItemInfo);
+                hotseatView.setVisibility(VISIBLE);
+                hotseatView.setOnClickListener(mControllerCallbacks.getItemOnClickListener());
+                hotseatView.setOnLongClickListener(
+                        mControllerCallbacks.getItemOnLongClickListener());
+            } else {
+                hotseatView.setVisibility(GONE);
+                hotseatView.setOnClickListener(null);
+                hotseatView.setOnLongClickListener(null);
+            }
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        boolean handled = delegateTouchIfNecessary(event);
+        return super.onTouchEvent(event) || handled;
+    }
+
+    /**
+     * User touched the Taskbar background. Determine whether the touch is close enough to a view
+     * that we should forward the touches to it.
+     * @return Whether a delegate view was chosen and it handled the touch event.
+     */
+    private boolean delegateTouchIfNecessary(MotionEvent event) {
+        final float x = event.getX();
+        final float y = event.getY();
+        if (mDelegateView == null && event.getAction() == MotionEvent.ACTION_DOWN) {
+            View delegateView = findDelegateView(x, y);
+            if (delegateView != null) {
+                mDelegateTargeted = true;
+                mDelegateView = delegateView;
+                mDelegateSlopBounds.set(mTempDelegateBounds);
+                mDelegateSlopBounds.inset(-mTouchSlop, -mTouchSlop);
+            }
+        }
+
+        boolean sendToDelegate = mDelegateTargeted;
+        boolean inBounds = true;
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_MOVE:
+                inBounds = mDelegateSlopBounds.contains(x, y);
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                mDelegateTargeted = false;
+                break;
+        }
+
+        boolean handled = false;
+        if (sendToDelegate) {
+            if (inBounds) {
+                // Offset event coordinates to be inside the target view
+                event.setLocation(mDelegateView.getWidth() / 2f, mDelegateView.getHeight() / 2f);
+            } else {
+                // Offset event coordinates to be outside the target view (in case it does
+                // something like tracking pressed state)
+                event.setLocation(-mTouchSlop * 2, -mTouchSlop * 2);
+            }
+            handled = mDelegateView.dispatchTouchEvent(event);
+            // Cleanup if this was the last event to send to the delegate.
+            if (!mDelegateTargeted) {
+                mDelegateView = null;
+            }
+        }
+        return handled;
+    }
+
+    /**
+     * Return an item whose touch bounds contain the given coordinates,
+     * or null if no such item exists.
+     *
+     * Also sets {@link #mTempDelegateBounds} to be the touch bounds of the chosen delegate view.
+     */
+    private @Nullable View findDelegateView(float x, float y) {
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            if (!child.isShown() || !child.isClickable()) {
+                continue;
+            }
+            int childCenterX = child.getLeft() + child.getWidth() / 2;
+            int childCenterY = child.getTop() + child.getHeight() / 2;
+            mTempDelegateBounds.set(
+                    childCenterX - mIconTouchSize / 2f,
+                    childCenterY - mIconTouchSize / 2f,
+                    childCenterX + mIconTouchSize / 2f,
+                    childCenterY + mIconTouchSize / 2f);
+            if (mTempDelegateBounds.contains(x, y)) {
+                return child;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's
+     * touch bounds.
+     */
+    public boolean isEventOverAnyItem(MotionEvent ev) {
+        getLocationOnScreen(mTempOutLocation);
+        float xInOurCoordinates = ev.getX() - mTempOutLocation[0];
+        float yInOurCoorindates = ev.getY() - mTempOutLocation[1];
+        return findDelegateView(xInOurCoordinates, yInOurCoorindates) != null;
+    }
+
+    @Override
+    public boolean onDragEvent(DragEvent event) {
+        switch (event.getAction()) {
+            case DragEvent.ACTION_DRAG_STARTED:
+                mIsDraggingItem = true;
+                return true;
+            case DragEvent.ACTION_DRAG_ENDED:
+                mIsDraggingItem = false;
+                break;
+        }
+        return super.onDragEvent(event);
+    }
+
+    public boolean isDraggingItem() {
+        return mIsDraggingItem;
+    }
+
+    private View inflate(@LayoutRes int layoutResId) {
+        return LayoutInflater.from(getContext()).inflate(layoutResId, this, false);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
index 597c17b..22c4a7e 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -184,7 +184,10 @@
     }
 
     private int getOutlineOffsetY() {
-        return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
+        if (mDisplay != DISPLAY_TASKBAR) {
+            return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
+        }
+        return (getMeasuredHeight() / 2) - mNormalizedIconRadius;
     }
 
     private void drawEffect(Canvas canvas, boolean isBadged) {
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 5f6e59f..ce14197 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -153,6 +153,13 @@
         return deviceState.isInDeferredGestureRegion(ev);
     }
 
+    /**
+     * @return Whether the gesture in progress should be cancelled.
+     */
+    public boolean shouldCancelCurrentGesture() {
+        return false;
+    }
+
     public abstract void onExitOverview(RotationTouchHelper deviceState,
             Runnable exitRunnable);
 
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index deb86ec..8b0d782 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -26,6 +26,7 @@
 import android.content.res.Resources;
 import android.graphics.Rect;
 import android.util.Log;
+import android.view.MotionEvent;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
@@ -311,4 +312,22 @@
         boolean isImeVisible = (systemUiStateFlags & SYSUI_STATE_IME_SHOWING) != 0;
         taskbarController.setIsImeVisible(isImeVisible);
     }
+
+    @Override
+    public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
+        TaskbarController taskbarController = getTaskbarController();
+        if (taskbarController == null) {
+            return super.deferStartingActivity(deviceState, ev);
+        }
+        return taskbarController.isEventOverAnyTaskbarItem(ev);
+    }
+
+    @Override
+    public boolean shouldCancelCurrentGesture() {
+        TaskbarController taskbarController = getTaskbarController();
+        if (taskbarController == null) {
+            return super.shouldCancelCurrentGesture();
+        }
+        return taskbarController.isDraggingItem();
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index a98fc1c..8ebea33 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -514,9 +514,14 @@
             }
         }
 
-        boolean cleanUpConsumer = (action == ACTION_UP || action == ACTION_CANCEL)
+        boolean cancelGesture = mGestureState.getActivityInterface() != null
+                && mGestureState.getActivityInterface().shouldCancelCurrentGesture();
+        boolean cleanUpConsumer = (action == ACTION_UP || action == ACTION_CANCEL || cancelGesture)
                 && mConsumer != null
                 && !mConsumer.getActiveConsumerInHierarchy().isConsumerDetachedFromGesture();
+        if (cancelGesture) {
+            event.setAction(ACTION_CANCEL);
+        }
         mUncheckedConsumer.onMotionEvent(event);
 
         if (cleanUpConsumer) {
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 96c30b5..e593fb4 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -57,6 +57,7 @@
             <enum name="widget_section" value="3" />
             <enum name="shortcut_popup" value="4" />
             <enum name="hero_app" value="5" />
+            <enum name="taskbar" value="6" />
         </attr>
         <attr name="centerVertically" format="boolean" />
     </declare-styleable>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3425f91..447c9ac 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -232,6 +232,8 @@
     <string name="abandoned_promise_explanation">The app for this icon isn\'t installed.
         You can remove it, or search for the app and install it manually.
     </string>
+    <!-- Title for an app which is being installed. -->
+    <string name="app_installing_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> installing, <xliff:g id="progress" example="30%">%2$s</xliff:g> complete</string>
     <!-- Title for an app which is being downloaded. -->
     <string name="app_downloading_title"><xliff:g id="name" example="Messenger">%1$s</xliff:g> downloading, <xliff:g id="progress" example="30%">%2$s</xliff:g> complete</string>
     <!-- Title for an app whose download has been started. -->
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 575d6cd..140340a 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -92,6 +92,7 @@
     private static final int DISPLAY_ALL_APPS = 1;
     private static final int DISPLAY_FOLDER = 2;
     private static final int DISPLAY_HERO_APP = 5;
+    protected static final int DISPLAY_TASKBAR = 6;
 
     private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed};
     private static final float HIGHLIGHT_SCALE = 1.16f;
@@ -140,7 +141,7 @@
     private Drawable mIcon;
     private boolean mCenterVertically;
 
-    private final int mDisplay;
+    protected final int mDisplay;
 
     private final CheckLongPressHelper mLongPressHelper;
 
@@ -206,6 +207,8 @@
             defaultIconSize = grid.folderChildIconSizePx;
         } else if (mDisplay == DISPLAY_HERO_APP) {
             defaultIconSize = grid.allAppsIconSizePx;
+        } else if (mDisplay == DISPLAY_TASKBAR) {
+            defaultIconSize = grid.iconSizePx;
         } else {
             // widget_selection or shortcut_popup
             defaultIconSize = grid.iconSizePx;
@@ -289,6 +292,7 @@
         setTag(info);
         applyLoadingState(promiseStateChanged);
         applyDotState(info, false /* animate */);
+        setDownloadStateContentDescription(info, info.getProgressLevel());
     }
 
     public void applyFromApplicationInfo(AppInfo info) {
@@ -304,6 +308,7 @@
             applyProgressLevel();
         }
         applyDotState(info, false /* animate */);
+        setDownloadStateContentDescription(info, info.getProgressLevel());
     }
 
     /**
@@ -316,6 +321,8 @@
 
         // Verify high res immediately
         verifyHighRes();
+
+        setDownloadStateContentDescription(info, info.getProgressLevel());
     }
 
     /**
@@ -483,6 +490,10 @@
      * @param canvas The canvas to draw to.
      */
     protected void drawDotIfNecessary(Canvas canvas) {
+        if (mDisplay == DISPLAY_TASKBAR) {
+            // TODO: support notification dots in Taskbar
+            return;
+        }
         if (!mForceHideDot && (hasDot() || mDotParams.scale > 0)) {
             getIconBounds(mDotParams.iconBounds);
             Utilities.scaleRectAboutCenter(mDotParams.iconBounds,
@@ -635,9 +646,7 @@
             setContentDescription(info.contentDescription != null
                     ? info.contentDescription : "");
         } else if (progressLevel > 0) {
-            setContentDescription(getContext()
-                    .getString(R.string.app_downloading_title, info.title,
-                            NumberFormat.getPercentInstance().format(progressLevel * 0.01)));
+            setDownloadStateContentDescription(info, progressLevel);
         } else {
             setContentDescription(getContext()
                     .getString(R.string.app_waiting_download_title, info.title));
@@ -713,6 +722,24 @@
         }
     }
 
+    private void setDownloadStateContentDescription(ItemInfoWithIcon info, int progressLevel) {
+        if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK)
+                != 0) {
+            String percentageString = NumberFormat.getPercentInstance()
+                    .format(progressLevel * 0.01);
+            if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
+                setContentDescription(getContext()
+                        .getString(
+                            R.string.app_installing_title, info.title, percentageString));
+            } else if ((info.runtimeStatusFlags
+                    & ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) {
+                setContentDescription(getContext()
+                        .getString(
+                            R.string.app_downloading_title, info.title, percentageString));
+            }
+        }
+    }
+
     /**
      * Sets the icon for this view based on the layout direction.
      */
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 0274775..2334267 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1917,6 +1917,13 @@
 
     @Override
     public boolean startActivitySafely(View v, Intent intent, ItemInfo item) {
+        if (isViewInTaskbar(v)) {
+            // Start the activity without the hacky workarounds below, which assume the View was
+            // clicked when Launcher was resumed and will be hidden until Launcher is re-resumed
+            // (this isn't the case for Taskbar).
+            return super.startActivitySafely(v, intent, item);
+        }
+
         if (!hasBeenResumed()) {
             // Workaround an issue where the WM launch animation is clobbered when finishing the
             // recents animation into launcher. Defer launching the activity until Launcher is
@@ -2818,6 +2825,13 @@
                 .start();
     }
 
+    /**
+     * @return Whether the View is in the same window as the Taskbar window.
+     */
+    public boolean isViewInTaskbar(View v) {
+        return false;
+    }
+
     private static class NonConfigInstance {
         public Configuration config;
         public Bitmap snapshot;
diff --git a/src/com/android/launcher3/LauncherAppTransitionManager.java b/src/com/android/launcher3/LauncherAppTransitionManager.java
index ac3ad9f..0fa441a 100644
--- a/src/com/android/launcher3/LauncherAppTransitionManager.java
+++ b/src/com/android/launcher3/LauncherAppTransitionManager.java
@@ -50,7 +50,7 @@
         return ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
     }
 
-    public boolean supportsAdaptiveIconAnimation() {
+    public boolean supportsAdaptiveIconAnimation(View clickedView) {
         return false;
     }
 
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index 3122c68..2647d6f 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -302,7 +302,7 @@
                 intent.setPackage(null);
             }
         }
-        if (v != null && launcher.getAppTransitionManager().supportsAdaptiveIconAnimation()) {
+        if (v != null && launcher.getAppTransitionManager().supportsAdaptiveIconAnimation(v)) {
             // Preload the icon to reduce latency b/w swapping the floating view with the original.
             FloatingIconView.fetchIcon(launcher, v, item, true /* isOpening */);
         }