Merge "Revert change to rotateBounds" into main
diff --git a/quickstep/res/values/ids.xml b/quickstep/res/values/ids.xml
new file mode 100644
index 0000000..3091d9e
--- /dev/null
+++ b/quickstep/res/values/ids.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<resources>
+    <!-- Used for A11y actions for bubble bar -->
+    <item type="id" name="action_move_left" />
+    <item type="id" name="action_move_right" />
+    <item type="id" name="action_dismiss_all" />
+</resources>
\ No newline at end of file
diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml
index 340d25b..98a2783 100644
--- a/quickstep/res/values/strings.xml
+++ b/quickstep/res/values/strings.xml
@@ -342,4 +342,10 @@
     <string name="bubble_bar_bubble_description"><xliff:g id="notification_title" example="some title">%1$s</xliff:g> from <xliff:g id="app_name" example="YouTube">%2$s</xliff:g></string>
     <!-- Content description for bubble bar when it has multiple bubbles. [CHAR_LIMIT=NONE] -->
     <string name="bubble_bar_description_multiple_bubbles"><xliff:g id="bubble_bar_bubble_description" example="some title from YouTube">%1$s</xliff:g> and <xliff:g id="bubble_count" example="4">%2$d</xliff:g> more</string>
+    <!-- Action in accessibility menu to move the bubble bar to the left side of the screen. [CHAR_LIMIT=30] -->
+    <string name="bubble_bar_action_move_left">Move left</string>
+    <!-- Action in accessibility menu to move the bubble bar to the right side of the screen. [CHAR_LIMIT=30] -->
+    <string name="bubble_bar_action_move_right">Move right</string>
+    <!-- Action in accessibility menu to dismiss all bubbles. [CHAR_LIMIT=30] -->
+    <string name="bubble_bar_action_dismiss_all">Dismiss all</string>
 </resources>
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 4794dfd..fab7975 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -30,6 +30,7 @@
 import android.content.Context;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.os.Bundle;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.util.LayoutDirection;
@@ -38,6 +39,7 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
 
 import androidx.dynamicanimation.animation.SpringForce;
@@ -367,6 +369,47 @@
         }
     }
 
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        // Always show only expand action as the menu is only for collapsed bubble bar
+        info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND);
+        info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_dismiss_all,
+                getResources().getString(R.string.bubble_bar_action_dismiss_all)));
+        if (mBubbleBarLocation.isOnLeft(isLayoutRtl())) {
+            info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_right,
+                    getResources().getString(R.string.bubble_bar_action_move_right)));
+        } else {
+            info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_move_left,
+                    getResources().getString(R.string.bubble_bar_action_move_left)));
+        }
+    }
+
+    @Override
+    public boolean performAccessibilityActionInternal(int action,
+            @androidx.annotation.Nullable Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+        if (action == AccessibilityNodeInfo.ACTION_EXPAND) {
+            mController.expandBubbleBar();
+            return true;
+        }
+        if (action == R.id.action_dismiss_all) {
+            mController.dismissBubbleBar();
+            return true;
+        }
+        if (action == R.id.action_move_left) {
+            mController.updateBubbleBarLocation(BubbleBarLocation.LEFT);
+            return true;
+        }
+        if (action == R.id.action_move_right) {
+            mController.updateBubbleBarLocation(BubbleBarLocation.RIGHT);
+            return true;
+        }
+        return false;
+    }
+
     @SuppressLint("RtlHardcoded")
     private void onBubbleBarLocationChanged() {
         final boolean onLeft = mBubbleBarLocation.isOnLeft(isLayoutRtl());
@@ -758,7 +801,6 @@
                 listener);
     }
 
-    // TODO: (b/280605790) animate it
     @Override
     public void addView(View child, int index, ViewGroup.LayoutParams params) {
         super.addView(child, index, params);
@@ -1382,5 +1424,14 @@
 
         /** Notifies the controller that the bubble bar was touched while it was animating. */
         void onBubbleBarTouchedWhileAnimating();
+
+        /** Requests the controller to expand bubble bar */
+        void expandBubbleBar();
+
+        /** Requests the controller to dismiss the bubble bar */
+        void dismissBubbleBar();
+
+        /** Requests the controller to update bubble bar location to the given value */
+        void updateBubbleBarLocation(BubbleBarLocation location);
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 2311d42..74a673b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -117,7 +117,7 @@
                 dp -> onBubbleBarConfigurationChanged(/* animate= */ true));
         mBubbleBarScale.updateValue(1f);
         mBubbleClickListener = v -> onBubbleClicked((BubbleView) v);
-        mBubbleBarClickListener = v -> onBubbleBarClicked();
+        mBubbleBarClickListener = v -> expandBubbleBar();
         mBubbleDragController.setupBubbleBarView(mBarView);
         mBarView.setOnClickListener(mBubbleBarClickListener);
         mBarView.addOnLayoutChangeListener(
@@ -137,6 +137,21 @@
             public void onBubbleBarTouchedWhileAnimating() {
                 BubbleBarViewController.this.onBubbleBarTouchedWhileAnimating();
             }
+
+            @Override
+            public void expandBubbleBar() {
+                BubbleBarViewController.this.expandBubbleBar();
+            }
+
+            @Override
+            public void dismissBubbleBar() {
+                onDismissAllBubbles();
+            }
+
+            @Override
+            public void updateBubbleBarLocation(BubbleBarLocation location) {
+                mBubbleBarController.updateBubbleBarLocation(location);
+            }
         });
     }
 
@@ -162,7 +177,7 @@
         mBubbleStashController.onNewBubbleAnimationInterrupted(false, mBarView.getTranslationY());
     }
 
-    private void onBubbleBarClicked() {
+    private void expandBubbleBar() {
         if (mShouldShowEducation) {
             mShouldShowEducation = false;
             // Get the bubble bar bounds on screen
@@ -609,17 +624,17 @@
     }
 
     /**
-     * Called when bubble was dragged into the dismiss target. Notifies System
+     * Called when given bubble was dismissed. Notifies SystemUI
      * @param bubble dismissed bubble item
      */
-    public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) {
+    public void onDismissBubble(@NonNull BubbleBarItem bubble) {
         mSystemUiProxy.dragBubbleToDismiss(bubble.getKey(), mTimeSource.currentTimeMillis());
     }
 
     /**
-     * Called when bubble stack was dragged into the dismiss target
+     * Called when bubble stack was dismissed
      */
-    public void onDismissAllBubblesWhileDragging() {
+    public void onDismissAllBubbles() {
         mSystemUiProxy.removeAllBubbles();
     }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
index a6096e2..6a63da8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java
@@ -143,10 +143,10 @@
         if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) {
             BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject();
             if (bubbleView.getBubble() != null) {
-                mBubbleBarViewController.onDismissBubbleWhileDragging(bubbleView.getBubble());
+                mBubbleBarViewController.onDismissBubble(bubbleView.getBubble());
             }
         } else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) {
-            mBubbleBarViewController.onDismissAllBubblesWhileDragging();
+            mBubbleBarViewController.onDismissAllBubbles();
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index cd62265..f902284 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -33,6 +33,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
@@ -416,7 +417,8 @@
                         | SYSUI_STATE_QUICK_SETTINGS_EXPANDED
                         | SYSUI_STATE_MAGNIFICATION_OVERLAP
                         | SYSUI_STATE_DEVICE_DREAMING
-                        | SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
+                        | SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION
+                        | SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING;
         return (gestureDisablingStates & mSystemUiStateFlags) == 0 && homeOrOverviewEnabled;
     }
 
diff --git a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
index 7a5a714..3a6d5c0 100644
--- a/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
+++ b/quickstep/src/com/android/quickstep/recents/di/RecentsDependencies.kt
@@ -24,6 +24,7 @@
 import com.android.quickstep.recents.data.TasksRepository
 import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
 import com.android.quickstep.recents.usecase.GetThumbnailUseCase
+import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase
 import com.android.quickstep.recents.viewmodel.RecentsViewData
 import com.android.quickstep.task.viewmodel.TaskContainerData
 import com.android.quickstep.task.viewmodel.TaskOverlayViewModel
@@ -162,6 +163,8 @@
                     )
                 }
                 GetThumbnailUseCase::class.java -> GetThumbnailUseCase(taskRepository = inject())
