Merge "Creates BubbleBarView & BubbleBarViewController & friends" into udc-dev
diff --git a/quickstep/res/layout/bubble_view.xml b/quickstep/res/layout/bubble_view.xml
new file mode 100644
index 0000000..0b1ed9f
--- /dev/null
+++ b/quickstep/res/layout/bubble_view.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <ImageView
+        android:id="@+id/icon_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:contentDescription="@null" />
+
+    <!--
+        Icon badge size is defined in Launcher3 BaseIconFactory as 0.444 of icon size.
+        Constraint guide starts from left, which means for a badge positioned on the right,
+        percent has to be 1 - 0.444 to have the same effect.
+    -->
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/app_icon_constraint_horizontal"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintGuide_percent="0.556" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/app_icon_constraint_vertical"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_percent="0.556" />
+
+    <ImageView
+        android:id="@+id/app_icon_view"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:contentDescription="@null"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="@id/app_icon_constraint_vertical"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="@id/app_icon_constraint_horizontal" />
+
+</merge>
\ No newline at end of file
diff --git a/quickstep/res/layout/bubblebar_item_view.xml b/quickstep/res/layout/bubblebar_item_view.xml
new file mode 100644
index 0000000..64fc4df
--- /dev/null
+++ b/quickstep/res/layout/bubblebar_item_view.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.taskbar.bubbles.BubbleView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/bubble_view"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"/>
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 3df5d57..cdb3b1c 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -344,6 +344,19 @@
     <!-- Recents overview -->
     <dimen name="recents_filter_icon_size">30dp</dimen>
 