+                SysUiStatusNavFlagsUseCase::class.java ->
+                    SysUiStatusNavFlagsUseCase(taskRepository = inject())
                 GetThumbnailPositionUseCase::class.java ->
                     GetThumbnailPositionUseCase(
                         deviceProfileRepository = inject(),
diff --git a/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.kt b/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.kt
new file mode 100644
index 0000000..1d19c7d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCase.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 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.quickstep.recents.usecase
+
+import android.view.WindowInsetsController
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_DARK_STATUS
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_NAV
+import com.android.launcher3.util.SystemUiController.FLAG_LIGHT_STATUS
+import com.android.quickstep.recents.data.RecentTasksRepository
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+
+/** UseCase to calculate flags for status bar and navigation bar */
+class SysUiStatusNavFlagsUseCase(private val taskRepository: RecentTasksRepository) {
+    fun getSysUiStatusNavFlags(taskId: Int): Int {
+        val thumbnailData =
+            runBlocking { taskRepository.getThumbnailById(taskId).firstOrNull() } ?: return 0
+
+        val thumbnailAppearance = thumbnailData.appearance
+        var flags = 0
+        flags =
+            flags or
+                if (
+                    thumbnailAppearance and WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS != 0
+                )
+                    FLAG_LIGHT_STATUS
+                else FLAG_DARK_STATUS
+        flags =
+            flags or
+                if (
+                    thumbnailAppearance and
+                        WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS != 0
+                )
+                    FLAG_LIGHT_NAV
+                else FLAG_DARK_NAV
+        return flags
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
new file mode 100644
index 0000000..8b8bc3e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/recents/viewmodel/TaskContainerViewModel.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 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.quickstep.recents.viewmodel
+
+import android.graphics.Bitmap
+import com.android.quickstep.recents.usecase.GetThumbnailUseCase
+import com.android.quickstep.recents.usecase.SysUiStatusNavFlagsUseCase
+
+class TaskContainerViewModel(
+    private val sysUiStatusNavFlagsUseCase: SysUiStatusNavFlagsUseCase,
+    private val getThumbnailUseCase: GetThumbnailUseCase
+) {
+    fun getThumbnail(taskId: Int): Bitmap? = getThumbnailUseCase.run(taskId)
+
+    fun getSysUiStatusNavFlags(taskId: Int) =
+        sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(taskId)
+}
diff --git a/quickstep/src/com/android/quickstep/util/AnimUtils.java b/quickstep/src/com/android/quickstep/util/AnimUtils.java
index 8e3d44f..31aca03 100644
--- a/quickstep/src/com/android/quickstep/util/AnimUtils.java
+++ b/quickstep/src/com/android/quickstep/util/AnimUtils.java
@@ -17,18 +17,28 @@
 package com.android.quickstep.util;
 
 import static com.android.app.animation.Interpolators.clampToProgress;
+import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 
+import android.animation.AnimatorSet;
+import android.annotation.NonNull;
 import android.os.Bundle;
 import android.os.IRemoteCallback;
 import android.view.animation.Interpolator;
 
+import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.statemanager.BaseState;
+import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.util.RunnableList;
+import com.android.quickstep.views.RecentsViewContainer;
 
 /**
  * Utility class containing methods to help manage animations, interpolators, and timings.
  */
 public class AnimUtils {
+    private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350;
+
     /**
      * Fetches device-specific timings for the Overview > Split animation
      * (splitscreen initiated from Overview).
@@ -59,6 +69,33 @@
     }
 
     /**
+     * Synchronizes the timing for the split dismiss animation to the current transition to
+     * NORMAL (launcher home/workspace)
+     */
+    public static void goToNormalStateWithSplitDismissal(@NonNull StateManager stateManager,
+            @NonNull RecentsViewContainer container,
+            @NonNull StatsLogManager.LauncherEvent exitReason,
+            @NonNull SplitAnimationController animationController) {
+        StateAnimationConfig config = new StateAnimationConfig();
+        BaseState startState = stateManager.getState();
+        long duration = startState.getTransitionDuration(container.asContext(),
+                false /*isToState*/);
+        if (duration == 0) {
+            // Case where we're in contextual on workspace (NORMAL), which by default has 0
+            // transition duration
+            duration = DURATION_DEFAULT_SPLIT_DISMISS;
+        }
+        config.duration = duration;
+        AnimatorSet stateAnim = stateManager.createAtomicAnimation(
+                startState, NORMAL, config);
+        AnimatorSet dismissAnim = animationController
+                .createPlaceholderDismissAnim(container, exitReason, duration);
+        stateAnim.play(dismissAnim);
+        stateManager.setCurrentAnimation(stateAnim, NORMAL);
+        stateAnim.start();
+    }
+
+    /**
      * Returns a IRemoteCallback which completes the provided list as a result
      */
     public static IRemoteCallback completeRunnableListCallback(RunnableList list) {
diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java
deleted file mode 100644
index bb4a7ec..0000000
--- a/quickstep/src/com/android/quickstep/views/IconView.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.quickstep.views;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.View;
-import android.widget.FrameLayout;
-
-import androidx.annotation.Nullable;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.MultiValueAlpha;
-import com.android.launcher3.views.ActivityContext;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.quickstep.util.RecentsOrientedState;
-
-/**
- * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
- * when the drawable changes.
- */
-public class IconView extends View implements TaskViewIcon {
-    private static final int NUM_ALPHA_CHANNELS = 2;
-    private static final int INDEX_CONTENT_ALPHA = 0;
-    private static final int INDEX_MODAL_ALPHA = 1;
-
-    private final MultiValueAlpha mMultiValueAlpha;
-
-    @Nullable
-    private Drawable mDrawable;
-    private int mDrawableWidth, mDrawableHeight;
-
-    public IconView(Context context) {
-        this(context, null);
-    }
-
-    public IconView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public IconView(Context context, AttributeSet attrs, int defStyleAttr) {
-        this(context, attrs, defStyleAttr, 0);
-    }
-
-    public IconView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
-            int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-        mMultiValueAlpha = new MultiValueAlpha(this, NUM_ALPHA_CHANNELS);
-        mMultiValueAlpha.setUpdateVisibility(/* updateVisibility= */ true);
-    }
-
-    /**
-     * Sets a {@link Drawable} to be displayed.
-     */
-    @Override
-    public void setDrawable(@Nullable Drawable d) {
-        if (mDrawable != null) {
-            mDrawable.setCallback(null);
-        }
-        mDrawable = d;
-        if (mDrawable != null) {
-            mDrawable.setCallback(this);
-            setDrawableSizeInternal(getWidth(), getHeight());
-        }
-        invalidate();
-    }
-
-    /**
-     * Sets the size of the icon drawable.
-     */
-    @Override
-    public void setDrawableSize(int iconWidth, int iconHeight) {
-        mDrawableWidth = iconWidth;
-        mDrawableHeight = iconHeight;
-        if (mDrawable != null) {
-            setDrawableSizeInternal(getWidth(), getHeight());
-        }
-    }
-
-    private void setDrawableSizeInternal(int selfWidth, int selfHeight) {
-        Rect selfRect = new Rect(0, 0, selfWidth, selfHeight);
-        Rect drawableRect = new Rect();
-        Gravity.apply(Gravity.CENTER, mDrawableWidth, mDrawableHeight, selfRect, drawableRect);
-        mDrawable.setBounds(drawableRect);
-    }
-
-    @Override
-    @Nullable
-    public Drawable getDrawable() {
-        return mDrawable;
-    }
-
-    @Override
-    public int getDrawableWidth() {
-        return mDrawableWidth;
-    }
-
-    @Override
-    public int getDrawableHeight() {
-        return mDrawableHeight;
-    }
-
-    @Override
-    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
-        super.onSizeChanged(w, h, oldw, oldh);
-        if (mDrawable != null) {
-            setDrawableSizeInternal(w, h);
-        }
-    }
-
-    @Override
-    protected boolean verifyDrawable(Drawable who) {
-        return super.verifyDrawable(who) || who == mDrawable;
-    }
-
-    @Override
-    protected void drawableStateChanged() {
-        super.drawableStateChanged();
-
-        final Drawable drawable = mDrawable;
-        if (drawable != null && drawable.isStateful()
-                && drawable.setState(getDrawableState())) {
-            invalidateDrawable(drawable);
-        }
-    }
-
-    @Override
-    protected void onDraw(Canvas canvas) {
-        if (mDrawable != null) {
-            mDrawable.draw(canvas);
-        }
-    }
-
-    @Override
-    public boolean hasOverlappingRendering() {
-        return false;
-    }
-
-    @Override
-    public void setContentAlpha(float alpha) {
-        mMultiValueAlpha.get(INDEX_CONTENT_ALPHA).setValue(alpha);
-    }
-
-    @Override
-    public void setModalAlpha(float alpha) {
-        mMultiValueAlpha.get(INDEX_MODAL_ALPHA).setValue(alpha);
-    }
-
-    /**
-     * Set the tint color of the icon, useful for scrimming or dimming.
-     *
-     * @param color to blend in.
-     * @param amount [0,1] 0 no tint, 1 full tint
-     */
-    @Override
-    public void setIconColorTint(int color, float amount) {
-        if (mDrawable != null) {
-            mDrawable.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount));
-        }
-    }
-
-    @Override
-    public void setIconOrientation(RecentsOrientedState orientationState, boolean isGridTask) {
-        RecentsPagedOrientationHandler orientationHandler =
-                orientationState.getOrientationHandler();
-        boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
-        DeviceProfile deviceProfile =
-                ActivityContext.lookupContext(getContext()).getDeviceProfile();
-
-        FrameLayout.LayoutParams iconParams = (FrameLayout.LayoutParams) getLayoutParams();
-
-        int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
-        int taskIconHeight = deviceProfile.overviewTaskIconSizePx;
-        int taskMargin = deviceProfile.overviewTaskMarginPx;
-
-        orientationHandler.setTaskIconParams(iconParams, taskMargin, taskIconHeight,
-                thumbnailTopMargin, isRtl);
-        iconParams.width = iconParams.height = taskIconHeight;
-        setLayoutParams(iconParams);
-
-        setRotation(orientationHandler.getDegreesRotated());
-        int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
-                : deviceProfile.overviewTaskIconDrawableSizePx;
-        setDrawableSize(iconDrawableSize, iconDrawableSize);
-    }
-
-    @Override
-    public View asView() {
-        return this;
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/views/IconView.kt b/quickstep/src/com/android/quickstep/views/IconView.kt
new file mode 100644
index 0000000..583207f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/IconView.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.views
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.View
+import android.widget.FrameLayout
+import androidx.core.view.updateLayoutParams
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.MultiValueAlpha
+import com.android.launcher3.views.ActivityContext
+import com.android.quickstep.util.RecentsOrientedState
+
+/**
+ * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout
+ * when the drawable changes.
+ */
+class IconView : View, TaskViewIcon {
+    private val multiValueAlpha: MultiValueAlpha = MultiValueAlpha(this, NUM_ALPHA_CHANNELS)
+    private var drawable: Drawable? = null
+    private var drawableWidth = 0
+    private var drawableHeight = 0
+
+    constructor(context: Context) : super(context)
+
+    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+    constructor(
+        context: Context,
+        attrs: AttributeSet?,
+        defStyleAttr: Int,
+    ) : super(context, attrs, defStyleAttr)
+
+    init {
+        multiValueAlpha.setUpdateVisibility(true)
+    }
+
+    /** Sets a [Drawable] to be displayed. */
+    override fun setDrawable(d: Drawable?) {
+        drawable?.callback = null
+
+        drawable = d
+        drawable?.let {
+            it.callback = this
+            setDrawableSizeInternal(width, height)
+        }
+        invalidate()
+    }
+
+    /** Sets the size of the icon drawable. */
+    override fun setDrawableSize(iconWidth: Int, iconHeight: Int) {
+        drawableWidth = iconWidth
+        drawableHeight = iconHeight
+        drawable?.let { setDrawableSizeInternal(width, height) }
+    }
+
+    private fun setDrawableSizeInternal(selfWidth: Int, selfHeight: Int) {
+        val selfRect = Rect(0, 0, selfWidth, selfHeight)
+        val drawableRect = Rect()
+        Gravity.apply(Gravity.CENTER, drawableWidth, drawableHeight, selfRect, drawableRect)
+        drawable?.bounds = drawableRect
+    }
+
+    override fun getDrawable(): Drawable? = drawable
+
+    override fun getDrawableWidth(): Int = drawableWidth
+
+    override fun getDrawableHeight(): Int = drawableHeight
+
+    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+        super.onSizeChanged(w, h, oldw, oldh)
+        drawable?.let { setDrawableSizeInternal(w, h) }
+    }
+
+    override fun verifyDrawable(who: Drawable): Boolean =
+        super.verifyDrawable(who) || who === drawable
+
+    override fun drawableStateChanged() {
+        super.drawableStateChanged()
+        drawable?.let {
+            if (it.isStateful && it.setState(drawableState)) {
+                invalidateDrawable(it)
+            }
+        }
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        drawable?.draw(canvas)
+    }
+
+    override fun hasOverlappingRendering(): Boolean = false
+
+    override fun setContentAlpha(alpha: Float) {
+        multiValueAlpha[INDEX_CONTENT_ALPHA].setValue(alpha)
+    }
+
+    override fun setModalAlpha(alpha: Float) {
+        multiValueAlpha[INDEX_MODAL_ALPHA].setValue(alpha)
+    }
+
+    /**
+     * Set the tint color of the icon, useful for scrimming or dimming.
+     *
+     * @param color to blend in.
+     * @param amount [0,1] 0 no tint, 1 full tint
+     */
+    override fun setIconColorTint(color: Int, amount: Float) {
+        drawable?.colorFilter = Utilities.makeColorTintingColorFilter(color, amount)
+    }
+
+    override fun setIconOrientation(orientationState: RecentsOrientedState, isGridTask: Boolean) {
+        val orientationHandler = orientationState.orientationHandler
+        val deviceProfile: DeviceProfile =
+            (ActivityContext.lookupContext(context) as ActivityContext).getDeviceProfile()
+        orientationHandler.setTaskIconParams(
+            iconParams = getLayoutParams() as FrameLayout.LayoutParams,
+            taskIconMargin = deviceProfile.overviewTaskMarginPx,
+            taskIconHeight = deviceProfile.overviewTaskIconSizePx,
+            thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx,
+            isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
+        )
+        updateLayoutParams<FrameLayout.LayoutParams> {
+            height = deviceProfile.overviewTaskIconSizePx
+            width = height
+        }
+        setRotation(orientationHandler.degreesRotated)
+        val iconDrawableSize =
+            if (isGridTask) deviceProfile.overviewTaskIconDrawableSizeGridPx
+            else deviceProfile.overviewTaskIconDrawableSizePx
+        setDrawableSize(iconDrawableSize, iconDrawableSize)
+    }
+
+    override fun asView(): View = this
+
+    companion object {
+        private const val NUM_ALPHA_CHANNELS = 2
+        private const val INDEX_CONTENT_ALPHA = 0
+        private const val INDEX_MODAL_ALPHA = 1
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index e48a7c6..b2d2302 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -54,6 +54,7 @@
 import com.android.quickstep.LauncherActivityInterface;
 import com.android.quickstep.RotationTouchHelper;
 import com.android.quickstep.SystemUiProxy;
+import com.android.quickstep.util.AnimUtils;
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.systemui.shared.recents.model.Task;
 
@@ -91,10 +92,12 @@
     protected void handleStartHome(boolean animated) {
         StateManager stateManager = getStateManager();
         animated &= stateManager.shouldAnimateStateChange();
-        stateManager.goToState(NORMAL, animated);
-        if (FeatureFlags.enableSplitContextually()) {
-            mSplitSelectStateController.getSplitAnimationController()
-                    .playPlaceholderDismissAnim(mContainer, LAUNCHER_SPLIT_SELECTION_EXIT_HOME);
+        if (mSplitSelectStateController.isSplitSelectActive()) {
+            AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer,
+                    LAUNCHER_SPLIT_SELECTION_EXIT_HOME,
+                    mSplitSelectStateController.getSplitAnimationController());
+        } else {
+            stateManager.goToState(NORMAL, animated);
         }
         AbstractFloatingView.closeAllOpenViews(mContainer, animated);
     }
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
index 3d994e8..f6393e4 100644
--- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -16,13 +16,11 @@
 
 package com.android.quickstep.views;
 
-import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON;
 import static com.android.settingslib.widget.theme.R.dimen.settingslib_preferred_minimum_touch_target;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
@@ -42,9 +40,8 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.statemanager.BaseState;
 import com.android.launcher3.statemanager.StateManager;
-import com.android.launcher3.states.StateAnimationConfig;
+import com.android.quickstep.util.AnimUtils;
 import com.android.quickstep.util.SplitSelectStateController;
 
 /**
@@ -57,7 +54,6 @@
 public class SplitInstructionsView extends LinearLayout {
     private static final int BOUNCE_DURATION = 250;
     private static final float BOUNCE_HEIGHT = 20;
-    private static final int DURATION_DEFAULT_SPLIT_DISMISS = 350;
 
     private final RecentsViewContainer mContainer;
     public boolean mIsCurrentlyAnimating = false;
@@ -165,25 +161,11 @@
     private void exitSplitSelection() {
         RecentsView recentsView = mContainer.getOverviewPanel();
         SplitSelectStateController splitSelectController = recentsView.getSplitSelectController();
-
         StateManager stateManager = recentsView.getStateManager();
-        BaseState startState = stateManager.getState();
-        long duration = startState.getTransitionDuration(mContainer.asContext(), false);
-        if (duration == 0) {
-            // Case where we're in contextual on workspace (NORMAL), which by default has 0
-            // transition duration
-            duration = DURATION_DEFAULT_SPLIT_DISMISS;
-        }
-        StateAnimationConfig config = new StateAnimationConfig();
-        config.duration = duration;
-        AnimatorSet stateAnim = stateManager.createAtomicAnimation(
-                startState, NORMAL, config);
-        AnimatorSet dismissAnim = splitSelectController.getSplitAnimationController()
-                .createPlaceholderDismissAnim(mContainer,
-                        LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON, duration);
-        stateAnim.play(dismissAnim);
-        stateManager.setCurrentAnimation(stateAnim, NORMAL);
-        stateAnim.start();
+
+        AnimUtils.goToNormalStateWithSplitDismissal(stateManager, mContainer,
+                LAUNCHER_SPLIT_SELECTION_EXIT_CANCEL_BUTTON,
+                splitSelectController.getSplitAnimationController());
     }
 
     void ensureProperRotation() {
diff --git a/quickstep/src/com/android/quickstep/views/TaskContainer.kt b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
index 79725c6..e7a8720 100644
--- a/quickstep/src/com/android/quickstep/views/TaskContainer.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskContainer.kt
@@ -33,7 +33,7 @@
 import com.android.quickstep.recents.di.get
 import com.android.quickstep.recents.di.getScope
 import com.android.quickstep.recents.di.inject
-import com.android.quickstep.recents.usecase.GetThumbnailUseCase
+import com.android.quickstep.recents.viewmodel.TaskContainerViewModel
 import com.android.quickstep.task.thumbnail.TaskThumbnail
 import com.android.quickstep.task.thumbnail.TaskThumbnailView
 import com.android.quickstep.task.viewmodel.TaskContainerData
@@ -61,10 +61,18 @@
 ) {
     val overlay: TaskOverlayFactory.TaskOverlay<*> = taskOverlayFactory.createOverlay(this)
     lateinit var taskContainerData: TaskContainerData
-    private val getThumbnailUseCase: GetThumbnailUseCase by RecentsDependencies.inject()
+
     private val taskThumbnailViewModel: TaskThumbnailViewModel by
         RecentsDependencies.inject(snapshotView)
 
+    // TODO(b/335649589): Ideally create and obtain this from DI.
+    private val taskContainerViewModel: TaskContainerViewModel by lazy {
+        TaskContainerViewModel(
+            sysUiStatusNavFlagsUseCase = RecentsDependencies.get(),
+            getThumbnailUseCase = RecentsDependencies.get()
+        )
+    }
+
     init {
         if (enableRefactorTaskThumbnail()) {
             require(snapshotView is TaskThumbnailView)
@@ -84,7 +92,7 @@
     val splitAnimationThumbnail: Bitmap?
         get() =
             if (enableRefactorTaskThumbnail()) {
-                getThumbnailUseCase.run(task.key.id)
+                taskContainerViewModel.getThumbnail(task.key.id)
             } else {
                 thumbnailViewDeprecated.thumbnail
             }
@@ -110,7 +118,9 @@
     // TODO(b/350743460) Support sysUiStatusNavFlags for new TTV.
     val sysUiStatusNavFlags: Int
         get() =
-            if (enableRefactorTaskThumbnail()) 0 else thumbnailViewDeprecated.sysUiStatusNavFlags
+            if (enableRefactorTaskThumbnail())
+                taskContainerViewModel.getSysUiStatusNavFlags(task.key.id)
+            else thumbnailViewDeprecated.sysUiStatusNavFlags
 
     /** Builds proto for logging */
     val itemInfo: WorkspaceItemInfo
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
new file mode 100644
index 0000000..ba4e206
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/recents/usecase/SysUiStatusNavFlagsUseCaseTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 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.quickstep.recents.usecase
+
+import android.content.ComponentName
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Color
+import com.android.quickstep.recents.data.FakeTasksRepository
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/** Test for [SysUiStatusNavFlagsUseCase] */
+class SysUiStatusNavFlagsUseCaseTest {
+    private lateinit var tasksRepository: FakeTasksRepository
+    private lateinit var sysUiStatusNavFlagsUseCase: SysUiStatusNavFlagsUseCase
+
+    @Before
+    fun setup() {
+        tasksRepository = FakeTasksRepository()
+        sysUiStatusNavFlagsUseCase = SysUiStatusNavFlagsUseCase(tasksRepository)
+        initTaskRepository()
+    }
+
+    @Test
+    fun onLightAppearanceReturnExpectedFlags() {
+        assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(FIRST_TASK_ID))
+            .isEqualTo(FLAGS_APPEARANCE_LIGHT_THEME)
+    }
+
+    @Test
+    fun onDarkAppearanceReturnExpectedFlags() {
+        assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(SECOND_TASK_ID))
+            .isEqualTo(FLAGS_APPEARANCE_DARK_THEME)
+    }
+
+    @Test
+    fun whenThumbnailIsNullReturnDefault() {
+        assertThat(sysUiStatusNavFlagsUseCase.getSysUiStatusNavFlags(UNKNOWN_TASK_ID))
+            .isEqualTo(FLAGS_DEFAULT)
+    }
+
+    private fun initTaskRepository() {
+        val firstTask =
+            Task(Task.TaskKey(FIRST_TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2000)).apply {
+                colorBackground = Color.BLACK
+            }
+        val firstThumbnailData =
+            ThumbnailData(
+                thumbnail =
+                    mock<Bitmap>().apply {
+                        whenever(width).thenReturn(THUMBNAIL_WIDTH)
+                        whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+                    },
+                appearance = APPEARANCE_LIGHT_THEME
+            )
+
+        val secondTask =
+            Task(Task.TaskKey(SECOND_TASK_ID, 0, Intent(), ComponentName("", ""), 0, 2005)).apply {
+                colorBackground = Color.BLACK
+            }
+        val secondThumbnailData =
+            ThumbnailData(
+                thumbnail =
+                    mock<Bitmap>().apply {
+                        whenever(width).thenReturn(THUMBNAIL_WIDTH)
+                        whenever(height).thenReturn(THUMBNAIL_HEIGHT)
+                    },
+                appearance = APPEARANCE_DARK_THEME
+            )
+
+        tasksRepository.seedTasks(listOf(firstTask, secondTask))
+        tasksRepository.seedThumbnailData(
+            mapOf(FIRST_TASK_ID to firstThumbnailData, SECOND_TASK_ID to secondThumbnailData)
+        )
+        tasksRepository.setVisibleTasks(listOf(FIRST_TASK_ID, SECOND_TASK_ID))
+    }
+
+    companion object {
+        const val FIRST_TASK_ID = 0
+        const val SECOND_TASK_ID = 100
+        const val UNKNOWN_TASK_ID = 404
+        const val THUMBNAIL_WIDTH = 100
+        const val THUMBNAIL_HEIGHT = 200
+        const val APPEARANCE_LIGHT_THEME = 24
+        const val FLAGS_APPEARANCE_LIGHT_THEME = 5
+        const val APPEARANCE_DARK_THEME = 0
+        const val FLAGS_APPEARANCE_DARK_THEME = 10
+        const val FLAGS_DEFAULT = 0
+    }
+}
diff --git a/res/drawable/rounded_action_button.xml b/res/drawable/rounded_action_button.xml
index ddd3042..ebfa996 100644
--- a/res/drawable/rounded_action_button.xml
+++ b/res/drawable/rounded_action_button.xml
@@ -22,8 +22,5 @@
     <stroke
         android:width="1dp"
         android:color="?attr/materialColorSurfaceContainerLow" />
-    <padding
-        android:left="@dimen/rounded_button_padding"
-        android:right="@dimen/rounded_button_padding" />
 </shape>
 
diff --git a/res/layout/work_apps_edu.xml b/res/layout/work_apps_edu.xml
index c581ae3..a45d585 100644
--- a/res/layout/work_apps_edu.xml
+++ b/res/layout/work_apps_edu.xml
@@ -44,8 +44,7 @@
         <FrameLayout
             android:layout_width="@dimen/rounded_button_width"
             android:layout_height="@dimen/rounded_button_width"
-            android:background="@drawable/rounded_action_button"
-            android:padding="@dimen/rounded_button_padding">
+            android:background="@drawable/rounded_action_button">
             <ImageButton
                 android:id="@+id/action_btn"
                 android:layout_width="@dimen/x_icon_size"
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 05724e2..af91b5a 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -172,8 +172,6 @@
     <dimen name="padded_rounded_button_height">48dp</dimen>
     <dimen name="rounded_button_height">48dp</dimen>
     <dimen name="rounded_button_radius">200dp</dimen>
-    <dimen name="rounded_button_padding">8dp</dimen>
-
 
     <!-- Widget tray -->
     <dimen name="widget_cell_vertical_padding">8dp</dimen>
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index db693f0..8b1f42b 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -157,7 +157,8 @@
                         isOverFolderOrSearchBar = isEventOverView(topView, ev) ||
                                 isEventOverAccessibleDropTargetBar(ev);
                         if (!isOverFolderOrSearchBar) {
-                            sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
+                            sendTapOutsideFolderAccessibilityEvent(
+                                    currentFolder.getIsEditingName());
                             mHoverPointClosesFolder = true;
                             return true;
                         }
@@ -167,7 +168,8 @@
                         isOverFolderOrSearchBar = isEventOverView(topView, ev) ||
                                 isEventOverAccessibleDropTargetBar(ev);
                         if (!isOverFolderOrSearchBar && !mHoverPointClosesFolder) {
-                            sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
+                            sendTapOutsideFolderAccessibilityEvent(
+                                    currentFolder.getIsEditingName());
                             mHoverPointClosesFolder = true;
                             return true;
                         } else if (!isOverFolderOrSearchBar) {
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 175ab4e..3edf1f2 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -136,7 +136,8 @@
      * We avoid measuring {@link #mContent} with a 0 width or height, as this
      * results in CellLayout being measured as UNSPECIFIED, which it does not support.
      */
-    private static final int MIN_CONTENT_DIMEN = 5;
+    @VisibleForTesting
+    static final int MIN_CONTENT_DIMEN = 5;
 
     public static final int STATE_CLOSED = 0;
     public static final int STATE_ANIMATING = 1;
@@ -144,7 +145,8 @@
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({STATE_CLOSED, STATE_ANIMATING, STATE_OPEN})
-    public @interface FolderState {}
+    public @interface FolderState {
+    }
 
     /**
      * Time for which the scroll hint is shown before automatically changing page.
@@ -165,7 +167,7 @@
     private static final int FOLDER_COLOR_ANIMATION_DURATION = 200;
 
     private static final int REORDER_DELAY = 250;
-    private static final int ON_EXIT_CLOSE_DELAY = 400;
+    static final int ON_EXIT_CLOSE_DELAY = 400;
     private static final Rect sTempRect = new Rect();
     private static final int MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION = 10;
 
@@ -185,10 +187,10 @@
                 || itemType == ITEM_TYPE_APP_PAIR;
     }
 
-    private final Alarm mReorderAlarm = new Alarm(Looper.getMainLooper());
-    private final Alarm mOnExitAlarm = new Alarm(Looper.getMainLooper());
-    private final Alarm mOnScrollHintAlarm = new Alarm(Looper.getMainLooper());
-    final Alarm mScrollPauseAlarm = new Alarm(Looper.getMainLooper());
+    private Alarm mReorderAlarm = new Alarm(Looper.getMainLooper());
+    private Alarm mOnExitAlarm = new Alarm(Looper.getMainLooper());
+    private Alarm mOnScrollHintAlarm = new Alarm(Looper.getMainLooper());
+    private Alarm mScrollPauseAlarm = new Alarm(Looper.getMainLooper());
 
     final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
 
@@ -198,7 +200,7 @@
     // Folder can be displayed in Launcher's activity or a separate window (e.g. Taskbar).
     // Anything specific to Launcher should use mLauncherDelegate, otherwise should
     // use mActivityContext.
-    protected final LauncherDelegate mLauncherDelegate;
+    protected LauncherDelegate mLauncherDelegate;
     protected final ActivityContext mActivityContext;
 
     protected DragController mDragController;
@@ -211,7 +213,7 @@
 
     @Thunk
     FolderPagedView mContent;
-    public FolderNameEditText mFolderName;
+    private FolderNameEditText mFolderName;
     private PageIndicatorDots mPageIndicator;
 
     protected View mFooter;
@@ -235,10 +237,10 @@
     private OnFolderStateChangedListener mPriorityOnFolderStateChangedListener;
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mRearrangeOnClose = false;
-    boolean mItemsInvalidated = false;
+    private boolean mItemsInvalidated = false;
     private View mCurrentDragView;
     private boolean mIsExternalDrag;
-    private boolean mDragInProgress = false;
+    private boolean mIsDragInProgress = false;
     private boolean mDeleteFolderOnDropCompleted = false;
     private boolean mSuppressFolderDeletion = false;
     private boolean mItemAddedBackToSelfViaIcon = false;
@@ -251,7 +253,7 @@
     private int mScrollAreaOffset;
 
     @Thunk
-    int mScrollHintDir = SCROLL_NONE;
+    private int mScrollHintDir = SCROLL_NONE;
     @Thunk
     int mCurrentScrollDir = SCROLL_NONE;
 
@@ -316,9 +318,9 @@
                 | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
         mFolderName.forceDisableSuggestions(true);
         mFolderName.setPadding(mFolderName.getPaddingLeft(),
-                (mFooterHeight - mFolderName.getLineHeight()) / 2,
+                (getFooterHeight() - mFolderName.getLineHeight()) / 2,
                 mFolderName.getPaddingRight(),
-                (mFooterHeight - mFolderName.getLineHeight()) / 2);
+                (getFooterHeight() - mFolderName.getLineHeight()) / 2);
 
         mKeyboardInsetAnimationCallback = new KeyboardInsetAnimationCallback(this);
         setWindowInsetsAnimationCallback(mKeyboardInsetAnimationCallback);
@@ -326,42 +328,54 @@
 
     public boolean onLongClick(View v) {
         // Return if global dragging is not enabled
-        if (!mLauncherDelegate.isDraggingEnabled()) return true;
+        if (!getIsLauncherDraggingEnabled()) return true;
         return startDrag(v, new DragOptions());
     }
 
+    @VisibleForTesting
+    boolean getIsLauncherDraggingEnabled() {
+        return mLauncherDelegate.isDraggingEnabled();
+    }
+
     public boolean startDrag(View v, DragOptions options) {
         Object tag = v.getTag();
         if (tag instanceof ItemInfo item) {
             mEmptyCellRank = item.rank;
             mCurrentDragView = v;
 
-            mDragController.addDragListener(this);
-            if (options.isAccessibleDrag) {
-                mDragController.addDragListener(new AccessibleDragListenerAdapter(
-                        mContent, FolderAccessibilityHelper::new) {
-                    @Override
-                    protected void enableAccessibleDrag(boolean enable,
-                            @Nullable DragObject dragObject) {
-                        super.enableAccessibleDrag(enable, dragObject);
-                        mFooter.setImportantForAccessibility(enable
-                                ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                                : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-                    }
-                });
-            }
-
-            mLauncherDelegate.beginDragShared(v, this, options);
+            addDragListener(options);
+            callBeginDragShared(v, options);
         }
         return true;
     }
 
+    void callBeginDragShared(View v, DragOptions options) {
+        mLauncherDelegate.beginDragShared(v, this, options);
+    }
+
+    void addDragListener(DragOptions options) {
+        getDragController().addDragListener(this);
+        if (!options.isAccessibleDrag) {
+            return;
+        }
+        getDragController().addDragListener(new AccessibleDragListenerAdapter(
+                mContent, FolderAccessibilityHelper::new) {
+            @Override
+            protected void enableAccessibleDrag(boolean enable,
+                    @Nullable DragObject dragObject) {
+                super.enableAccessibleDrag(enable, dragObject);
+                mFooter.setImportantForAccessibility(enable
+                        ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                        : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+            }
+        });
+    }
+
     @Override
     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
         if (dragObject.dragSource != this) {
             return;
         }
-
         mContent.removeItem(mCurrentDragView);
         mItemsInvalidated = true;
 
@@ -370,29 +384,23 @@
         try (SuppressInfoChanges s = new SuppressInfoChanges()) {
             mInfo.remove(dragObject.dragInfo, true);
         }
-        mDragInProgress = true;
+        mIsDragInProgress = true;
         mItemAddedBackToSelfViaIcon = false;
     }
 
     @Override
     public void onDragEnd() {
-        if (mIsExternalDrag && mDragInProgress) {
+        if (mIsExternalDrag && mIsDragInProgress) {
             completeDragExit();
         }
-        mDragInProgress = false;
-        mDragController.removeDragListener(this);
-    }
-
-    public boolean isEditingName() {
-        return mIsEditingName;
+        mIsDragInProgress = false;
+        getDragController().removeDragListener(this);
     }
 
     public void startEditingFolderName() {
-        post(() -> {
-            showLabelSuggestions();
-            mFolderName.setHint("");
-            mIsEditingName = true;
-        });
+        showLabelSuggestions();
+        mFolderName.setHint("");
+        mIsEditingName = true;
     }
 
     @Override
@@ -460,7 +468,11 @@
         return mFolderIcon;
     }
 
-    public void setDragController(DragController dragController) {
+    DragController getDragController() {
+        return mDragController;
+    }
+
+    void setDragController(DragController dragController) {
         mDragController = dragController;
     }
 
@@ -541,7 +553,7 @@
      * Show suggested folder title in FolderEditText if the first suggestion is non-empty, push
      * rest of the suggestions to InputMethodManager.
      */
-    private void showLabelSuggestions() {
+    void showLabelSuggestions() {
         if (mInfo.suggestedFolderNames == null) {
             return;
         }
@@ -635,11 +647,11 @@
      */
     public void beginExternalDrag() {
         mIsExternalDrag = true;
-        mDragInProgress = true;
+        mIsDragInProgress = true;
 
         // Since this folder opened by another controller, it might not get onDrop or
         // onDropComplete. Perform cleanup once drag-n-drop ends.
-        mDragController.addDragListener(this);
+        getDragController().addDragListener(this);
 
         ArrayList<ItemInfo> items = new ArrayList<>(mInfo.getContents());
         mEmptyCellRank = items.size();
@@ -663,16 +675,12 @@
      * is played.
      */
     private void animateOpen(List<ItemInfo> items, int pageNo) {
-        if (items == null || items.size() <= 1) {
-            Log.d(TAG, "Couldn't animate folder open because items is: " + items);
+        if (!shouldAnimateOpen(items)) {
             return;
         }
 
         Folder openFolder = getOpen(mActivityContext);
-        if (openFolder != null && openFolder != this) {
-            // Close any open folder before opening a folder.
-            openFolder.close(true);
-        }
+        closeOpenFolder(openFolder);
 
         mContent.bindItems(items);
         centerAboutIcon();
@@ -686,7 +694,7 @@
         // There was a one-off crash where the folder had a parent already.
         if (getParent() == null) {
             dragLayer.addView(this);
-            mDragController.addDropTarget(this);
+            getDragController().addDropTarget(this);
         } else {
             if (FeatureFlags.IS_STUDIO_BUILD) {
                 Log.e(TAG, "Opening folder (" + this + ") which already has a parent:"
@@ -735,7 +743,7 @@
 
             // Do not update the flag if we are in drag mode. The flag will be updated, when we
             // actually drop the icon.
-            final boolean updateAnimationFlag = !mDragInProgress;
+            final boolean updateAnimationFlag = !mIsDragInProgress;
             anim.addListener(new AnimatorListenerAdapter() {
 
                 @SuppressLint("InlinedApi")
@@ -769,12 +777,36 @@
         anim.start();
 
         // Make sure the folder picks up the last drag move even if the finger doesn't move.
-        if (mDragController.isDragging()) {
-            mDragController.forceTouchMove();
+        if (getDragController().isDragging()) {
+            getDragController().forceTouchMove();
         }
         mContent.verifyVisibleHighResIcons(mContent.getNextPage());
     }
 
+    /**
+     * Determines whether we should animate the folder opening.
+     */
+    boolean shouldAnimateOpen(List<ItemInfo> items) {
+        if (items == null || items.size() <= 1) {
+            Log.d(TAG, "Couldn't animate folder open because items is: " + items);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * If there's a folder already open, we want to close it before opening another one.
+     */
+    @VisibleForTesting
+    boolean closeOpenFolder(Folder openFolder) {
+        if (openFolder != null && openFolder != this) {
+            // Close any open folder before opening a folder.
+            openFolder.close(true);
+            return true;
+        }
+        return false;
+    }
+
     @Override
     protected boolean isOfType(int type) {
         return (type & TYPE_FOLDER) != 0;
@@ -788,7 +820,7 @@
             mCurrentAnimator.cancel();
         }
 
-        if (isEditingName()) {
+        if (mIsEditingName) {
             mFolderName.dispatchBackKey();
         }
 
@@ -872,7 +904,7 @@
         if (parent != null) {
             parent.removeView(this);
         }
-        mDragController.removeDropTarget(this);
+        getDragController().removeDropTarget(this);
         clearFocus();
         if (mFolderIcon != null) {
             mFolderIcon.setVisibility(View.VISIBLE);
@@ -893,12 +925,12 @@
             mRearrangeOnClose = false;
         }
         if (getItemCount() <= 1) {
-            if (!mDragInProgress && !mSuppressFolderDeletion) {
+            if (!mIsDragInProgress && !mSuppressFolderDeletion) {
                 replaceFolderWithFinalItem();
-            } else if (mDragInProgress) {
+            } else if (mIsDragInProgress) {
                 mDeleteFolderOnDropCompleted = true;
             }
-        } else if (!mDragInProgress) {
+        } else if (!mIsDragInProgress) {
             mContent.unbindItems();
         }
         mSuppressFolderDeletion = false;
@@ -1018,7 +1050,8 @@
         }
     }
 
-    private void clearDragInfo() {
+    @VisibleForTesting
+    void clearDragInfo() {
         mCurrentDragView = null;
         mIsExternalDrag = false;
     }
@@ -1059,7 +1092,8 @@
             if (getItemCount() <= 1) {
                 mDeleteFolderOnDropCompleted = true;
             }
-            if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon && target != this) {
+            if (mDeleteFolderOnDropCompleted && !mItemAddedBackToSelfViaIcon
+                    && target != this) {
                 replaceFolderWithFinalItem();
             }
         } else {
@@ -1090,7 +1124,7 @@
         }
 
         mDeleteFolderOnDropCompleted = false;
-        mDragInProgress = false;
+        mIsDragInProgress = false;
         mItemAddedBackToSelfViaIcon = false;
         mCurrentDragView = null;
 
@@ -1133,7 +1167,7 @@
     }
 
     public void notifyDrop() {
-        if (mDragInProgress) {
+        if (mIsDragInProgress) {
             mItemAddedBackToSelfViaIcon = true;
         }
     }
@@ -1176,28 +1210,41 @@
     }
 
     protected int getContentAreaHeight() {
-        DeviceProfile grid = mActivityContext.getDeviceProfile();
-        int maxContentAreaHeight = grid.availableHeightPx - grid.getTotalWorkspacePadding().y
-                - mFooterHeight;
-        int height = Math.min(maxContentAreaHeight,
+        int height = Math.min(getMaxContentAreaHeight(),
                 mContent.getDesiredHeight());
         return Math.max(height, MIN_CONTENT_DIMEN);
     }
 
-    private int getContentAreaWidth() {
+    @VisibleForTesting
+    int getMaxContentAreaHeight() {
+        DeviceProfile grid = mActivityContext.getDeviceProfile();
+        return grid.availableHeightPx - grid.getTotalWorkspacePadding().y
+                - getFooterHeight();
+    }
+
+    @VisibleForTesting
+    int getContentAreaWidth() {
         return Math.max(mContent.getDesiredWidth(), MIN_CONTENT_DIMEN);
     }
 
-    private int getFolderWidth() {
+    @VisibleForTesting
+    int getFolderWidth() {
         return getPaddingLeft() + getPaddingRight() + mContent.getDesiredWidth();
     }
 
-    private int getFolderHeight() {
+    @VisibleForTesting
+    int getFolderHeight() {
         return getFolderHeight(getContentAreaHeight());
     }
 
-    private int getFolderHeight(int contentAreaHeight) {
-        return getPaddingTop() + getPaddingBottom() + contentAreaHeight + mFooterHeight;
+    @VisibleForTesting
+    int getFolderHeight(int contentAreaHeight) {
+        return getPaddingTop() + getPaddingBottom() + contentAreaHeight + getFooterHeight();
+    }
+
+    @VisibleForTesting
+    int getFooterHeight() {
+        return mFooterHeight;
     }
 
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
@@ -1367,7 +1414,7 @@
         }
 
         // Clear the drag info, as it is no longer being dragged.
-        mDragInProgress = false;
+        mIsDragInProgress = false;
 
         if (mContent.getPageCount() > 1) {
             // The animation has already been shown while opening the folder.
@@ -1436,7 +1483,8 @@
         }
     }
 
-    private View getViewForInfo(final ItemInfo item) {
+    @VisibleForTesting
+    View getViewForInfo(final ItemInfo item) {
         return mContent.iterateOverItems((info, view) -> info == item);
     }
 
@@ -1494,7 +1542,7 @@
             if (hasFocus) {
                 mFromLabelState = mInfo.getFromLabelState();
                 mFromTitle = mInfo.title;
-                startEditingFolderName();
+                post(this::startEditingFolderName);
             } else {
                 StatsLogger statsLogger = mStatsLogManager.logger()
                         .withItemInfo(mInfo)
@@ -1627,7 +1675,7 @@
     /** Navigation bar back key or hardware input back key has been issued. */
     @Override
     public void onBackInvoked() {
-        if (isEditingName()) {
+        if (mIsEditingName) {
             mFolderName.dispatchBackKey();
         } else {
             super.onBackInvoked();
@@ -1639,7 +1687,7 @@
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
             BaseDragLayer dl = (BaseDragLayer) getParent();
 
-            if (isEditingName()) {
+            if (mIsEditingName) {
                 if (!dl.isEventOverView(mFolderName, ev)) {
                     mFolderName.dispatchBackKey();
                     return true;
@@ -1686,6 +1734,95 @@
         return mContent;
     }
 
+    @VisibleForTesting
+    void setItemAddedBackToSelfViaIcon(boolean value) {
+        mItemAddedBackToSelfViaIcon = value;
+    }
+
+    @VisibleForTesting
+    boolean getItemAddedBackToSelfViaIcon() {
+        return mItemAddedBackToSelfViaIcon;
+    }
+
+    @VisibleForTesting
+    void setIsDragInProgress(boolean value) {
+        mIsDragInProgress = value;
+    }
+
+    @VisibleForTesting
+    boolean getIsDragInProgress() {
+        return mIsDragInProgress;
+    }
+
+    @VisibleForTesting
+    View getCurrentDragView() {
+        return mCurrentDragView;
+    }
+
+    @VisibleForTesting
+    void setCurrentDragView(View view) {
+        mCurrentDragView = view;
+    }
+
+    @VisibleForTesting
+    boolean getItemsInvalidated() {
+        return mItemsInvalidated;
+    }
+
+    @VisibleForTesting
+    void setItemsInvalidated(boolean value) {
+        mItemsInvalidated = value;
+    }
+
+    @VisibleForTesting
+    boolean getIsExternalDrag() {
+        return mIsExternalDrag;
+    }
+
+    @VisibleForTesting
+    void setIsExternalDrag(boolean value) {
+        mIsExternalDrag = value;
+    }
+
+    public boolean getIsEditingName() {
+        return mIsEditingName;
+    }
+
+    @VisibleForTesting
+    void setIsEditingName(boolean value) {
+        mIsEditingName = value;
+    }
+
+    @VisibleForTesting
+    void setFolderName(FolderNameEditText value) {
+        mFolderName = value;
+    }
+
+    @VisibleForTesting
+    FolderNameEditText getFolderName() {
+        return mFolderName;
+    }
+
+    @VisibleForTesting
+    boolean getIsOpen() {
+        return mIsOpen;
+    }
+
+    @VisibleForTesting
+    void setIsOpen(boolean value) {
+        mIsOpen = value;
+    }
+
+    @VisibleForTesting
+    boolean getRearrangeOnClose() {
+        return mRearrangeOnClose;
+    }
+
+    @VisibleForTesting
+    void setRearrangeOnClose(boolean value) {
+        mRearrangeOnClose = value;
+    }
+
     /** Returns the height of the current folder's bottom edge from the bottom of the screen. */
     private int getHeightFromBottom() {
         BaseDragLayer.LayoutParams layoutParams = (BaseDragLayer.LayoutParams) getLayoutParams();
@@ -1696,10 +1833,15 @@
     }
 
     @VisibleForTesting
-    public boolean getDeleteFolderOnDropCompleted() {
+    boolean getDeleteFolderOnDropCompleted() {
         return mDeleteFolderOnDropCompleted;
     }
 
+    @VisibleForTesting
+    void setDeleteFolderOnDropCompleted(boolean value) {
+        mDeleteFolderOnDropCompleted = value;
+    }
+
     /**
      * Save this listener for the special case of when we update the state and concurrently
      * add another listener to {@link #mOnFolderStateChangedListeners} to avoid a
@@ -1709,7 +1851,13 @@
         mPriorityOnFolderStateChangedListener = listener;
     }
 
-    private void setState(@FolderState int newState) {
+    @VisibleForTesting
+    int getState() {
+        return mState;
+    }
+
+    @VisibleForTesting
+    void setState(@FolderState int newState) {
         mState = newState;
         if (mPriorityOnFolderStateChangedListener != null) {
             mPriorityOnFolderStateChangedListener.onFolderStateChanged(mState);
@@ -1721,6 +1869,60 @@
         }
     }
 
+    @VisibleForTesting
+    Alarm getOnExitAlarm() {
+        return mOnExitAlarm;
+    }
+
+    @VisibleForTesting
+    void setOnExitAlarm(Alarm value) {
+        mOnExitAlarm = value;
+    }
+
+    @VisibleForTesting
+    Alarm getReorderAlarm() {
+        return mReorderAlarm;
+    }
+
+    @VisibleForTesting
+    void setReorderAlarm(Alarm value) {
+        mReorderAlarm = value;
+    }
+
+    @VisibleForTesting
+    Alarm getOnScrollHintAlarm() {
+        return mOnScrollHintAlarm;
+    }
+
+    @VisibleForTesting
+    void setOnScrollHintAlarm(Alarm value) {
+        mOnScrollHintAlarm = value;
+    }
+
+    @VisibleForTesting
+    Alarm getScrollPauseAlarm() {
+        return mScrollPauseAlarm;
+    }
+
+    @VisibleForTesting
+    void setScrollPauseAlarm(Alarm value) {
+        mScrollPauseAlarm = value;
+    }
+
+    @VisibleForTesting
+    int getScrollHintDir() {
+        return mScrollHintDir;
+    }
+
+    @VisibleForTesting
+    void setScrollHintDir(int value) {
+        mScrollHintDir = value;
+    }
+
+    @VisibleForTesting
+    int getScrollAreaOffset() {
+        return mScrollAreaOffset;
+    }
     /**
      * Adds the provided listener to the running list of Folder listeners
      * {@link #mOnFolderStateChangedListeners}
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 3c4cf5a..588a6db 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -256,8 +256,8 @@
                 mFolder.getContent(), contentStart, contentEnd, finalRadius, !mIsOpening));
 
         // Fade in the folder name, as the text can overlap the icons when grid size is small.
-        mFolder.mFolderName.setAlpha(mIsOpening ? 0f : 1f);
-        play(a, getAnimator(mFolder.mFolderName, View.ALPHA, 0, 1),
+        mFolder.getFolderName().setAlpha(mIsOpening ? 0f : 1f);
+        play(a, getAnimator(mFolder.getFolderName(), View.ALPHA, 0, 1),
                 mIsOpening ? FOLDER_NAME_ALPHA_DURATION : 0,
                 mIsOpening ? mDuration - FOLDER_NAME_ALPHA_DURATION : FOLDER_NAME_ALPHA_DURATION);
 
@@ -318,7 +318,7 @@
                 mFolder.mFooter.setScaleX(1f);
                 mFolder.mFooter.setScaleY(1f);
                 mFolder.mFooter.setTranslationX(0f);
-                mFolder.mFolderName.setAlpha(1f);
+                mFolder.getFolderName().setAlpha(1f);
 
                 mFolder.setClipChildren(mFolderClipChildren);
                 mFolder.setClipToPadding(mFolderClipToPadding);
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index e9859cf..a0b695a 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -459,7 +459,7 @@
 
         mInfo.setTitle(newTitle, mFolder.mLauncherDelegate.getModelWriter());
         onTitleChanged(mInfo.title);
-        mFolder.mFolderName.setText(mInfo.title);
+        mFolder.getFolderName().setText(mInfo.title);
 
         // Logging for folder creation flow
         StatsLogManager.newInstance(getContext()).logger()
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 1b5ef42..9dc2d24 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -373,8 +373,8 @@
         // Update footer
         mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE);
         // Set the gravity as LEFT or RIGHT instead of START, as START depends on the actual text.
-        mFolder.mFolderName.setGravity(getPageCount() > 1 ?
-                (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL);
+        mFolder.getFolderName().setGravity(getPageCount() > 1
+                ? (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL);
     }
 
     public int getDesiredWidth() {
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
index e44ea1d..a691e45 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorDots.java
@@ -43,6 +43,7 @@
 import android.view.animation.OvershootInterpolator;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Insettable;
 import com.android.launcher3.R;
@@ -131,7 +132,8 @@
     private float mCurrentPosition;
     private float mFinalPosition;
     private boolean mIsScrollPaused;
-    private boolean mIsTwoPanels;
+    @VisibleForTesting
+    boolean mIsTwoPanels;
     private ObjectAnimator mAnimator;
     private @Nullable ObjectAnimator mAlphaAnimator;
 
@@ -477,6 +479,21 @@
         return sTempRect;
     }
 
+    @VisibleForTesting
+    int getActivePage() {
+        return mActivePage;
+    }
+
+    @VisibleForTesting
+    int getNumPages() {
+        return mNumPages;
+    }
+
+    @VisibleForTesting
+    float getCurrentPosition() {
+        return mCurrentPosition;
+    }
+
     private class MyOutlineProver extends ViewOutlineProvider {
 
         @Override
diff --git a/src/com/android/launcher3/util/ShortcutUtil.java b/src/com/android/launcher3/util/ShortcutUtil.java
index 07b7941..aa4f8af 100644
--- a/src/com/android/launcher3/util/ShortcutUtil.java
+++ b/src/com/android/launcher3/util/ShortcutUtil.java
@@ -54,14 +54,6 @@
                 ? ((WorkspaceItemInfo) info).getPersonKeys() : Utilities.EMPTY_STRING_ARRAY;
     }
 
-    /**
-     * Returns true if the item is a deep shortcut.
-     */
-    public static boolean isDeepShortcut(ItemInfo info) {
-        return info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
-                && info instanceof WorkspaceItemInfo;
-    }
-
     private static boolean isActive(ItemInfo info) {
         boolean isLoading = info instanceof WorkspaceItemInfo
                 && ((WorkspaceItemInfo) info).hasPromiseIconUi();
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
index 51749a7..6bae1ba 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -31,6 +31,7 @@
 import android.provider.Settings;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.Utilities;
 
@@ -49,14 +50,14 @@
 
     public static final VibrationEffect EFFECT_CLICK =
             createPredefined(VibrationEffect.EFFECT_CLICK);
-    private static final Uri HAPTIC_FEEDBACK_URI =
-            Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED);
+    @VisibleForTesting
+    static final Uri HAPTIC_FEEDBACK_URI = Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED);
 
-    private static final float LOW_TICK_SCALE = 0.9f;
-    private static final float DRAG_TEXTURE_SCALE = 0.03f;
-    private static final float DRAG_COMMIT_SCALE = 0.5f;
-    private static final float DRAG_BUMP_SCALE = 0.4f;
-    private static final int DRAG_TEXTURE_EFFECT_SIZE = 200;
+    @VisibleForTesting static final float LOW_TICK_SCALE = 0.9f;
+    @VisibleForTesting static final float DRAG_TEXTURE_SCALE = 0.03f;
+    @VisibleForTesting static final float DRAG_COMMIT_SCALE = 0.5f;
+    @VisibleForTesting static final float DRAG_BUMP_SCALE = 0.4f;
+    @VisibleForTesting static final int DRAG_TEXTURE_EFFECT_SIZE = 200;
 
     @Nullable
     private final VibrationEffect mDragEffect;
@@ -73,22 +74,29 @@
      */
     public static final VibrationEffect OVERVIEW_HAPTIC = EFFECT_CLICK;
 
-    private final Context mContext;
     private final Vibrator mVibrator;
     private final boolean mHasVibrator;
-    private final SettingsCache.OnChangeListener mHapticChangeListener =
+
+    private final SettingsCache mSettingsCache;
+
+    @VisibleForTesting
+    final SettingsCache.OnChangeListener mHapticChangeListener =
             isEnabled -> mIsHapticFeedbackEnabled = isEnabled;
 
     private boolean mIsHapticFeedbackEnabled;
 
     private VibratorWrapper(Context context) {
-        mContext = context;
-        mVibrator = context.getSystemService(Vibrator.class);
+        this(context.getSystemService(Vibrator.class), SettingsCache.INSTANCE.get(context));
+    }
+
+    @VisibleForTesting
+    VibratorWrapper(Vibrator vibrator, SettingsCache settingsCache) {
+        mVibrator = vibrator;
         mHasVibrator = mVibrator.hasVibrator();
+        mSettingsCache = settingsCache;
         if (mHasVibrator) {
-            SettingsCache cache = SettingsCache.INSTANCE.get(mContext);
-            cache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
-            mIsHapticFeedbackEnabled = cache.getValue(HAPTIC_FEEDBACK_URI, 0);
+            mSettingsCache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
+            mIsHapticFeedbackEnabled = mSettingsCache.getValue(HAPTIC_FEEDBACK_URI, 0);
         } else {
             mIsHapticFeedbackEnabled = false;
         }
@@ -98,12 +106,7 @@
 
             // Drag texture, Commit, and Bump should only be used for premium phones.
             // Before using these haptics make sure check if the device can use it
-            VibrationEffect.Composition dragEffect = VibrationEffect.startComposition();
-            for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) {
-                dragEffect.addPrimitive(
-                        PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE);
-            }
-            mDragEffect = dragEffect.compose();
+            mDragEffect = getDragEffect();
             mCommitEffect = VibrationEffect.startComposition().addPrimitive(
                     VibrationEffect.Composition.PRIMITIVE_TICK, DRAG_COMMIT_SCALE).compose();
             mBumpEffect = VibrationEffect.startComposition().addPrimitive(
@@ -124,8 +127,7 @@
     @Override
     public void close() {
         if (mHasVibrator) {
-            SettingsCache.INSTANCE.get(mContext)
-                    .unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
+            mSettingsCache.unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
         }
     }
 
@@ -215,4 +217,13 @@
             vibrate(primitiveLowTickEffect);
         }
     }
+
+    static VibrationEffect getDragEffect() {
+        VibrationEffect.Composition dragEffect = VibrationEffect.startComposition();
+        for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) {
+            dragEffect.addPrimitive(
+                    PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE);
+        }
+        return dragEffect.compose();
+    }
 }
diff --git a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
index 104209e..856f4b3 100644
--- a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
@@ -110,7 +110,7 @@
         }
         View background = RoundedCornerEnforcement.findBackground(this);
         if (background == null
-                || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) {
+                || RoundedCornerEnforcement.hasAppWidgetOptedOut(background)) {
             resetRoundedCorners();
             return;
         }
diff --git a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
index 1e46ffd..2e5e251 100644
--- a/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
+++ b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java
@@ -67,7 +67,7 @@
     /**
      * Check whether the app widget has opted out of the enforcement.
      */
-    public static boolean hasAppWidgetOptedOut(@NonNull View appWidget, @NonNull View background) {
+    public static boolean hasAppWidgetOptedOut(@NonNull View background) {
         return background.getId() == android.R.id.background && background.getClipToOutline();
     }
 
diff --git a/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
new file mode 100644
index 0000000..3dd8dbc
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/pm/InstallSessionHelperTest.kt
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2024 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.pm
+
+import android.content.pm.ApplicationInfo
+import android.content.pm.ApplicationInfo.FLAG_INSTALLED
+import android.content.pm.LauncherApps
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.PROMISE_ICON_IDS
+import com.android.launcher3.util.Executors.MODEL_EXECUTOR
+import com.android.launcher3.util.IntArray
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.TestUtil
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InstallSessionHelperTest {
+
+    private val launcherModelHelper = LauncherModelHelper()
+    private val sandboxContext = spy(launcherModelHelper.sandboxContext)
+    private val packageManager = sandboxContext.packageManager
+    private val expectedAppPackage = "expectedAppPackage"
+    private val expectedInstallerPackage = "expectedInstallerPackage"
+    private val mockPackageInstaller: PackageInstaller = mock()
+
+    private lateinit var installSessionHelper: InstallSessionHelper
+    private lateinit var launcherApps: LauncherApps
+
+    @Before
+    fun setup() {
+        whenever(packageManager.packageInstaller).thenReturn(mockPackageInstaller)
+        whenever(sandboxContext.packageName).thenReturn(expectedInstallerPackage)
+        launcherApps = sandboxContext.spyService(LauncherApps::class.java)
+        installSessionHelper = InstallSessionHelper(sandboxContext)
+    }
+
+    @Test
+    fun `getActiveSessions fetches verified install sessions from LauncherApps`() {
+        // Given
+        val expectedVerifiedSession1 =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 0
+                installerPackageName = expectedInstallerPackage
+                appPackageName = expectedAppPackage
+                userId = 0
+            }
+        val expectedVerifiedSession2 =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "app2"
+                userId = 0
+            }
+        val expectedSessions = listOf(expectedVerifiedSession1, expectedVerifiedSession2)
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(expectedSessions)
+        // When
+        val actualSessions = installSessionHelper.getActiveSessions()
+        // Then
+        assertThat(actualSessions.values.toList()).isEqualTo(expectedSessions)
+    }
+
+    @Test
+    fun `getActiveSessionInfo fetches verified install sessions for given user and pkg`() {
+        // Given
+        val expectedVerifiedSession =
+            PackageInstaller.SessionInfo().apply {
+                installerPackageName = expectedInstallerPackage
+                appPackageName = expectedAppPackage
+                userId = 0
+            }
+        whenever(launcherApps.allPackageInstallerSessions)
+            .thenReturn(listOf(expectedVerifiedSession))
+        // When
+        val actualSession =
+            installSessionHelper.getActiveSessionInfo(UserHandle(0), expectedAppPackage)
+        // Then
+        assertThat(actualSession).isEqualTo(expectedVerifiedSession)
+    }
+
+    @Test
+    fun `getVerifiedSessionInfo verifies and returns session for given id`() {
+        // Given
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = expectedAppPackage
+                userId = 0
+            }
+        whenever(mockPackageInstaller.getSessionInfo(1)).thenReturn(expectedSession)
+        // When
+        val actualSession = installSessionHelper.getVerifiedSessionInfo(1)
+        // Then
+        assertThat(actualSession).isEqualTo(expectedSession)
+    }
+
+    @Test
+    fun `isTrustedPackage returns true if LauncherApps finds ApplicationInfo`() {
+        // Given
+        val expectedApplicationInfo =
+            ApplicationInfo().apply {
+                flags = flags or FLAG_INSTALLED
+                enabled = true
+            }
+        doReturn(expectedApplicationInfo)
+            .whenever(launcherApps)
+            .getApplicationInfo(expectedAppPackage, ApplicationInfo.FLAG_SYSTEM, UserHandle(0))
+        // When
+        val actualResult = installSessionHelper.isTrustedPackage(expectedAppPackage, UserHandle(0))
+        // Then
+        assertThat(actualResult).isTrue()
+    }
+
+    @Test
+    fun `getAllVerifiedSessions verifies and returns all active install sessions`() {
+        // Given
+        val expectedVerifiedSession1 =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 0
+                installerPackageName = expectedInstallerPackage
+                appPackageName = expectedAppPackage
+                userId = 0
+            }
+        val expectedVerifiedSession2 =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "app2"
+                userId = 0
+            }
+        val expectedSessions = listOf(expectedVerifiedSession1, expectedVerifiedSession2)
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(expectedSessions)
+        // When
+        val actualSessions = installSessionHelper.allVerifiedSessions
+        // Then
+        assertThat(actualSessions).isEqualTo(expectedSessions)
+    }
+
+    @Test
+    fun `promiseIconAddedForId returns true if there is a promiseIcon with the session id`() {
+        // Given
+        val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+        LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "app2"
+                userId = 0
+            }
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+        // When
+        var actualResult = false
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            actualResult = installSessionHelper.promiseIconAddedForId(1)
+        }
+        // Then
+        assertThat(actualResult).isTrue()
+    }
+
+    @Test
+    fun `removePromiseIconId removes promiseIconId for given Session id`() {
+        // Given
+        val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+        LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "app2"
+                userId = 0
+            }
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+        // When
+        var actualResult = true
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            installSessionHelper.removePromiseIconId(1)
+            actualResult = installSessionHelper.promiseIconAddedForId(1)
+        }
+        // Then
+        assertThat(actualResult).isFalse()
+    }
+
+    @Test
+    fun `tryQueuePromiseAppIcon will update promise icon ids from eligible sessions`() {
+        // Given
+        val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+        LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "appPackage"
+                userId = 0
+                appIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8)
+                appLabel = "appLabel"
+                installReason = PackageManager.INSTALL_REASON_USER
+            }
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+        // When
+        var wasPromiseIconAdded = false
+        var actualPromiseIconIds = ""
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            installSessionHelper.removePromiseIconId(1)
+            installSessionHelper.tryQueuePromiseAppIcon(expectedSession)
+            wasPromiseIconAdded = installSessionHelper.promiseIconAddedForId(1)
+            actualPromiseIconIds = LauncherPrefs.get(sandboxContext).get(PROMISE_ICON_IDS)
+        }
+        // Then
+        assertThat(wasPromiseIconAdded).isTrue()
+        assertThat(actualPromiseIconIds).isEqualTo(expectedIdString)
+    }
+
+    @Test
+    fun `verifySessionInfo is true if can verify given SessionInfo`() {
+        // Given
+        val expectedIdString = IntArray().apply { add(1) }.toConcatString()
+        LauncherPrefs.get(sandboxContext).putSync(Pair(PROMISE_ICON_IDS, expectedIdString))
+        val expectedSession =
+            PackageInstaller.SessionInfo().apply {
+                sessionId = 1
+                installerPackageName = expectedInstallerPackage
+                appPackageName = "appPackage"
+                userId = 0
+                appIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8)
+                appLabel = "appLabel"
+                installReason = PackageManager.INSTALL_REASON_USER
+            }
+        whenever(launcherApps.allPackageInstallerSessions).thenReturn(listOf(expectedSession))
+        // When
+        var actualResult = false
+        TestUtil.runOnExecutorSync(MODEL_EXECUTOR) {
+            actualResult = installSessionHelper.verifySessionInfo(expectedSession)
+        }
+        // Then
+        assertThat(actualResult).isTrue()
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ShortcutUtilTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/ShortcutUtilTest.kt
new file mode 100644
index 0000000..c43e563
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ShortcutUtilTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 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 androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.Utilities
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShortcutUtilTest {
+
+    @Test
+    fun `supportsShortcuts returns true if the item is active and it is an app`() {
+        // A blank workspace item info should be active and should be ITEM_TYPE_APPLICATION
+        val itemInfo = WorkspaceItemInfo()
+        // Action
+        val result = ShortcutUtil.supportsShortcuts(itemInfo)
+        // Verify
+        assertEquals(true, result)
+    }
+
+    @Test
+    fun `supportsDeepShortcuts returns true if the app is active and an app and widgets are enabled`() {
+        // Setup
+        val itemInfo = WorkspaceItemInfo()
+        // Action
+        val result = ShortcutUtil.supportsDeepShortcuts(itemInfo)
+        // Verify
+        assertEquals(true, result)
+    }
+
+    @Test
+    fun `getShortcutIdIfPinnedShortcut returns null if the item is an app`() {
+        // Setup
+        val itemInfo = WorkspaceItemInfo()
+        // Action
+        val result = ShortcutUtil.getShortcutIdIfPinnedShortcut(itemInfo)
+        // Verify
+        assertNull(result)
+    }
+
+    @Test
+    fun `getPersonKeysIfPinnedShortcut returns empty string array if item type is an app`() {
+        // Setup
+        val itemInfo = WorkspaceItemInfo()
+        // Action
+        val result = ShortcutUtil.getPersonKeysIfPinnedShortcut(itemInfo)
+        // Verify
+        assertArrayEquals(Utilities.EMPTY_STRING_ARRAY, result)
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt
new file mode 100644
index 0000000..330c394
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/VibratorWrapperTest.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2024 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.media.AudioAttributes
+import android.os.SystemClock
+import android.os.VibrationEffect
+import android.os.VibrationEffect.Composition.PRIMITIVE_LOW_TICK
+import android.os.VibrationEffect.Composition.PRIMITIVE_TICK
+import android.os.Vibrator
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.util.VibratorWrapper.HAPTIC_FEEDBACK_URI
+import com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC
+import com.android.launcher3.util.VibratorWrapper.VIBRATION_ATTRS
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.never
+import org.mockito.kotlin.same
+import org.mockito.kotlin.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class VibratorWrapperTest {
+
+    @Mock private lateinit var settingsCache: SettingsCache
+    @Mock private lateinit var vibrator: Vibrator
+    @Captor private lateinit var vibrationEffectCaptor: ArgumentCaptor<VibrationEffect>
+
+    private lateinit var underTest: VibratorWrapper
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        `when`(settingsCache.getValue(HAPTIC_FEEDBACK_URI, 0)).thenReturn(true)
+        `when`(vibrator.hasVibrator()).thenReturn(true)
+        `when`(vibrator.areAllPrimitivesSupported(PRIMITIVE_TICK)).thenReturn(true)
+        `when`(vibrator.areAllPrimitivesSupported(PRIMITIVE_LOW_TICK)).thenReturn(true)
+        `when`(vibrator.getPrimitiveDurations(PRIMITIVE_LOW_TICK)).thenReturn(intArrayOf(10))
+
+        underTest = VibratorWrapper(vibrator, settingsCache)
+    }
+
+    @Test
+    fun init_register_onChangeListener() {
+        verify(settingsCache).register(HAPTIC_FEEDBACK_URI, underTest.mHapticChangeListener)
+    }
+
+    @Test
+    fun close_unregister_onChangeListener() {
+        underTest.close()
+
+        verify(settingsCache).unregister(HAPTIC_FEEDBACK_URI, underTest.mHapticChangeListener)
+    }
+
+    @Test
+    fun vibrate() {
+        underTest.vibrate(OVERVIEW_HAPTIC)
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(OVERVIEW_HAPTIC, VIBRATION_ATTRS)
+    }
+
+    @Test
+    fun vibrate_primitive_id() {
+        underTest.vibrate(PRIMITIVE_TICK, 1f, OVERVIEW_HAPTIC)
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+        val expectedEffect =
+            VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK, 1f).compose()
+        assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+    }
+
+    @Test
+    fun vibrate_with_invalid_primitive_id_use_fallback_effect() {
+        underTest.vibrate(-1, 1f, OVERVIEW_HAPTIC)
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(OVERVIEW_HAPTIC, VIBRATION_ATTRS)
+    }
+
+    @Test
+    fun vibrate_for_taskbar_unstash() {
+        underTest.vibrateForTaskbarUnstash()
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+        val expectedEffect =
+            VibrationEffect.startComposition()
+                .addPrimitive(PRIMITIVE_LOW_TICK, VibratorWrapper.LOW_TICK_SCALE)
+                .compose()
+        assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+    }
+
+    @Test
+    fun vibrate_for_drag_bump() {
+        underTest.vibrateForDragBump()
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+        val expectedEffect =
+            VibrationEffect.startComposition()
+                .addPrimitive(PRIMITIVE_LOW_TICK, VibratorWrapper.DRAG_BUMP_SCALE)
+                .compose()
+        assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+    }
+
+    @Test
+    fun vibrate_for_drag_commit() {
+        underTest.vibrateForDragCommit()
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+        val expectedEffect =
+            VibrationEffect.startComposition()
+                .addPrimitive(PRIMITIVE_TICK, VibratorWrapper.DRAG_COMMIT_SCALE)
+                .compose()
+        assertThat(vibrationEffectCaptor.value).isEqualTo(expectedEffect)
+    }
+
+    @Test
+    fun vibrate_for_drag_texture() {
+        SystemClock.setCurrentTimeMillis(40000)
+
+        underTest.vibrateForDragTexture()
+
+        awaitTasksCompleted()
+        verify(vibrator).vibrate(vibrationEffectCaptor.capture(), same(VIBRATION_ATTRS))
+        assertThat(vibrationEffectCaptor.value).isEqualTo(VibratorWrapper.getDragEffect())
+    }
+
+    @Test
+    fun vibrate_for_drag_texture_within_time_window_noOp() {
+        SystemClock.setCurrentTimeMillis(40000)
+        underTest.vibrateForDragTexture()
+        awaitTasksCompleted()
+        reset(vibrator)
+
+        underTest.vibrateForDragTexture()
+
+        verifyNoMoreInteractions(vibrator)
+    }
+
+    @Test
+    fun haptic_feedback_disabled_no_vibrate() {
+        `when`(vibrator.hasVibrator()).thenReturn(false)
+        underTest = VibratorWrapper(vibrator, settingsCache)
+
+        underTest.vibrate(OVERVIEW_HAPTIC)
+
+        awaitTasksCompleted()
+        verify(vibrator, never())
+            .vibrate(any(VibrationEffect::class.java), any(AudioAttributes::class.java))
+    }
+
+    @Test
+    fun cancel_vibrate() {
+        underTest.cancelVibrate()
+
+        awaitTasksCompleted()
+        verify(vibrator).cancel()
+    }
+
+    private fun awaitTasksCompleted() {
+        Executors.UI_HELPER_EXECUTOR.submit<Any> { null }.get()
+    }
+}
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
new file mode 100644
index 0000000..db77702
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/RoundedCornerEnforcementTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 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.widget
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.R
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RoundedCornerEnforcementTest {
+
+    @Test
+    fun `Widget view has one background`() {
+        val mockWidgetView = mock(LauncherAppWidgetHostView::class.java)
+
+        doReturn(android.R.id.background).whenever(mockWidgetView).id
+
+        assertSame(RoundedCornerEnforcement.findBackground(mockWidgetView), mockWidgetView)
+    }
+
+    @Test
+    fun `Widget opted out of rounded corner enforcement`() {
+        val mockView = mock(View::class.java)
+
+        doReturn(android.R.id.background).whenever(mockView).id
+        doReturn(true).whenever(mockView).clipToOutline
+
+        assertTrue(RoundedCornerEnforcement.hasAppWidgetOptedOut(mockView))
+    }
+
+    @Test
+    fun `Compute rect based on widget view with background`() {
+        val mockBackgroundView = mock(View::class.java)
+        val mockWidgetView = mock(ViewGroup::class.java)
+        val testRect = Rect(0, 0, 0, 0)
+
+        doReturn(WIDTH).whenever(mockBackgroundView).width
+        doReturn(HEIGHT).whenever(mockBackgroundView).height
+        doReturn(LEFT).whenever(mockBackgroundView).left
+        doReturn(TOP).whenever(mockBackgroundView).top
+        doReturn(mockWidgetView).whenever(mockBackgroundView).parent
+
+        RoundedCornerEnforcement.computeRoundedRectangle(
+            mockWidgetView,
+            mockBackgroundView,
+            testRect
+        )
+
+        assertEquals(Rect(50, 75, 250, 275), testRect)
+    }
+
+    @Test
+    fun `Compute system radius`() {
+        val mockContext = mock(Context::class.java)
+        val mockRes = mock(Resources::class.java)
+
+        doReturn(mockRes).whenever(mockContext).resources
+        doReturn(RADIUS)
+            .whenever(mockRes)
+            .getDimension(eq(android.R.dimen.system_app_widget_background_radius))
+        doReturn(LAUNCHER_RADIUS)
+            .whenever(mockRes)
+            .getDimension(eq(R.dimen.enforced_rounded_corner_max_radius))
+
+        assertEquals(RADIUS, RoundedCornerEnforcement.computeEnforcedRadius(mockContext))
+    }
+
+    companion object {
+        const val WIDTH = 200
+        const val HEIGHT = 200
+        const val LEFT = 50
+        const val TOP = 75
+
+        const val RADIUS = 8f
+        const val LAUNCHER_RADIUS = 16f
+    }
+}
diff --git a/tests/src/com/android/launcher3/folder/FolderNameInfosTest.kt b/tests/src/com/android/launcher3/folder/FolderNameInfosTest.kt
new file mode 100644
index 0000000..b491f17
--- /dev/null
+++ b/tests/src/com/android/launcher3/folder/FolderNameInfosTest.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2024 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.folder
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.folder.FolderNameInfos.*
+import org.junit.Test
+import org.junit.runner.RunWith
+
+data class Label(val index: Int, val label: String, val score: Float)
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderNameInfosTest {
+
+    companion object {
+        val statusList =
+            listOf(
+                SUCCESS,
+                HAS_PRIMARY,
+                HAS_SUGGESTIONS,
+                ERROR_NO_PROVIDER,
+                ERROR_APP_LOOKUP_FAILED,
+                ERROR_ALL_APP_LOOKUP_FAILED,
+                ERROR_NO_LABELS_GENERATED,
+                ERROR_LABEL_LOOKUP_FAILED,
+                ERROR_ALL_LABEL_LOOKUP_FAILED,
+                ERROR_NO_PACKAGES,
+            )
+    }
+
+    @Test
+    fun status() {
+        assertStatus(statusList)
+        assertStatus(
+            listOf(
+                ERROR_NO_PROVIDER,
+                ERROR_APP_LOOKUP_FAILED,
+                ERROR_ALL_APP_LOOKUP_FAILED,
+                ERROR_NO_LABELS_GENERATED,
+                ERROR_LABEL_LOOKUP_FAILED,
+                ERROR_ALL_LABEL_LOOKUP_FAILED,
+                ERROR_NO_PACKAGES,
+            )
+        )
+        assertStatus(
+            listOf(
+                SUCCESS,
+                HAS_PRIMARY,
+                HAS_SUGGESTIONS,
+            )
+        )
+        assertStatus(
+            listOf(
+                SUCCESS,
+                HAS_PRIMARY,
+                HAS_SUGGESTIONS,
+            )
+        )
+    }
+
+    fun assertStatus(statusList: List<Int>) {
+        var infos = FolderNameInfos()
+        statusList.forEach { infos.setStatus(it) }
+        assert(infos.status() == statusList.sum()) {
+            "There is an overlap on the status constants!"
+        }
+    }
+
+    @Test
+    fun hasPrimary() {
+        assertHasPrimary(
+            createNameInfos(listOf(Label(0, "label", 1f)), statusList),
+            hasPrimary = true
+        )
+        assertHasPrimary(
+            createNameInfos(listOf(Label(1, "label", 1f)), statusList),
+            hasPrimary = false
+        )
+        assertHasPrimary(
+            createNameInfos(
+                listOf(Label(0, "label", 1f)),
+                listOf(
+                    ERROR_NO_PROVIDER,
+                    ERROR_APP_LOOKUP_FAILED,
+                    ERROR_ALL_APP_LOOKUP_FAILED,
+                    ERROR_NO_LABELS_GENERATED,
+                    ERROR_LABEL_LOOKUP_FAILED,
+                    ERROR_ALL_LABEL_LOOKUP_FAILED,
+                    ERROR_NO_PACKAGES,
+                )
+            ),
+            hasPrimary = false
+        )
+    }
+
+    private fun assertHasPrimary(nameInfos: FolderNameInfos, hasPrimary: Boolean) =
+        assert(nameInfos.hasPrimary() == hasPrimary)
+
+    private fun createNameInfos(labels: List<Label>?, statusList: List<Int>?): FolderNameInfos {
+        val infos = FolderNameInfos()
+        labels?.forEach { infos.setLabel(it.index, it.label, it.score) }
+        statusList?.forEach { infos.setStatus(it) }
+        return infos
+    }
+
+    @Test
+    fun hasSuggestions() {
+        assertHasSuggestions(
+            createNameInfos(listOf(Label(0, "label", 1f)), null),
+            hasSuggestions = true
+        )
+        assertHasSuggestions(createNameInfos(null, null), hasSuggestions = false)
+        // There is a max of 4 suggestions
+        assertHasSuggestions(
+            createNameInfos(listOf(Label(5, "label", 1f)), null),
+            hasSuggestions = false
+        )
+        assertHasSuggestions(
+            createNameInfos(
+                listOf(
+                    Label(0, "label", 1f),
+                    Label(1, "label", 1f),
+                    Label(2, "label", 1f),
+                    Label(3, "label", 1f)
+                ),
+                null
+            ),
+            hasSuggestions = true
+        )
+    }
+
+    private fun assertHasSuggestions(nameInfos: FolderNameInfos, hasSuggestions: Boolean) =
+        assert(nameInfos.hasSuggestions() == hasSuggestions)
+
+    @Test
+    fun hasContains() {
+        assertContains(
+            createNameInfos(
+                listOf(
+                    Label(0, "label1", 1f),
+                    Label(1, "label2", 1f),
+                    Label(2, "label3", 1f),
+                    Label(3, "label4", 1f)
+                ),
+                null
+            ),
+            label = Label(-1, "label3", -1f),
+            contains = true
+        )
+        assertContains(
+            createNameInfos(
+                listOf(
+                    Label(0, "label1", 1f),
+                    Label(1, "label2", 1f),
+                    Label(2, "label3", 1f),
+                    Label(3, "label4", 1f)
+                ),
+                null
+            ),
+            label = Label(-1, "label5", -1f),
+            contains = false
+        )
+        assertContains(
+            createNameInfos(null, null),
+            label = Label(-1, "label1", -1f),
+            contains = false
+        )
+        assertContains(
+            createNameInfos(
+                listOf(
+                    Label(0, "label1", 1f),
+                    Label(1, "label2", 1f),
+                    Label(2, "lAbel3", 1f),
+                    Label(3, "lEbel4", 1f)
+                ),
+                null
+            ),
+            label = Label(-1, "LaBEl3", -1f),
+            contains = true
+        )
+    }
+
+    private fun assertContains(nameInfos: FolderNameInfos, label: Label, contains: Boolean) =
+        assert(nameInfos.contains(label.label) == contains)
+}
diff --git a/tests/src/com/android/launcher3/folder/FolderTest.kt b/tests/src/com/android/launcher3/folder/FolderTest.kt
index e1daa74..4eb335e 100644
--- a/tests/src/com/android/launcher3/folder/FolderTest.kt
+++ b/tests/src/com/android/launcher3/folder/FolderTest.kt
@@ -18,23 +18,56 @@
 
 import android.content.Context
 import android.graphics.Point
+import android.view.KeyEvent
 import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.widget.TextView
+import androidx.core.view.isVisible
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.launcher3.Alarm
+import com.android.launcher3.DragSource
 import com.android.launcher3.DropTarget.DragObject
 import com.android.launcher3.LauncherAppState
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+import com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER
+import com.android.launcher3.OnAlarmListener
+import com.android.launcher3.R
 import com.android.launcher3.celllayout.board.FolderPoint
 import com.android.launcher3.celllayout.board.TestWorkspaceBuilder
+import com.android.launcher3.dragndrop.DragController
+import com.android.launcher3.dragndrop.DragOptions
+import com.android.launcher3.dragndrop.DragView
+import com.android.launcher3.folder.Folder.MIN_CONTENT_DIMEN
+import com.android.launcher3.folder.Folder.ON_EXIT_CLOSE_DELAY
+import com.android.launcher3.folder.Folder.SCROLL_LEFT
+import com.android.launcher3.folder.Folder.SCROLL_NONE
+import com.android.launcher3.folder.Folder.STATE_ANIMATING
+import com.android.launcher3.folder.Folder.STATE_CLOSED
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.ItemInfo
 import com.android.launcher3.util.ActivityContextWrapper
 import com.android.launcher3.util.ModelTestExtensions.clearModelDb
+import java.util.ArrayList
 import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertNull
+import junit.framework.TestCase.assertTrue
 import org.junit.After
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito
+import org.mockito.Mockito.spy
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.whenever
 
 /** Tests for [Folder] */
 @SmallTest
@@ -44,7 +77,7 @@
     private val context: Context =
         ActivityContextWrapper(ApplicationProvider.getApplicationContext())
     private val workspaceBuilder = TestWorkspaceBuilder(context)
-    private val folder: Folder = Mockito.spy(Folder(context, null))
+    private val folder: Folder = spy(Folder(context, null))
 
     @After
     fun tearDown() {
@@ -60,8 +93,10 @@
         folder.mContent = Mockito.mock(FolderPagedView::class.java)
         val dragLayout = Mockito.mock(View::class.java)
         val dragObject = Mockito.mock(DragObject::class.java)
-        assertEquals(folder.deleteFolderOnDropCompleted, false)
+        folder.deleteFolderOnDropCompleted = false
+
         folder.onDropCompleted(dragLayout, dragObject, true)
+
         verify(folder, times(1)).replaceFolderWithFinalItem()
         assertEquals(folder.deleteFolderOnDropCompleted, false)
     }
@@ -74,12 +109,819 @@
         folder.mContent = Mockito.mock(FolderPagedView::class.java)
         val dragLayout = Mockito.mock(View::class.java)
         val dragObject = Mockito.mock(DragObject::class.java)
-        assertEquals(folder.deleteFolderOnDropCompleted, false)
+        folder.deleteFolderOnDropCompleted = false
+
         folder.onDropCompleted(dragLayout, dragObject, true)
+
         verify(folder, times(0)).replaceFolderWithFinalItem()
         assertEquals(folder.deleteFolderOnDropCompleted, false)
     }
 
+    @Test
+    fun `Test that we accept valid item type ITEM_TYPE_APPLICATION`() {
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_APPLICATION
+
+        val willAcceptResult = Folder.willAccept(itemInfo)
+
+        assertTrue(willAcceptResult)
+    }
+
+    @Test
+    fun `Test that we accept valid item type ITEM_TYPE_DEEP_SHORTCUT`() {
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_DEEP_SHORTCUT
+
+        val willAcceptResult = Folder.willAccept(itemInfo)
+
+        assertTrue(willAcceptResult)
+    }
+
+    @Test
+    fun `Test that we accept valid item type ITEM_TYPE_APP_PAIR`() {
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_APP_PAIR
+
+        val willAcceptResult = Folder.willAccept(itemInfo)
+
+        assertTrue(willAcceptResult)
+    }
+
+    @Test
+    fun `Test that we do not accept invalid item type ITEM_TYPE_APPWIDGET`() {
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_APPWIDGET
+
+        val willAcceptResult = Folder.willAccept(itemInfo)
+
+        assertFalse(willAcceptResult)
+    }
+
+    @Test
+    fun `Test that we do not accept invalid item type ITEM_TYPE_FOLDER`() {
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_FOLDER
+
+        val willAcceptResult = Folder.willAccept(itemInfo)
+
+        assertFalse(willAcceptResult)
+    }
+
+    @Test
+    fun `We should not animate open if items is null or less than or equal to 1`() {
+        folder.mInfo = Mockito.mock(FolderInfo::class.java)
+        val shouldAnimateOpenResult = folder.shouldAnimateOpen(null)
+
+        assertFalse(shouldAnimateOpenResult)
+        assertFalse(
+            folder.shouldAnimateOpen(arrayListOf<ItemInfo>(Mockito.mock(ItemInfo::class.java)))
+        )
+    }
+
+    @Test
+    fun `We should animate open if items greater than 1`() {
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = folderInfo
+
+        val shouldAnimateOpenResult = folder.shouldAnimateOpen(folder.mInfo.getContents())
+
+        assertTrue(shouldAnimateOpenResult)
+    }
+
+    @Test
+    fun `Should be true if there is an open folder`() {
+        val closeOpenFolderResult = folder.closeOpenFolder(Mockito.mock(Folder::class.java))
+
+        assertTrue(closeOpenFolderResult)
+    }
+
+    @Test
+    fun `Should be false if the open folder is this folder`() {
+        val closeOpenFolderResult = folder.closeOpenFolder(folder)
+
+        assertFalse(closeOpenFolderResult)
+    }
+
+    @Test
+    fun `Should be false if there is not an open folder`() {
+        val closeOpenFolderResult = folder.closeOpenFolder(null)
+
+        assertFalse(closeOpenFolderResult)
+    }
+
+    @Test
+    fun `If drag is in progress we should set mItemAddedBackToSelfViaIcon to true`() {
+        folder.itemAddedBackToSelfViaIcon = false
+        folder.isDragInProgress = true
+
+        folder.notifyDrop()
+
+        assertTrue(folder.itemAddedBackToSelfViaIcon)
+    }
+
+    @Test
+    fun `If drag is not in progress we should not set mItemAddedBackToSelfViaIcon to true`() {
+        folder.itemAddedBackToSelfViaIcon = false
+        folder.isDragInProgress = false
+
+        folder.notifyDrop()
+
+        assertFalse(folder.itemAddedBackToSelfViaIcon)
+    }
+
+    @Test
+    fun `If launcher dragging is not enabled onLongClick should return true`() {
+        `when`(folder.isLauncherDraggingEnabled).thenReturn(false)
+
+        val onLongClickResult = folder.onLongClick(Mockito.mock(View::class.java))
+
+        assertTrue(onLongClickResult)
+    }
+
+    @Test
+    fun `If launcher dragging is enabled we should return startDrag result`() {
+        `when`(folder.isLauncherDraggingEnabled).thenReturn(true)
+        val viewMock = Mockito.mock(View::class.java)
+        val dragOptions = Mockito.mock(DragOptions::class.java)
+
+        val onLongClickResult = folder.onLongClick(viewMock)
+
+        assertEquals(onLongClickResult, folder.startDrag(viewMock, dragOptions))
+        verify(folder, times(1)).startDrag(viewMock, dragOptions)
+    }
+
+    @Test
+    fun `Verify start drag works as intended when view is instanceof ItemInfo`() {
+        val itemInfo = ItemInfo()
+        itemInfo.rank = 5
+        val viewMock = Mockito.mock(View::class.java)
+        val dragOptions = DragOptions()
+        `when`(viewMock.tag).thenReturn(itemInfo)
+        folder.dragController = Mockito.mock(DragController::class.java)
+
+        folder.startDrag(viewMock, dragOptions)
+
+        assertEquals(folder.mEmptyCellRank, 5)
+        assertEquals(folder.currentDragView, viewMock)
+        verify(folder, times(1)).addDragListener(dragOptions)
+        verify(folder, times(1)).callBeginDragShared(viewMock, dragOptions)
+    }
+
+    @Test
+    fun `Verify start drag works as intended when view is not instanceof ItemInfo`() {
+        val viewMock = Mockito.mock(View::class.java)
+        val dragOptions = DragOptions()
+
+        folder.startDrag(viewMock, dragOptions)
+
+        verify(folder, times(0)).addDragListener(dragOptions)
+        verify(folder, times(0)).callBeginDragShared(viewMock, dragOptions)
+    }
+
+    @Test
+    fun `Verify that onDragStart has an effect if dragSource is this folder`() {
+        folder.itemsInvalidated = false
+        folder.isDragInProgress = false
+        folder.itemAddedBackToSelfViaIcon = true
+        folder.currentDragView = Mockito.mock(View::class.java)
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = spy(folderInfo)
+        val dragObject = DragObject(context)
+        dragObject.dragInfo = Mockito.mock(ItemInfo::class.java)
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        dragObject.dragSource = folder
+
+        folder.onDragStart(dragObject, DragOptions())
+
+        verify(folder.mContent, times(1)).removeItem(folder.currentDragView)
+        verify(folder.mInfo, times(1)).remove(dragObject.dragInfo, true)
+        assertTrue(folder.itemsInvalidated)
+        assertTrue(folder.isDragInProgress)
+        assertFalse(folder.itemAddedBackToSelfViaIcon)
+    }
+
+    @Test
+    fun `Verify that onDragStart has no effects if dragSource is not this folder`() {
+        folder.itemsInvalidated = false
+        folder.isDragInProgress = false
+        folder.itemAddedBackToSelfViaIcon = true
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        val dragObject = DragObject(context)
+        dragObject.dragSource = Mockito.mock(DragSource::class.java)
+
+        folder.onDragStart(dragObject, DragOptions())
+
+        verify(folder.mContent, times(0)).removeItem(folder.currentDragView)
+        assertFalse(folder.itemsInvalidated)
+        assertFalse(folder.isDragInProgress)
+        assertTrue(folder.itemAddedBackToSelfViaIcon)
+    }
+
+    @Test
+    fun `Verify onDragEnd that we call completeDragExit and set drag in progress false`() {
+        doNothing().`when`(folder).completeDragExit()
+        folder.isExternalDrag = true
+        folder.isDragInProgress = true
+        folder.dragController = Mockito.mock(DragController::class.java)
+
+        folder.onDragEnd()
+
+        verify(folder, times(1)).completeDragExit()
+        verify(folder.dragController, times(1)).removeDragListener(folder)
+        assertFalse(folder.isDragInProgress)
+    }
+
+    @Test
+    fun `Verify onDragEnd that we do not call completeDragExit and set drag in progress false`() {
+        folder.isExternalDrag = false
+        folder.isDragInProgress = true
+        folder.dragController = Mockito.mock(DragController::class.java)
+
+        folder.onDragEnd()
+
+        verify(folder, times(0)).completeDragExit()
+        verify(folder.dragController, times(1)).removeDragListener(folder)
+        assertFalse(folder.isDragInProgress)
+    }
+
+    @Test
+    fun `startEditingFolderName should set hint to empty and showLabelSuggestions`() {
+        doNothing().`when`(folder).showLabelSuggestions()
+        folder.isEditingName = false
+        folder.folderName = FolderNameEditText(context)
+        folder.folderName.hint = "hello"
+
+        folder.startEditingFolderName()
+
+        verify(folder, times(1)).showLabelSuggestions()
+        assertEquals("", folder.folderName.hint)
+        assertTrue(folder.isEditingName)
+    }
+
+    @Test
+    fun `Ensure we set the title and hint correctly onBackKey when we have a new title`() {
+        val expectedHint = null
+        val expectedTitle = "hello"
+        folder.isEditingName = true
+        folder.folderName = spy(FolderNameEditText(context))
+        folder.folderName.setText(expectedTitle)
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = spy(folderInfo)
+        folder.mInfo.title = "world"
+        folder.mFolderIcon = Mockito.mock(FolderIcon::class.java)
+
+        folder.onBackKey()
+
+        assertEquals(expectedTitle, folder.mInfo.title)
+        verify(folder.mFolderIcon, times(1)).onTitleChanged(expectedTitle)
+        assertEquals(expectedHint, folder.folderName.hint)
+        assertFalse(folder.isEditingName)
+        verify(folder.folderName, times(1)).clearFocus()
+    }
+
+    @Test
+    fun `Ensure we set the title and hint correctly onBackKey when we do not have a new title`() {
+        val expectedHint = context.getString(R.string.folder_hint_text)
+        val expectedTitle = ""
+        folder.isEditingName = true
+        folder.folderName = spy(FolderNameEditText(context))
+        folder.folderName.setText(expectedTitle)
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = spy(folderInfo)
+        folder.mInfo.title = "world"
+        folder.mFolderIcon = Mockito.mock(FolderIcon::class.java)
+
+        folder.onBackKey()
+
+        assertEquals(expectedTitle, folder.mInfo.title)
+        verify(folder.mFolderIcon, times(1)).onTitleChanged(expectedTitle)
+        assertEquals(expectedHint, folder.folderName.hint)
+        assertFalse(folder.isEditingName)
+        verify(folder.folderName, times(1)).clearFocus()
+    }
+
+    @Test
+    fun `ensure onEditorAction calls dispatchBackKey when actionId is IME_ACTION_DONE`() {
+        folder.folderName = Mockito.mock(FolderNameEditText::class.java)
+
+        val result =
+            folder.onEditorAction(
+                Mockito.mock(TextView::class.java),
+                EditorInfo.IME_ACTION_DONE,
+                Mockito.mock(KeyEvent::class.java)
+            )
+
+        assertTrue(result)
+        verify(folder.folderName, times(1)).dispatchBackKey()
+    }
+
+    @Test
+    fun `ensure onEditorAction does not call dispatchBackKey when actionId is not IME_ACTION_DONE`() {
+        folder.folderName = Mockito.mock(FolderNameEditText::class.java)
+
+        val result =
+            folder.onEditorAction(
+                Mockito.mock(TextView::class.java),
+                EditorInfo.IME_ACTION_NONE,
+                Mockito.mock(KeyEvent::class.java)
+            )
+
+        assertFalse(result)
+        verify(folder.folderName, times(0)).dispatchBackKey()
+    }
+
+    @Test
+    fun `in completeDragExit we close the folder when mIsOpen`() {
+        doNothing().`when`(folder).close(true)
+        folder.setIsOpen(true)
+        folder.rearrangeOnClose = false
+
+        folder.completeDragExit()
+
+        verify(folder, times(1)).close(true)
+        assertTrue(folder.rearrangeOnClose)
+    }
+
+    @Test
+    fun `in completeDragExit we want to rearrange on close when it is animating`() {
+        folder.setIsOpen(false)
+        folder.rearrangeOnClose = false
+        folder.state = STATE_ANIMATING
+
+        folder.completeDragExit()
+
+        verify(folder, times(0)).close(true)
+        assertTrue(folder.rearrangeOnClose)
+    }
+
+    @Test
+    fun `in completeDragExit we want to call rearrangeChildren and clearDragInfo when not open and not animating`() {
+        doNothing().`when`(folder).rearrangeChildren()
+        doNothing().`when`(folder).clearDragInfo()
+        folder.setIsOpen(false)
+        folder.rearrangeOnClose = false
+        folder.state = STATE_CLOSED
+
+        folder.completeDragExit()
+
+        verify(folder, times(0)).close(true)
+        assertFalse(folder.rearrangeOnClose)
+        verify(folder, times(1)).rearrangeChildren()
+        verify(folder, times(1)).clearDragInfo()
+    }
+
+    @Test
+    fun `clearDragInfo should set current drag view to null and isExternalDrag to false`() {
+        folder.currentDragView = Mockito.mock(DragView::class.java)
+        folder.isExternalDrag = true
+
+        folder.clearDragInfo()
+
+        assertNull(folder.currentDragView)
+        assertFalse(folder.isExternalDrag)
+    }
+
+    @Test
+    fun `onDragExit should set alarm if drag is not complete`() {
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        dragObject.dragComplete = false
+
+        folder.onDragExit(dragObject)
+
+        verify(folder.onExitAlarm, times(1)).setOnAlarmListener(folder.mOnExitAlarmListener)
+        verify(folder.onExitAlarm, times(1)).setAlarm(ON_EXIT_CLOSE_DELAY.toLong())
+    }
+
+    @Test
+    fun `onDragExit should not set alarm if drag is complete`() {
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        dragObject.dragComplete = true
+
+        folder.onDragExit(dragObject)
+
+        verify(folder.onExitAlarm, times(0)).setOnAlarmListener(folder.mOnExitAlarmListener)
+        verify(folder.onExitAlarm, times(0)).setAlarm(ON_EXIT_CLOSE_DELAY.toLong())
+    }
+
+    @Test
+    fun `onDragExit should not clear scroll hint if already SCROLL_NONE`() {
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        folder.scrollHintDir = SCROLL_NONE
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+
+        folder.onDragExit(dragObject)
+
+        verify(folder.mContent, times(0)).clearScrollHint()
+    }
+
+    @Test
+    fun `onDragExit should clear scroll hint if not SCROLL_NONE and then set scroll hint to scroll none`() {
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        folder.scrollHintDir = SCROLL_LEFT
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+
+        folder.onDragExit(dragObject)
+
+        verify(folder.mContent, times(1)).clearScrollHint()
+        assertEquals(folder.scrollHintDir, SCROLL_NONE)
+    }
+
+    @Test
+    fun `onDragExit we should cancel reorder pause and hint alarms`() {
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        val dragObject = Mockito.mock(DragObject::class.java)
+        folder.scrollHintDir = SCROLL_NONE
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        folder.reorderAlarm = Mockito.mock(Alarm::class.java)
+        folder.onScrollHintAlarm = Mockito.mock(Alarm::class.java)
+        folder.scrollPauseAlarm = Mockito.mock(Alarm::class.java)
+
+        folder.onDragExit(dragObject)
+
+        verify(folder.reorderAlarm, times(1)).cancelAlarm()
+        verify(folder.onScrollHintAlarm, times(1)).cancelAlarm()
+        verify(folder.scrollPauseAlarm, times(1)).cancelAlarm()
+        assertEquals(folder.scrollHintDir, SCROLL_NONE)
+    }
+
+    @Test
+    fun `when calling prepareAccessibilityDrop we should cancel pending reorder alarm and call onAlarm`() {
+        folder.reorderAlarm = Mockito.mock(Alarm::class.java)
+        folder.mReorderAlarmListener = Mockito.mock(OnAlarmListener::class.java)
+        `when`(folder.reorderAlarm.alarmPending()).thenReturn(true)
+
+        folder.prepareAccessibilityDrop()
+
+        verify(folder.reorderAlarm, times(1)).cancelAlarm()
+        verify(folder.mReorderAlarmListener, times(1)).onAlarm(folder.reorderAlarm)
+    }
+
+    @Test
+    fun `when calling prepareAccessibilityDrop we should not do anything if there is no pending alarm`() {
+        folder.reorderAlarm = Mockito.mock(Alarm::class.java)
+        folder.mReorderAlarmListener = Mockito.mock(OnAlarmListener::class.java)
+        `when`(folder.reorderAlarm.alarmPending()).thenReturn(false)
+
+        folder.prepareAccessibilityDrop()
+
+        verify(folder.reorderAlarm, times(0)).cancelAlarm()
+        verify(folder.mReorderAlarmListener, times(0)).onAlarm(folder.reorderAlarm)
+    }
+
+    @Test
+    fun `isDropEnabled should be true as long as state is not STATE_ANIMATING`() {
+        folder.state = STATE_CLOSED
+
+        val isDropEnabled = folder.isDropEnabled
+
+        assertTrue(isDropEnabled)
+    }
+
+    @Test
+    fun `isDropEnabled should be false if state is STATE_ANIMATING`() {
+        folder.state = STATE_ANIMATING
+
+        val isDropEnabled = folder.isDropEnabled
+
+        assertFalse(isDropEnabled)
+    }
+
+    @Test
+    fun `getItemCount should return the number of items in the folder`() {
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = folderInfo
+
+        val itemCount = folder.itemCount
+
+        assertEquals(itemCount, 2)
+    }
+
+    @Test
+    fun `hideItem should set the visibility of the corresponding ItemInfo to invisible`() {
+        val itemInfo = ItemInfo()
+        val view = View(context)
+        view.isVisible = true
+        doReturn(view).whenever(folder).getViewForInfo(itemInfo)
+
+        folder.hideItem(itemInfo)
+
+        assertFalse(view.isVisible)
+    }
+
+    @Test
+    fun `showItem should set the visibility of the corresponding ItemInfo to visible`() {
+        val itemInfo = ItemInfo()
+        val view = View(context)
+        view.isVisible = false
+        doReturn(view).whenever(folder).getViewForInfo(itemInfo)
+
+        folder.showItem(itemInfo)
+
+        assertTrue(view.isVisible)
+    }
+
+    @Test
+    fun `onDragEnter should cancel exit alarm and set the scroll area offset to dragRegionWidth divided by two minus xOffset`() {
+        folder.mPrevTargetRank = 1
+        val dragObject = Mockito.mock(DragObject::class.java)
+        val dragView = Mockito.mock(DragView::class.java)
+        dragObject.dragView = dragView
+        folder.onExitAlarm = Mockito.mock(Alarm::class.java)
+        `when`(dragObject.dragView.getDragRegionWidth()).thenReturn(100)
+        dragObject.xOffset = 20
+
+        folder.onDragEnter(dragObject)
+
+        verify(folder.onExitAlarm, times(1)).cancelAlarm()
+        assertEquals(-1, folder.mPrevTargetRank)
+        assertEquals(30, folder.scrollAreaOffset)
+    }
+
+    @Test
+    fun `acceptDrop should return true with the correct item type as a parameter`() {
+        val dragObject = Mockito.mock(DragObject::class.java)
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_APP_PAIR
+        dragObject.dragInfo = itemInfo
+
+        val result = folder.acceptDrop(dragObject)
+
+        assertTrue(result)
+    }
+
+    @Test
+    fun `acceptDrop should return false with the incorrect item type as a parameter`() {
+        val dragObject = Mockito.mock(DragObject::class.java)
+        val itemInfo = Mockito.mock(ItemInfo::class.java)
+        itemInfo.itemType = ITEM_TYPE_APPWIDGET
+        dragObject.dragInfo = itemInfo
+
+        val result = folder.acceptDrop(dragObject)
+
+        assertFalse(result)
+    }
+
+    @Test
+    fun `rearrangeChildren should return early if content view are not bound`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        folder.itemsInvalidated = false
+        doReturn(false).whenever(folder.mContent).areViewsBound()
+
+        folder.rearrangeChildren()
+
+        verify(folder.mContent, times(0)).arrangeChildren(folder.iconsInReadingOrder)
+        assertFalse(folder.itemsInvalidated)
+    }
+
+    @Test
+    fun `rearrangeChildren should call arrange children and invalidate items`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        folder.itemsInvalidated = false
+        doReturn(true).whenever(folder.mContent).areViewsBound()
+        val iconsInReadingOrderList = ArrayList<View>()
+        `when`(folder.iconsInReadingOrder).thenReturn(iconsInReadingOrderList)
+        doNothing().`when`(folder.mContent).arrangeChildren(iconsInReadingOrderList)
+
+        folder.rearrangeChildren()
+
+        verify(folder.mContent, times(1)).arrangeChildren(folder.iconsInReadingOrder)
+        assertTrue(folder.itemsInvalidated)
+    }
+
+    @Test
+    fun `getItemCount should return the size of info getContents size`() {
+        val folderInfo =
+            workspaceBuilder.createFolderInCell(FolderPoint(Point(1, 0), TWO_ICON_FOLDER_TYPE), 0)
+        folder.mInfo = folderInfo
+
+        val itemCount = folder.itemCount
+
+        assertEquals(2, itemCount)
+    }
+
+    @Test
+    fun `replaceFolderWithFinalItem should set mDestroyed to true if we replace folder with final item`() {
+        val launcherDelegate = Mockito.mock(LauncherDelegate::class.java)
+        folder.mLauncherDelegate = launcherDelegate
+        `when`(folder.mLauncherDelegate.replaceFolderWithFinalItem(folder)).thenReturn(true)
+
+        folder.replaceFolderWithFinalItem()
+
+        assertTrue(folder.isDestroyed)
+    }
+
+    @Test
+    fun `replaceFolderWithFinalItem should set mDestroyed to false if we do not replace folder with final item`() {
+        val launcherDelegate = Mockito.mock(LauncherDelegate::class.java)
+        folder.mLauncherDelegate = launcherDelegate
+        `when`(folder.mLauncherDelegate.replaceFolderWithFinalItem(folder)).thenReturn(false)
+
+        folder.replaceFolderWithFinalItem()
+
+        assertFalse(folder.isDestroyed)
+    }
+
+    @Test
+    fun `getContentAreaHeight should return maxContentAreaHeight`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredHeight).thenReturn(100)
+        `when`(folder.maxContentAreaHeight).thenReturn(50)
+
+        val height = folder.contentAreaHeight
+
+        assertEquals(50, height)
+    }
+
+    @Test
+    fun `getContentAreaHeight should return desiredHeight`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredHeight).thenReturn(50)
+        `when`(folder.maxContentAreaHeight).thenReturn(100)
+
+        val height = folder.contentAreaHeight
+
+        assertEquals(50, height)
+    }
+
+    @Test
+    fun `getContentAreaHeight should return MIN_CONTENT_DIMEN`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredHeight).thenReturn(1)
+        `when`(folder.maxContentAreaHeight).thenReturn(2)
+
+        val height = folder.contentAreaHeight
+
+        assertEquals(MIN_CONTENT_DIMEN, height)
+    }
+
+    @Test
+    fun `getContentAreaWidth should return desired width`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredWidth).thenReturn(50)
+
+        val width = folder.contentAreaWidth
+
+        assertEquals(50, width)
+    }
+
+    @Test
+    fun `getContentAreaWidth should return MIN_CONTENT_DIMEN`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredWidth).thenReturn(1)
+
+        val width = folder.contentAreaWidth
+
+        assertEquals(MIN_CONTENT_DIMEN, width)
+    }
+
+    @Test
+    fun `getFolderWidth should return padding left plus padding right plus desired width`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.mContent.desiredWidth).thenReturn(1)
+        `when`(folder.paddingLeft).thenReturn(10)
+        `when`(folder.paddingRight).thenReturn(10)
+
+        val width = folder.folderWidth
+
+        assertEquals(21, width)
+    }
+
+    @Test
+    fun `getFolderHeight with no params should return getFolderHeight`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.contentAreaHeight).thenReturn(100)
+        `when`(folder.getFolderHeight(folder.contentAreaHeight)).thenReturn(120)
+
+        val height = folder.folderHeight
+
+        assertEquals(120, height)
+    }
+
+    @Test
+    fun `getFolderWidth with contentAreaHeight should return padding top plus padding bottom plus contentAreaHeight plus footer height`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        `when`(folder.footerHeight).thenReturn(100)
+        `when`(folder.paddingTop).thenReturn(10)
+        `when`(folder.paddingBottom).thenReturn(10)
+
+        val height = folder.getFolderHeight(100)
+
+        assertEquals(220, height)
+    }
+
+    @Test
+    fun `onRemove should call removeItem with the correct views`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        val items =
+            arrayListOf<ItemInfo>(
+                Mockito.mock(ItemInfo::class.java),
+                Mockito.mock(ItemInfo::class.java)
+            )
+        val view1 = Mockito.mock(View::class.java)
+        val view2 = Mockito.mock(View::class.java)
+        doReturn(view1).whenever(folder).getViewForInfo(items[0])
+        doReturn(view2).whenever(folder).getViewForInfo(items[1])
+        doReturn(2).whenever(folder).itemCount
+
+        folder.onRemove(items)
+
+        verify(folder.mContent, times(1)).removeItem(view1)
+        verify(folder.mContent, times(1)).removeItem(view2)
+    }
+
+    @Test
+    fun `onRemove should set mRearrangeOnClose to true and not call rearrangeChildren if animating`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        folder.state = STATE_ANIMATING
+        val items =
+            arrayListOf<ItemInfo>(
+                Mockito.mock(ItemInfo::class.java),
+                Mockito.mock(ItemInfo::class.java)
+            )
+        val view1 = Mockito.mock(View::class.java)
+        val view2 = Mockito.mock(View::class.java)
+        doReturn(view1).whenever(folder).getViewForInfo(items[0])
+        doReturn(view2).whenever(folder).getViewForInfo(items[1])
+        doReturn(2).whenever(folder).itemCount
+
+        folder.onRemove(items)
+
+        assertTrue(folder.rearrangeOnClose)
+        verify(folder, times(0)).rearrangeChildren()
+    }
+
+    @Test
+    fun `onRemove should set not change mRearrangeOnClose and not call rearrangeChildren if not animating`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        folder.state = STATE_CLOSED
+        folder.rearrangeOnClose = false
+        val items =
+            arrayListOf<ItemInfo>(
+                Mockito.mock(ItemInfo::class.java),
+                Mockito.mock(ItemInfo::class.java)
+            )
+        val view1 = Mockito.mock(View::class.java)
+        val view2 = Mockito.mock(View::class.java)
+        doReturn(view1).whenever(folder).getViewForInfo(items[0])
+        doReturn(view2).whenever(folder).getViewForInfo(items[1])
+        doReturn(2).whenever(folder).itemCount
+
+        folder.onRemove(items)
+
+        assertFalse(folder.rearrangeOnClose)
+        verify(folder, times(1)).rearrangeChildren()
+    }
+
+    @Test
+    fun `onRemove should call close if mIsOpen is true and item count is less than or equal to one`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        val items =
+            arrayListOf<ItemInfo>(
+                Mockito.mock(ItemInfo::class.java),
+                Mockito.mock(ItemInfo::class.java)
+            )
+        val view1 = Mockito.mock(View::class.java)
+        val view2 = Mockito.mock(View::class.java)
+        doReturn(view1).whenever(folder).getViewForInfo(items[0])
+        doReturn(view2).whenever(folder).getViewForInfo(items[1])
+        doReturn(1).whenever(folder).itemCount
+        folder.setIsOpen(true)
+        doNothing().`when`(folder).close(true)
+
+        folder.onRemove(items)
+
+        verify(folder, times(1)).close(true)
+    }
+
+    @Test
+    fun `onRemove should call replaceFolderWithFinalItem if mIsOpen is false and item count is less than or equal to one`() {
+        folder.mContent = Mockito.mock(FolderPagedView::class.java)
+        val items =
+            arrayListOf<ItemInfo>(
+                Mockito.mock(ItemInfo::class.java),
+                Mockito.mock(ItemInfo::class.java)
+            )
+        val view1 = Mockito.mock(View::class.java)
+        val view2 = Mockito.mock(View::class.java)
+        doReturn(view1).whenever(folder).getViewForInfo(items[0])
+        doReturn(view2).whenever(folder).getViewForInfo(items[1])
+        doReturn(1).whenever(folder).itemCount
+        folder.setIsOpen(false)
+
+        folder.onRemove(items)
+
+        verify(folder, times(1)).replaceFolderWithFinalItem()
+    }
+
     companion object {
         const val TWO_ICON_FOLDER_TYPE = 'A'
     }
diff --git a/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt b/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt
new file mode 100644
index 0000000..9a8f957
--- /dev/null
+++ b/tests/src/com/android/launcher3/pageindicators/PageIndicatorDotsTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 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.pageindicators
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import com.android.launcher3.util.ActivityContextWrapper
+import junit.framework.TestCase.assertEquals
+import org.junit.Test
+import org.mockito.Mockito
+
+class PageIndicatorDotsTest {
+
+    private val context: Context =
+        ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+    private val pageIndicatorDots: PageIndicatorDots = Mockito.spy(PageIndicatorDots(context))
+
+    @Test
+    fun `setActiveMarker should set the active page to the parameter passed`() {
+        pageIndicatorDots.setActiveMarker(2)
+
+        assertEquals(2, pageIndicatorDots.activePage)
+    }
+
+    @Test
+    fun `setActiveMarker should set the active page to the parameter passed divided by two in two panel layouts`() {
+        pageIndicatorDots.mIsTwoPanels = true
+
+        pageIndicatorDots.setActiveMarker(5)
+
+        assertEquals(2, pageIndicatorDots.activePage)
+    }
+
+    @Test
+    fun `setMarkersCount should set the number of pages to the passed parameter and if the last page gets removed we want to go to the previous page`() {
+        pageIndicatorDots.setMarkersCount(3)
+
+        assertEquals(3, pageIndicatorDots.numPages)
+    }
+
+    @Test
+    fun `for setMarkersCount if the last page gets removed we want to go to the previous page`() {
+        pageIndicatorDots.setActiveMarker(2)
+
+        pageIndicatorDots.setMarkersCount(2)
+
+        assertEquals(1, pageIndicatorDots.activePage)
+        assertEquals(pageIndicatorDots.activePage.toFloat(), pageIndicatorDots.currentPosition)
+    }
+}