+    <!-- Bubble bar -->
+    <dimen name="bubblebar_size">72dp</dimen>
+    <dimen name="bubblebar_stashed_handle_width">55dp</dimen>
+    <dimen name="bubblebar_stashed_size">@dimen/transient_taskbar_stashed_height</dimen>
+    <dimen name="bubblebar_stashed_handle_height">@dimen/taskbar_stashed_handle_height</dimen>
+    <dimen name="bubblebar_pointer_size">8dp</dimen>
+
+    <dimen name="bubblebar_icon_size">50dp</dimen>
+    <dimen name="bubblebar_badge_size">24dp</dimen>
+    <dimen name="bubblebar_icon_overlap">12dp</dimen>
+    <dimen name="bubblebar_icon_spacing">3dp</dimen>
+    <dimen name="bubblebar_icon_elevation">1dp</dimen>
+
     <!-- Launcher splash screen -->
     <!-- Note: keep this value in sync with the WindowManager/Shell dimens.xml -->
     <!--     starting_surface_exit_animation_window_shift_length -->
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
new file mode 100644
index 0000000..667c6f5
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 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.bubbles
+
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorFilter
+import android.graphics.Paint
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.ShapeDrawable
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.Utilities.mapToRange
+import com.android.launcher3.anim.Interpolators
+import com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.wm.shell.common.TriangleShape
+
+/** Drawable for the background of the bubble bar. */
+class BubbleBarBackground(context: TaskbarActivityContext, private val backgroundHeight: Float) :
+    Drawable() {
+
+    private val DARK_THEME_SHADOW_ALPHA = 51f
+    private val LIGHT_THEME_SHADOW_ALPHA = 25f
+
+    private val paint: Paint = Paint()
+    private val pointerSize: Float
+
+    private val shadowAlpha: Float
+    private var shadowBlur = 0f
+    private var keyShadowDistance = 0f
+
+    private var arrowPositionX: Float = 0f
+    private var showingArrow: Boolean = false
+    private var arrowDrawable: ShapeDrawable
+
+    init {
+        paint.color = context.getColor(R.color.taskbar_background)
+        paint.flags = Paint.ANTI_ALIAS_FLAG
+        paint.style = Paint.Style.FILL
+
+        val res = context.resources
+        shadowBlur = res.getDimension(R.dimen.transient_taskbar_shadow_blur)
+        keyShadowDistance = res.getDimension(R.dimen.transient_taskbar_key_shadow_distance)
+        pointerSize = res.getDimension(R.dimen.bubblebar_pointer_size)
+
+        shadowAlpha =
+            if (Utilities.isDarkTheme(context)) DARK_THEME_SHADOW_ALPHA
+            else LIGHT_THEME_SHADOW_ALPHA
+
+        arrowDrawable =
+            ShapeDrawable(TriangleShape.create(pointerSize, pointerSize, /* pointUp= */ true))
+        arrowDrawable.setBounds(0, 0, pointerSize.toInt(), pointerSize.toInt())
+        arrowDrawable.paint.flags = Paint.ANTI_ALIAS_FLAG
+        arrowDrawable.paint.style = Paint.Style.FILL
+        arrowDrawable.paint.color = context.getColor(R.color.taskbar_background)
+    }
+
+    fun showArrow(show: Boolean) {
+        showingArrow = show
+    }
+
+    fun setArrowPosition(x: Float) {
+        arrowPositionX = x
+    }
+
+    /** Draws the background with the given paint and height, on the provided canvas. */
+    override fun draw(canvas: Canvas) {
+        canvas.save()
+
+        // TODO (b/277359345): Should animate the alpha similar to taskbar (see TaskbarDragLayer)
+        // Draw shadows.
+        val newShadowAlpha =
+            mapToRange(paint.alpha.toFloat(), 0f, 255f, 0f, shadowAlpha, Interpolators.LINEAR)
+        paint.setShadowLayer(
+            shadowBlur,
+            0f,
+            keyShadowDistance,
+            setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha))
+        )
+        arrowDrawable.paint.setShadowLayer(
+            shadowBlur,
+            0f,
+            keyShadowDistance,
+            setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha))
+        )
+
+        // Draw background.
+        val radius = backgroundHeight / 2f
+        canvas.drawRoundRect(
+            0f,
+            0f,
+            canvas.width.toFloat(),
+            canvas.height.toFloat(),
+            radius,
+            radius,
+            paint
+        )
+
+        if (showingArrow) {
+            // Draw arrow.
+            val transX = arrowPositionX - pointerSize / 2f
+            canvas.translate(transX, -pointerSize)
+            arrowDrawable.draw(canvas)
+        }
+
+        canvas.restore()
+    }
+
+    override fun getOpacity(): Int {
+        return paint.alpha
+    }
+
+    override fun setAlpha(alpha: Int) {
+        paint.alpha = alpha
+    }
+
+    override fun setColorFilter(colorFilter: ColorFilter?) {
+        paint.colorFilter = colorFilter
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBubble.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBubble.kt
new file mode 100644
index 0000000..b1633e7
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBubble.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 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.bubbles
+
+import android.graphics.Bitmap
+import android.graphics.Path
+import com.android.wm.shell.common.bubbles.BubbleInfo
+
+/** Contains state info about a bubble in the bubble bar as well as presentation information. */
+data class BubbleBarBubble(
+    val info: BubbleInfo,
+    val view: BubbleView,
+    val badge: Bitmap,
+    val icon: Bitmap,
+    val dotColor: Int,
+    val dotPath: Path,
+    val appName: String
+) {
+
+    fun getKey(): String {
+        return info.key
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
new file mode 100644
index 0000000..07daf06
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2023 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.bubbles;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.R;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.views.ActivityContext;
+
+import java.util.List;
+
+/**
+ * The view that holds all the bubble views. Modifying this view should happen through
+ * {@link BubbleBarViewController}. Updates to the bubbles themselves (adds, removes, updates,
+ * selection) should happen through BubbleBarController which is the source of truth
+ * for state information about the bubbles.
+ * <p>
+ * The bubble bar has a couple of visual states:
+ * - stashed as a handle
+ * - unstashed but collapsed, in this state the bar is showing but the bubbles are stacked within it
+ * - unstashed and expanded, in this state the bar is showing and the bubbles are shown in a row
+ *   with one of the bubbles being selected. Additionally, WMShell will display the expanded bubble
+ *   view above the bar.
+ * <p>
+ * The bubble bar has some behavior related to taskbar:
+ * - When taskbar is unstashed, bubble bar will also become unstashed (but in its "collapsed"
+ *   state)
+ * - When taskbar is stashed, bubble bar will also become stashed (unless bubble bar is in its
+ *   "expanded" state)
+ * - When bubble bar is in its "expanded" state, taskbar becomes stashed
+ * <p>
+ * If there are no bubbles, the bubble bar and bubble stashed handle are not shown. Additionally
+ * the bubble bar and stashed handle are not shown on lockscreen.
+ * <p>
+ * When taskbar is in persistent or 3 button nav mode, the bubble bar is not available, and instead
+ * the bubbles are shown fully by WMShell in their floating mode.
+ */
+public class BubbleBarView extends FrameLayout {
+
+    private static final String TAG = BubbleBarView.class.getSimpleName();
+
+    // TODO: (b/273594744) calculate the amount of space we have and base the max on that
+    //  if it's smaller than 5.
+    private static final int MAX_BUBBLES = 5;
+
+    private final TaskbarActivityContext mActivityContext;
+    private final BubbleBarBackground mBubbleBarBackground;
+
+    // The current bounds of all the bubble bar.
+    private final Rect mBubbleBarBounds = new Rect();
+    // The amount the bubbles overlap when they are stacked in the bubble bar
+    private final float mIconOverlapAmount;
+    // The spacing between the bubbles when they are expanded in the bubble bar
+    private final float mIconSpacing;
+    // The size of a bubble in the bar
+    private final float mIconSize;
+    // The elevation of the bubbles within the bar
+    private final float mBubbleElevation;
+
+    // Whether the bar is expanded (i.e. the bubble activity is being displayed).
+    private boolean mIsBarExpanded = false;
+    // The currently selected bubble view.
+    private BubbleView mSelectedBubbleView;
+    // The click listener when the bubble bar is collapsed.
+    private View.OnClickListener mOnClickListener;
+
+    private final Rect mTempRect = new Rect();
+
+    // We don't reorder the bubbles when they are expanded as it could be jarring for the user
+    // this runnable will be populated with any reordering of the bubbles that should be applied
+    // once they are collapsed.
+    @Nullable
+    private Runnable mReorderRunnable;
+
+    public BubbleBarView(Context context) {
+        this(context, null);
+    }
+
+    public BubbleBarView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        mActivityContext = ActivityContext.lookupContext(context);
+
+        mIconOverlapAmount = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_overlap);
+        mIconSpacing = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_spacing);
+        mIconSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
+        mBubbleElevation = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_elevation);
+        setClipToPadding(false);
+
+        mBubbleBarBackground = new BubbleBarBackground(mActivityContext,
+                getResources().getDimensionPixelSize(R.dimen.bubblebar_size));
+        setBackgroundDrawable(mBubbleBarBackground);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        mBubbleBarBounds.left = left;
+        mBubbleBarBounds.top = top;
+        mBubbleBarBounds.right = right;
+        mBubbleBarBounds.bottom = bottom;
+
+        // The bubble bar handle is aligned to the bottom edge of the screen so scale towards that.
+        setPivotX(getWidth());
+        setPivotY(getHeight());
+
+        // Position the views
+        updateChildrenRenderNodeProperties();
+    }
+
+    /**
+     * Returns the bounds of the bubble bar.
+     */
+    public Rect getBubbleBarBounds() {
+        return mBubbleBarBounds;
+    }
+
+    // TODO: (b/273592694) animate it
+    @Override
+    public void addView(View child, int index, ViewGroup.LayoutParams params) {
+        if (getChildCount() + 1 > MAX_BUBBLES) {
+            removeViewInLayout(getChildAt(getChildCount() - 1));
+        }
+        super.addView(child, index, params);
+    }
+
+    /**
+     * Updates the z order, positions, and badge visibility of the bubble views in the bar based
+     * on the expanded state.
+     */
+    // TODO: (b/273592694) animate it
+    private void updateChildrenRenderNodeProperties() {
+        int bubbleCount = getChildCount();
+        final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f;
+        for (int i = 0; i < bubbleCount; i++) {
+            BubbleView bv = (BubbleView) getChildAt(i);
+            bv.setTranslationY(ty);
+            if (mIsBarExpanded) {
+                final float tx = i * (mIconSize + mIconSpacing);
+                bv.setTranslationX(tx);
+                bv.setZ(0);
+                bv.showBadge();
+            } else {
+                bv.setZ((MAX_BUBBLES * mBubbleElevation) - i);
+                bv.setTranslationX(i * mIconOverlapAmount);
+                if (i > 0) {
+                    bv.hideBadge();
+                } else {
+                    bv.showBadge();
+                }
+            }
+        }
+    }
+
+    /**
+     * Reorders the views to match the provided list.
+     */
+    public void reorder(List<BubbleView> viewOrder) {
+        if (isExpanded()) {
+            mReorderRunnable = () -> doReorder(viewOrder);
+        } else {
+            doReorder(viewOrder);
+        }
+    }
+
+    // TODO: (b/273592694) animate it
+    private void doReorder(List<BubbleView> viewOrder) {
+        if (!isExpanded()) {
+            for (int i = 0; i < viewOrder.size(); i++) {
+                View child = viewOrder.get(i);
+                if (child != null) {
+                    removeViewInLayout(child);
+                    addViewInLayout(child, i, child.getLayoutParams());
+                }
+            }
+            updateChildrenRenderNodeProperties();
+        }
+    }
+
+    /**
+     * Sets which bubble view should be shown as selected.
+     */
+    // TODO: (b/273592694) animate it
+    public void setSelectedBubble(BubbleView view) {
+        mSelectedBubbleView = view;
+        updateArrowForSelected();
+        invalidate();
+    }
+
+    private void updateArrowForSelected() {
+        if (mSelectedBubbleView == null) {
+            Log.w(TAG, "trying to update selection arrow without a selected view!");
+            return;
+        }
+        final int index = indexOfChild(mSelectedBubbleView);
+        // Find the center of the bubble when it's expanded, set the arrow position to it.
+        final float tx = getPaddingStart() + index * (mIconSize + mIconSpacing) + mIconSize / 2f;
+        mBubbleBarBackground.setArrowPosition(tx);
+    }
+
+    @Override
+    public void setOnClickListener(View.OnClickListener listener) {
+        mOnClickListener = listener;
+        setOrUnsetClickListener();
+    }
+
+    /**
+     * The click listener used for the bubble view gets added / removed depending on whether
+     * the bar is expanded or collapsed, this updates whether the listener is set based on state.
+     */
+    private void setOrUnsetClickListener() {
+        super.setOnClickListener(mIsBarExpanded ? null : mOnClickListener);
+    }
+
+    /**
+     * Sets whether the bubble bar is expanded or collapsed.
+     */
+    // TODO: (b/273592694) animate it
+    public void setExpanded(boolean isBarExpanded) {
+        if (mIsBarExpanded != isBarExpanded) {
+            mIsBarExpanded = isBarExpanded;
+            updateArrowForSelected();
+            setOrUnsetClickListener();
+            if (!isBarExpanded && mReorderRunnable != null) {
+                mReorderRunnable.run();
+                mReorderRunnable = null;
+            }
+            mBubbleBarBackground.showArrow(mIsBarExpanded);
+            requestLayout(); // trigger layout to reposition views & update size for expansion
+        }
+    }
+
+    /**
+     * Returns whether the bubble bar is expanded.
+     */
+    public boolean isExpanded() {
+        return mIsBarExpanded;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int childCount = getChildCount();
+        final float iconWidth = mIsBarExpanded
+                ? (childCount * (mIconSize + mIconSpacing))
+                : mIconSize + ((childCount - 1) * mIconOverlapAmount);
+        final int totalWidth = (int) iconWidth + getPaddingStart() + getPaddingEnd();
+        setMeasuredDimension(totalWidth, MeasureSpec.getSize(heightMeasureSpec));
+
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            measureChild(child, (int) mIconSize, (int) mIconSize);
+        }
+    }
+
+    /**
+     * Returns whether the given MotionEvent, *in screen coordinates*, is within bubble bar
+     * touch bounds.
+     */
+    public boolean isEventOverAnyItem(MotionEvent ev) {
+        if (getVisibility() == View.VISIBLE) {
+            getBoundsOnScreen(mTempRect);
+            return mTempRect.contains((int) ev.getX(), (int) ev.getY());
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (!mIsBarExpanded) {
+            // When the bar is collapsed, all taps on it should expand it.
+            return true;
+        }
+        return super.onInterceptTouchEvent(ev);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
new file mode 100644
index 0000000..deac42f
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2023 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.bubbles;
+
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+
+import android.graphics.Rect;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.util.MultiPropertyFactory;
+import com.android.launcher3.util.MultiValueAlpha;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Controller for {@link BubbleBarView}. Manages the visibility of the bubble bar as well as
+ * responding to changes in bubble state provided by BubbleBarController.
+ */
+public class BubbleBarViewController {
+
+    private static final String TAG = BubbleBarViewController.class.getSimpleName();
+
+    private final TaskbarActivityContext mActivity;
+    private final BubbleBarView mBarView;
+    private final int mIconSize;
+
+    // Initialized in init.
+    private View.OnClickListener mBubbleClickListener;
+    private View.OnClickListener mBubbleBarClickListener;
+
+    // These are exposed to BubbleStashController to animate for stashing/un-stashing
+    private final MultiValueAlpha mBubbleBarAlpha;
+    private final AnimatedFloat mBubbleBarScale = new AnimatedFloat(this::updateScale);
+    private final AnimatedFloat mBubbleBarTranslationY = new AnimatedFloat(
+            this::updateTranslationY);
+
+    // Modified when swipe up is happening on the bubble bar or task bar.
+    private float mBubbleBarSwipeUpTranslationY;
+
+    // Whether the bar is hidden for a sysui state.
+    private boolean mHiddenForSysui;
+    // Whether the bar is hidden because there are no bubbles.
+    private boolean mHiddenForNoBubbles;
+
+    public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView) {
+        mActivity = activity;
+        mBarView = barView;
+        mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */);
+        mBubbleBarAlpha.setUpdateVisibility(true);
+        mIconSize = activity.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
+    }
+
+    public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) {
+        mActivity.addOnDeviceProfileChangeListener(dp ->
+                mBarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarHeight
+        );
+        mBarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarHeight;
+        mBubbleBarScale.updateValue(1f);
+        mBubbleClickListener = v -> onBubbleClicked(v);
+        mBubbleBarClickListener = v -> setExpanded(true);
+        mBarView.setOnClickListener(mBubbleBarClickListener);
+        // TODO: when barView layout changes tell taskbarInsetsController the insets have changed.
+    }
+
+    private void onBubbleClicked(View v) {
+        BubbleBarBubble bubble = ((BubbleView) v).getBubble();
+        if (bubble == null) {
+            Log.e(TAG, "bubble click listener, bubble was null");
+        }
+        // TODO: handle the click
+    }
+
+    //
+    // The below animators are exposed to BubbleStashController so it can manage the stashing
+    // animation.
+    //
+
+    public MultiPropertyFactory<View> getBubbleBarAlpha() {
+        return mBubbleBarAlpha;
+    }
+
+    public AnimatedFloat getBubbleBarScale() {
+        return mBubbleBarScale;
+    }
+
+    public AnimatedFloat getBubbleBarTranslationY() {
+        return mBubbleBarTranslationY;
+    }
+
+    /**
+     * Whether the bubble bar is visible or not.
+     */
+    public boolean isBubbleBarVisible() {
+        return mBarView.getVisibility() == VISIBLE;
+    }
+
+    /**
+     * The bounds of the bubble bar.
+     */
+    public Rect getBubbleBarBounds() {
+        return mBarView.getBubbleBarBounds();
+    }
+
+    /**
+     * When the bubble bar is not stashed, it can be collapsed (the icons are in a stack) or
+     * expanded (the icons are in a row). This indicates whether the bubble bar is expanded.
+     */
+    public boolean isExpanded() {
+        return mBarView.isExpanded();
+    }
+
+    /**
+     * Whether the motion event is within the bounds of the bubble bar.
+     */
+    public boolean isEventOverAnyItem(MotionEvent ev) {
+        return mBarView.isEventOverAnyItem(ev);
+    }
+
+    //
+    // Visibility of the bubble bar
+    //
+
+    /**
+     * Returns whether the bubble bar is hidden because there are no bubbles.
+     */
+    public boolean isHiddenForNoBubbles() {
+        return mHiddenForNoBubbles;
+    }
+
+    /**
+     * Sets whether the bubble bar should be hidden because there are no bubbles.
+     */
+    public void setHiddenForBubbles(boolean hidden) {
+        if (mHiddenForNoBubbles != hidden) {
+            mHiddenForNoBubbles = hidden;
+            updateVisibilityForStateChange();
+        }
+    }
+
+    /**
+     * Sets whether the bubble bar should be hidden due to SysUI state (e.g. on lockscreen).
+     */
+    public void setHiddenForSysui(boolean hidden) {
+        if (mHiddenForSysui != hidden) {
+            mHiddenForSysui = hidden;
+            updateVisibilityForStateChange();
+        }
+    }
+
+    // TODO: (b/273592694) animate it
+    private void updateVisibilityForStateChange() {
+        // TODO: check if it's stashed
+        if (!mHiddenForSysui && !mHiddenForNoBubbles) {
+            mBarView.setVisibility(VISIBLE);
+        } else {
+            mBarView.setVisibility(INVISIBLE);
+        }
+    }
+
+    //
+    // Modifying view related properties.
+    //
+
+    /**
+     * Sets the translation of the bubble bar during the swipe up gesture.
+     */
+    public void setTranslationYForSwipe(float transY) {
+        mBubbleBarSwipeUpTranslationY = transY;
+        updateTranslationY();
+    }
+
+    private void updateTranslationY() {
+        mBarView.setTranslationY(mBubbleBarTranslationY.value
+                + mBubbleBarSwipeUpTranslationY);
+    }
+
+    /**
+     * Applies scale properties for the entire bubble bar.
+     */
+    private void updateScale() {
+        float scale = mBubbleBarScale.value;
+        mBarView.setScaleX(scale);
+        mBarView.setScaleY(scale);
+    }
+
+    //
+    // Manipulating the specific bubble views in the bar
+    //
+
+    /**
+     * Removes the provided bubble from the bubble bar.
+     */
+    public void removeBubble(BubbleBarBubble b) {
+        if (b != null) {
+            mBarView.removeView(b.getView());
+        } else {
+            Log.w(TAG, "removeBubble, bubble was null!");
+        }
+    }
+
+    /**
+     * Adds the provided bubble to the bubble bar.
+     */
+    public void addBubble(BubbleBarBubble b) {
+        if (b != null) {
+            mBarView.addView(b.getView(), 0, new FrameLayout.LayoutParams(mIconSize, mIconSize));
+            b.getView().setOnClickListener(mBubbleClickListener);
+        } else {
+            Log.w(TAG, "addBubble, bubble was null!");
+        }
+    }
+
+    /**
+     * Reorders the bubbles based on the provided list.
+     */
+    public void reorderBubbles(List<BubbleBarBubble> newOrder) {
+        List<BubbleView> viewList = newOrder.stream().filter(Objects::nonNull)
+                .map(BubbleBarBubble::getView).toList();
+        mBarView.reorder(viewList);
+    }
+
+    /**
+     * Updates the selected bubble.
+     */
+    public void updateSelectedBubble(BubbleBarBubble newlySelected) {
+        mBarView.setSelectedBubble(newlySelected.getView());
+    }
+
+    /**
+     * Sets whether the bubble bar should be expanded (not unstashed, but have the contents
+     * within it expanded). This method notifies SystemUI that the bubble bar is expanded and
+     * showing a selected bubble. This method should ONLY be called from UI events originating
+     * from Launcher.
+     */
+    public void setExpanded(boolean isExpanded) {
+        if (isExpanded != mBarView.isExpanded()) {
+            mBarView.setExpanded(isExpanded);
+            if (!isExpanded) {
+                // TODO: Tell SysUi to collapse the bubble
+            } else {
+                // TODO: Tell SysUi to show the bubble
+                // TODO: Tell taskbar stash controller to stash without bubbles following
+            }
+        }
+    }
+
+    /**
+     * Sets whether the bubble bar should be expanded. This method is used in response to UI events
+     * from SystemUI.
+     */
+    public void setExpandedFromSysui(boolean isExpanded) {
+        // TODO: Tell bubble bar stash controller to stash or unstash the bubble bar
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
new file mode 100644
index 0000000..e92d4fb
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2023 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.bubbles;
+
+import com.android.launcher3.taskbar.TaskbarControllers;
+import com.android.launcher3.util.RunnableList;
+
+/**
+ * Hosts various bubble controllers to facilitate passing between one another.
+ */
+public class BubbleControllers {
+
+    public final BubbleBarViewController bubbleBarViewController;
+
+    private final RunnableList mPostInitRunnables = new RunnableList();
+
+    /**
+     * Want to add a new controller? Don't forget to:
+     *   * Call init
+     *   * Call onDestroy
+     */
+    public BubbleControllers(BubbleBarViewController bubbleBarViewController) {
+        this.bubbleBarViewController = bubbleBarViewController;
+    }
+
+    /**
+     * Initializes all controllers. Note that controllers can now reference each other through this
+     * BubbleControllers instance, but should be careful to only access things that were created
+     * in constructors for now, as some controllers may still be waiting for init().
+     */
+    public void init(TaskbarControllers taskbarControllers) {
+        bubbleBarViewController.init(taskbarControllers, this);
+
+        mPostInitRunnables.executeAllAndDestroy();
+    }
+
+    /**
+     * If all controllers are already initialized, runs the given callback immediately. Otherwise,
+     * queues it to run after calling init() on all controllers. This should likely be used in any
+     * case where one controller is telling another controller to do something inside init().
+     */
+    public void runAfterInit(Runnable runnable) {
+        // If this has been executed in init, it automatically runs adds to it.
+        mPostInitRunnables.add(runnable);
+    }
+
+    /**
+     * Cleans up all controllers.
+     */
+    public void onDestroy() {
+        // TODO
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
new file mode 100644
index 0000000..e22e63a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 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.bubbles;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Outline;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.ImageView;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import com.android.launcher3.R;
+import com.android.launcher3.icons.IconNormalizer;
+
+// TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
+// TODO: (b/269670235) currently this doesn't show the 'update dot'
+/**
+ * View that displays a bubble icon, along with an app badge on either the left or
+ * right side of the view.
+ */
+public class BubbleView extends ConstraintLayout {
+
+    // TODO: (b/269670235) currently we don't render the 'update dot', this will be used for that.
+    public static final int DEFAULT_PATH_SIZE = 100;
+
+    private final ImageView mBubbleIcon;
+    private final ImageView mAppIcon;
+    private final int mBubbleSize;
+
+    // TODO: (b/273310265) handle RTL
+    private boolean mOnLeft = false;
+
+    private BubbleBarBubble mBubble;
+
+    public BubbleView(Context context) {
+        this(context, null);
+    }
+
+    public BubbleView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public BubbleView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        // We manage positioning the badge ourselves
+        setLayoutDirection(LAYOUT_DIRECTION_LTR);
+
+        LayoutInflater.from(context).inflate(R.layout.bubble_view, this);
+
+        mBubbleSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
+        mBubbleIcon = findViewById(R.id.icon_view);
+        mAppIcon = findViewById(R.id.app_icon_view);
+
+        setFocusable(true);
+        setClickable(true);
+        setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                BubbleView.this.getOutline(outline);
+            }
+        });
+    }
+
+    private void getOutline(Outline outline) {
+        final int normalizedSize = IconNormalizer.getNormalizedCircleSize(mBubbleSize);
+        final int inset = (mBubbleSize - normalizedSize) / 2;
+        outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
+    }
+
+    /** Sets the bubble being rendered in this view. */
+    void setBubble(BubbleBarBubble bubble) {
+        mBubble = bubble;
+        mBubbleIcon.setImageBitmap(bubble.getIcon());
+        mAppIcon.setImageBitmap(bubble.getBadge());
+    }
+
+    /** Returns the bubble being rendered in this view. */
+    @Nullable
+    BubbleBarBubble getBubble() {
+        return mBubble;
+    }
+
+    /** Shows the app badge on this bubble. */
+    void showBadge() {
+        Bitmap appBadgeBitmap = mBubble.getBadge();
+        if (appBadgeBitmap == null) {
+            mAppIcon.setVisibility(GONE);
+            return;
+        }
+
+        int translationX;
+        if (mOnLeft) {
+            translationX = -(mBubble.getIcon().getWidth() - appBadgeBitmap.getWidth());
+        } else {
+            translationX = 0;
+        }
+
+        mAppIcon.setTranslationX(translationX);
+        mAppIcon.setVisibility(VISIBLE);
+    }
+
+    /** Hides the app badge on this bubble. */
+    void hideBadge() {
+        mAppIcon.setVisibility(GONE);
+    }
+
+    @Override
+    public String toString() {
+        return "BubbleView{" + mBubble + "}";
+    }
+}