Merge "Change the misused logic" into main
diff --git a/go/quickstep/src/com/android/launcher3/util/MainThreadInitializedObject.java b/go/quickstep/src/com/android/launcher3/util/MainThreadInitializedObject.java
new file mode 100644
index 0000000..e1f3508
--- /dev/null
+++ b/go/quickstep/src/com/android/launcher3/util/MainThreadInitializedObject.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.util;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+
+import android.content.Context;
+import android.os.Looper;
+
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Utility class for defining singletons which are initiated on main thread.
+ */
+public class MainThreadInitializedObject<T extends SafeCloseable> {
+
+    private final ObjectProvider<T> mProvider;
+    private T mValue;
+
+    public MainThreadInitializedObject(ObjectProvider<T> provider) {
+        mProvider = provider;
+    }
+
+    public T get(Context context) {
+        Context app = context.getApplicationContext();
+        if (app instanceof ObjectSandbox sc) {
+            return sc.getObject(this);
+        }
+
+        if (mValue == null) {
+            if (Looper.myLooper() == Looper.getMainLooper()) {
+                mValue = TraceHelper.allowIpcs("main.thread.object", () -> mProvider.get(app));
+            } else {
+                try {
+                    return MAIN_EXECUTOR.submit(() -> get(context)).get();
+                } catch (InterruptedException | ExecutionException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        }
+        return mValue;
+    }
+
+    public interface ObjectProvider<T> {
+
+        T get(Context context);
+    }
+
+    /** Sandbox for isolating {@link MainThreadInitializedObject} instances from Launcher. */
+    public interface ObjectSandbox {
+
+        /**
+         * Find a cached object from mObjectMap if we have already created one. If not, generate
+         * an object using the provider.
+         */
+        <T extends SafeCloseable> T getObject(MainThreadInitializedObject<T> object);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 6241111..ade75eb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -97,12 +97,12 @@
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.R;
+import com.android.launcher3.allapps.ActivityAllAppsContainerView;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.apppairs.AppPairIcon;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.desktop.DesktopAppLaunchTransition;
 import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType;
-import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.logger.LauncherAtom;
@@ -149,6 +149,7 @@
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.LauncherBindableItemsContainer;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.RunnableList;
@@ -877,17 +878,23 @@
         }
     }
 
-    @Override
-    public DotInfo getDotInfoForItem(ItemInfo info) {
-        return getPopupDataProvider().getDotInfoForItem(info);
-    }
-
     @NonNull
     @Override
     public PopupDataProvider getPopupDataProvider() {
         return mControllers.taskbarPopupController.getPopupDataProvider();
     }
 
+    @NonNull
+    @Override
+    public LauncherBindableItemsContainer getContent() {
+        return mControllers.taskbarViewController.getContent();
+    }
+
+    @Override
+    public ActivityAllAppsContainerView<?> getAppsView() {
+        return mControllers.taskbarAllAppsController.getAppsView();
+    }
+
     @Override
     public View.AccessibilityDelegate getAccessibilityDelegate() {
         return mAccessibilityDelegate;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
index 5d8b821..a9ee584 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
@@ -35,11 +35,7 @@
 import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.R;
-import com.android.launcher3.dot.FolderDotInfo;
-import com.android.launcher3.folder.Folder;
-import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.notification.NotificationListener;
@@ -49,8 +45,6 @@
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.splitscreen.SplitShortcut;
 import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.LauncherBindableItemsContainer;
-import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.ShortcutUtil;
 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
 import com.android.launcher3.views.ActivityContext;
@@ -65,7 +59,6 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Objects;
-import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -93,7 +86,7 @@
 
     public TaskbarPopupController(TaskbarActivityContext context) {
         mContext = context;
-        mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
+        mPopupDataProvider = new PopupDataProvider(mContext);
     }
 
     public void init(TaskbarControllers controllers) {
@@ -132,39 +125,6 @@
         mAllowInitialSplitSelection = allowInitialSplitSelection;
     }
 
-    private void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
-        final PackageUserKey packageUserKey = new PackageUserKey(null, null);
-        Predicate<ItemInfo> matcher = info -> !packageUserKey.updateFromItemInfo(info)
-                || updatedDots.test(packageUserKey);
-
-        LauncherBindableItemsContainer.ItemOperator op = (info, v) -> {
-            if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView) {
-                if (matcher.test(info)) {
-                    ((BubbleTextView) v).applyDotState(info, true /* animate */);
-                }
-            } else if (info instanceof FolderInfo && v instanceof FolderIcon) {
-                FolderInfo fi = (FolderInfo) info;
-                if (fi.anyMatch(matcher)) {
-                    FolderDotInfo folderDotInfo = new FolderDotInfo();
-                    for (ItemInfo si : fi.getContents()) {
-                        folderDotInfo.addDotInfo(mPopupDataProvider.getDotInfoForItem(si));
-                    }
-                    ((FolderIcon) v).setDotInfo(folderDotInfo);
-                }
-            }
-
-            // process all the shortcuts
-            return false;
-        };
-
-        mControllers.taskbarViewController.mapOverItems(op);
-        Folder folder = Folder.getOpen(mContext);
-        if (folder != null) {
-            folder.iterateOverItems(op);
-        }
-        mControllers.taskbarAllAppsController.updateNotificationDots(updatedDots);
-    }
-
     /**
      * Shows the notifications and deep shortcuts associated with a Taskbar {@param icon}.
      * @return the container if shown or null.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index c92f20b..de8e286 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -62,7 +62,6 @@
 import com.android.launcher3.taskbar.customization.TaskbarDividerContainer;
 import com.android.launcher3.uioverrides.PredictedAppIcon;
 import com.android.launcher3.util.DisplayController;
-import com.android.launcher3.util.LauncherBindableItemsContainer;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ActivityContext;
 import com.android.quickstep.util.GroupTask;
@@ -1098,20 +1097,6 @@
     }
 
     /**
-     * Maps {@code op} over all the child views.
-     */
-    public void mapOverItems(LauncherBindableItemsContainer.ItemOperator op) {
-        // map over all the shortcuts on the taskbar
-        for (int i = 0; i < getChildCount(); i++) {
-            View item = getChildAt(i);
-            // TODO(b/344657629): Support GroupTask as well for notification dots/popup
-            if (item.getTag() instanceof ItemInfo itemInfo && op.evaluate(itemInfo, item)) {
-                return;
-            }
-        }
-    }
-
-    /**
      * Finds the first icon to match one of the given matchers, from highest to lowest priority.
      *
      * @return The first match, or All Apps button if no match was found.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 384468c..6ae13d4 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -83,14 +83,16 @@
 import com.android.launcher3.model.data.TaskItemInfo;
 import com.android.launcher3.taskbar.bubbles.BubbleBarController;
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
+import com.android.launcher3.taskbar.customization.TaskbarAllAppsButtonContainer;
+import com.android.launcher3.taskbar.customization.TaskbarDividerContainer;
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LauncherBindableItemsContainer;
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
 import com.android.launcher3.util.MultiPropertyFactory;
 import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
 import com.android.launcher3.util.MultiTranslateDelegate;
 import com.android.launcher3.util.MultiValueAlpha;
+import com.android.launcher3.util.SandboxContext;
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.SingleTask;
 import com.android.systemui.shared.recents.model.Task;
@@ -735,10 +737,21 @@
         for (View iconView : getIconViews()) {
             if (iconView instanceof BubbleTextView btv) {
                 btv.updateRunningState(getRunningAppState(btv));
+                if (shouldUpdateIconContentDescription(btv)) {
+                    btv.setContentDescription(
+                            btv.getContentDescription() + " " + btv.getIconStateDescription());
+                }
             }
         }
     }
 
+    private boolean shouldUpdateIconContentDescription(BubbleTextView btv) {
+        boolean isInDesktopMode = mControllers.taskbarDesktopModeController.isInDesktopMode();
+        boolean isAllAppsButton = btv instanceof TaskbarAllAppsButtonContainer;
+        boolean isDividerButton = btv instanceof TaskbarDividerContainer;
+        return isInDesktopMode && !isAllAppsButton && !isDividerButton;
+    }
+
     /**
      * @return A set of Task ids of running apps that are pinned in the taskbar.
      */
@@ -1167,11 +1180,8 @@
         mTaskbarNavButtonTranslationY.updateValue(-deviceProfile.getTaskbarOffsetY());
     }
 
-    /**
-     * Maps the given operator to all the top-level children of TaskbarView.
-     */
-    public void mapOverItems(LauncherBindableItemsContainer.ItemOperator op) {
-        mTaskbarView.mapOverItems(op);
+    public LauncherBindableItemsContainer getContent() {
+        return mModelCallbacks;
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
index ddbf3b7..6c55b28 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsController.java
@@ -35,7 +35,6 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Predicate;
 /**
  * Handles the all apps overlay window initialization, updates, and its data.
  * <p>
@@ -120,13 +119,6 @@
         mZeroStateSearchSuggestions = zeroStateSearchSuggestions;
     }
 
-    /** Updates the current notification dots. */
-    public void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
-        if (mAppsView != null) {
-            mAppsView.getAppsStore().updateNotificationDots(updatedDots);
-        }
-    }
-
     /** Toggles visibility of {@link TaskbarAllAppsContainerView} in the overlay window. */
     public void toggle() {
         toggle(false);
@@ -218,6 +210,11 @@
         mAppsView = null;
     }
 
+    @Nullable
+    public TaskbarAllAppsContainerView getAppsView() {
+        return mAppsView;
+    }
+
     @VisibleForTesting
     public int getTaskbarAllAppsTopPadding() {
         // Allow null-pointer since this should only be null if the apps view is not showing.
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java
index 0fce237..636d89b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayContext.java
@@ -18,12 +18,11 @@
 import android.content.Context;
 import android.view.View;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
-import com.android.launcher3.dot.DotInfo;
-import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.taskbar.BaseTaskbarContext;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
@@ -125,6 +124,7 @@
         return mDragController::startDragOnLongClick;
     }
 
+    @NonNull
     @Override
     public PopupDataProvider getPopupDataProvider() {
         return mTaskbarContext.getPopupDataProvider();
@@ -136,11 +136,6 @@
     }
 
     @Override
-    public DotInfo getDotInfoForItem(ItemInfo info) {
-        return mTaskbarContext.getDotInfoForItem(info);
-    }
-
-    @Override
     public void onDragStart() {}
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
index b230fa6..79328df 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.kt
@@ -36,8 +36,9 @@
 import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X
 import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y
 import com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW
-import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE
 import com.android.quickstep.util.AnimUtils
+import com.android.quickstep.views.AddDesktopButton
+import com.android.quickstep.views.ClearAllButton
 import com.android.quickstep.views.RecentsView
 import com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET
 import com.android.quickstep.views.RecentsView.CONTENT_ALPHA
@@ -287,8 +288,8 @@
         val clearAllButtonAlpha =
             if (state.areElementsVisible(launcher, LauncherState.CLEAR_ALL_BUTTON)) 1f else 0f
         propertySetter.setFloat(
-            recentsView.clearAllButton.visibilityAlphaProperty,
-            MULTI_PROPERTY_VALUE,
+            recentsView.clearAllButton,
+            ClearAllButton.VISIBILITY_ALPHA,
             clearAllButtonAlpha,
             LINEAR,
         )
@@ -302,8 +303,8 @@
         )
         recentsView.addDeskButton?.let {
             propertySetter.setFloat(
-                it.visibilityAlphaProperty,
-                MULTI_PROPERTY_VALUE,
+                it,
+                AddDesktopButton.VISIBILITY_ALPHA,
                 if (state.areElementsVisible(launcher, LauncherState.ADD_DESK_BUTTON)) 1f else 0f,
                 LINEAR,
             )
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
index 88b7155..454a307 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
@@ -36,7 +36,6 @@
 import com.android.quickstep.views.TaskView
 import com.google.android.msdl.data.model.MSDLToken
 import kotlin.math.abs
-import kotlin.math.sign
 
 /** Touch controller for handling task view card dismiss swipes */
 class TaskViewDismissTouchController<CONTAINER>(
@@ -53,6 +52,8 @@
             recentsView.pagedOrientationHandler.upDownSwipeDirection,
         )
     private val isRtl = isRtl(container.resources)
+    private val upDirection: Int = recentsView.pagedOrientationHandler.getUpDirection(isRtl)
+
     private val tempTaskThumbnailBounds = Rect()
 
     private var taskBeingDragged: TaskView? = null
@@ -96,7 +97,11 @@
         }
 
         onControllerTouchEvent(ev)
-        return detector.isDraggingState && detector.wasInitialTouchPositive()
+        val upDirectionIsPositive = upDirection == SingleAxisSwipeDetector.DIRECTION_POSITIVE
+        val wasInitialTouchUp =
+            (upDirectionIsPositive && detector.wasInitialTouchPositive()) ||
+                (!upDirectionIsPositive && !detector.wasInitialTouchPositive())
+        return detector.isDraggingState && wasInitialTouchUp
     }
 
     override fun onControllerTouchEvent(ev: MotionEvent?): Boolean = detector.onTouchEvent(ev)
@@ -107,25 +112,27 @@
         if (!canInterceptTouch(ev)) {
             return false
         }
-
         taskBeingDragged =
             recentsView.taskViews
                 .firstOrNull {
                     recentsView.isTaskViewVisible(it) && container.dragLayer.isEventOverView(it, ev)
                 }
                 ?.also {
+                    val secondaryLayerDimension =
+                        recentsView.pagedOrientationHandler.getSecondaryDimension(
+                            container.dragLayer
+                        )
                     // Dismiss length as bottom of task so it is fully off screen when dismissed.
                     it.getThumbnailBounds(tempTaskThumbnailBounds, relativeToDragLayer = true)
-                    dismissLength = tempTaskThumbnailBounds.bottom
+                    dismissLength =
+                        recentsView.pagedOrientationHandler.getTaskDismissLength(
+                            secondaryLayerDimension,
+                            tempTaskThumbnailBounds,
+                        )
                     verticalFactor =
-                        recentsView.pagedOrientationHandler.secondaryTranslationDirectionFactor
+                        recentsView.pagedOrientationHandler.getTaskDismissVerticalDirection()
                 }
-
-        detector.setDetectableScrollConditions(
-            recentsView.pagedOrientationHandler.getUpDirection(isRtl),
-            /* ignoreSlop = */ false,
-        )
-
+        detector.setDetectableScrollConditions(upDirection, /* ignoreSlop= */ false)
         return true
     }
 
@@ -148,8 +155,8 @@
             boundToRange(abs(currentDisplacement), 0f, dismissLength.toFloat())
         // When swiping below origin, allow slight undershoot to simulate resisting the movement.
         val totalDisplacement =
-            if (isDisplacementPositiveDirection(currentDisplacement))
-                boundedDisplacement * sign(currentDisplacement)
+            if (recentsView.pagedOrientationHandler.isGoingUp(currentDisplacement, isRtl))
+                boundedDisplacement * verticalFactor
             else
                 mapToRange(
                     boundedDisplacement,
@@ -158,7 +165,7 @@
                     0f,
                     container.resources.getDimension(R.dimen.task_dismiss_max_undershoot),
                     DECELERATE,
-                )
+                ) * -verticalFactor
         taskBeingDragged.secondaryDismissTranslationProperty.setValue(
             taskBeingDragged,
             totalDisplacement,
@@ -207,8 +214,9 @@
         }
         val isBeyondDismissThreshold =
             abs(currentDisplacement) > abs(DISMISS_THRESHOLD_FRACTION * dismissLength)
-        val isFlingingTowardsDismiss = detector.isFling(velocity) && velocity < 0
-        val isFlingingTowardsRestState = detector.isFling(velocity) && velocity > 0
+        val velocityIsGoingUp = recentsView.pagedOrientationHandler.isGoingUp(velocity, isRtl)
+        val isFlingingTowardsDismiss = detector.isFling(velocity) && velocityIsGoingUp
+        val isFlingingTowardsRestState = detector.isFling(velocity) && !velocityIsGoingUp
         val isDismissing =
             isFlingingTowardsDismiss || (isBeyondDismissThreshold && !isFlingingTowardsRestState)
         springAnimation =
@@ -232,10 +240,6 @@
             }
     }
 
-    // Returns if the current task being dragged is towards "positive" (e.g. dismissal).
-    private fun isDisplacementPositiveDirection(displacement: Float): Boolean =
-        sign(displacement) == sign(verticalFactor.toFloat())
-
     private fun clearState() {
         detector.finishedScrolling()
         detector.setDetectableScrollConditions(0, false)
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt
index c740dad..8ee552d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt
@@ -53,6 +53,7 @@
             recentsView.pagedOrientationHandler.upDownSwipeDirection,
         )
     private val isRtl = isRtl(container.resources)
+    private val downDirection = recentsView.pagedOrientationHandler.getDownDirection(isRtl)
 
     private var taskBeingDragged: TaskView? = null
     private var launchEndDisplacement: Float = 0f
@@ -104,7 +105,11 @@
             }
         }
         onControllerTouchEvent(ev)
-        return detector.isDraggingState && !detector.wasInitialTouchPositive()
+        val downDirectionIsNegative = downDirection == SingleAxisSwipeDetector.DIRECTION_NEGATIVE
+        val wasInitialTouchDown =
+            (downDirectionIsNegative && !detector.wasInitialTouchPositive()) ||
+                (!downDirectionIsNegative && detector.wasInitialTouchPositive())
+        return detector.isDraggingState && wasInitialTouchDown
     }
 
     override fun onControllerTouchEvent(ev: MotionEvent) = detector.onTouchEvent(ev)
@@ -120,15 +125,12 @@
                 }
                 ?.also {
                     verticalFactor =
-                        recentsView.pagedOrientationHandler.secondaryTranslationDirectionFactor
+                        recentsView.pagedOrientationHandler.getTaskDragDisplacementFactor(isRtl)
                 }
         if (!canTaskLaunchTaskView(taskBeingDragged)) {
             return false
         }
-        detector.setDetectableScrollConditions(
-            recentsView.pagedOrientationHandler.getDownDirection(isRtl),
-            /* ignoreSlop = */ false,
-        )
+        detector.setDetectableScrollConditions(downDirection, /* ignoreSlop= */ false)
         return true
     }
 
@@ -143,7 +145,10 @@
             recentsView.createTaskLaunchAnimation(taskBeingDragged, maxDuration, ZOOM_IN)
         // Since the thumbnail is what is filling the screen, based the end displacement on it.
         taskBeingDragged.getThumbnailBounds(tempRect, /* relativeToDragLayer= */ true)
-        launchEndDisplacement = (secondaryLayerDimension - tempRect.bottom).toFloat()
+        launchEndDisplacement =
+            recentsView.pagedOrientationHandler
+                .getTaskLaunchLength(secondaryLayerDimension, tempRect)
+                .toFloat() * verticalFactor
         playbackController =
             pendingAnimation.createPlaybackController()?.apply {
                 taskViewRecentsTouchContext.onUserControlledAnimationCreated(this)
@@ -163,8 +168,9 @@
 
         val isBeyondLaunchThreshold =
             abs(playbackController.progressFraction) > abs(LAUNCH_THRESHOLD_FRACTION)
-        val isFlingingTowardsLaunch = detector.isFling(velocity) && velocity > 0
-        val isFlingingTowardsRestState = detector.isFling(velocity) && velocity < 0
+        val velocityIsNegative = !recentsView.pagedOrientationHandler.isGoingUp(velocity, isRtl)
+        val isFlingingTowardsLaunch = detector.isFling(velocity) && velocityIsNegative
+        val isFlingingTowardsRestState = detector.isFling(velocity) && !velocityIsNegative
         val isLaunching =
             isFlingingTowardsLaunch || (isBeyondLaunchThreshold && !isFlingingTowardsRestState)
 
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index 51f2a33..2631efe 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -26,7 +26,6 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_SCRIM_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
-import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE;
 import static com.android.quickstep.fallback.RecentsState.OVERVIEW_SPLIT_SELECT;
 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
 import static com.android.quickstep.views.RecentsView.DESKTOP_CAROUSEL_DETACH_PROGRESS;
@@ -52,6 +51,8 @@
 import com.android.launcher3.anim.PropertySetter;
 import com.android.launcher3.statemanager.StateManager.StateHandler;
 import com.android.launcher3.states.StateAnimationConfig;
+import com.android.quickstep.views.AddDesktopButton;
+import com.android.quickstep.views.ClearAllButton;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
 
@@ -97,12 +98,12 @@
     private void setProperties(RecentsState state, StateAnimationConfig config,
             PropertySetter setter) {
         float clearAllButtonAlpha = state.hasClearAllButton() ? 1 : 0;
-        setter.setFloat(mRecentsView.getClearAllButton().visibilityAlphaProperty,
-                MULTI_PROPERTY_VALUE, clearAllButtonAlpha, LINEAR);
+        setter.setFloat(mRecentsView.getClearAllButton(),
+                ClearAllButton.VISIBILITY_ALPHA, clearAllButtonAlpha, LINEAR);
         if (mRecentsView.getAddDeskButton() != null) {
             float addDeskButtonAlpha = state.hasAddDeskButton() ? 1 : 0;
-            setter.setFloat(mRecentsView.getAddDeskButton().getVisibilityAlphaProperty(),
-                    MULTI_PROPERTY_VALUE, addDeskButtonAlpha, LINEAR);
+            setter.setFloat(mRecentsView.getAddDeskButton(), AddDesktopButton.VISIBILITY_ALPHA,
+                    addDeskButtonAlpha, LINEAR);
         }
         float overviewButtonAlpha = state.hasOverviewActions() ? 1 : 0;
         setter.setFloat(mRecentsViewContainer.getActionsView().getVisibilityAlpha(),
diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
index ee9b3e8..e3c9b2b 100644
--- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
@@ -311,6 +311,14 @@
 
     override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) 1 else -1
 
+    override fun getTaskDismissVerticalDirection(): Int = 1
+
+    override fun getTaskDismissLength(secondaryDimension: Int, taskThumbnailBounds: Rect): Int =
+        secondaryDimension - taskThumbnailBounds.left
+
+    override fun getTaskLaunchLength(secondaryDimension: Int, taskThumbnailBounds: Rect): Int =
+        taskThumbnailBounds.left
+
     /* -------------------- */
 
     override fun getChildBounds(
diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.kt
index f2e53fc..1883649 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.kt
@@ -284,6 +284,14 @@
     // Ignore rtl since it only affects X value displacement, Y displacement doesn't change
     override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = 1
 
+    override fun getTaskDismissVerticalDirection(): Int = -1
+
+    override fun getTaskDismissLength(secondaryDimension: Int, taskThumbnailBounds: Rect): Int =
+        taskThumbnailBounds.bottom
+
+    override fun getTaskLaunchLength(secondaryDimension: Int, taskThumbnailBounds: Rect): Int =
+        secondaryDimension - taskThumbnailBounds.bottom
+
     /* -------------------- */
 
     override fun getDistanceToBottomOfRect(dp: DeviceProfile, rect: Rect): Int =
diff --git a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
index 9b3c467..a7bc93b 100644
--- a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
@@ -82,13 +82,13 @@
 
     fun getSplitTranslationDirectionFactor(
         @StagePosition stagePosition: Int,
-        deviceProfile: DeviceProfile
+        deviceProfile: DeviceProfile,
     ): Int
 
     fun <T> getSplitSelectTaskOffset(
         primary: FloatProperty<T>,
         secondary: FloatProperty<T>,
-        deviceProfile: DeviceProfile
+        deviceProfile: DeviceProfile,
     ): Pair<FloatProperty<T>, FloatProperty<T>>
 
     fun getDistanceToBottomOfRect(dp: DeviceProfile, rect: Rect): Int
@@ -101,7 +101,7 @@
         placeholderInset: Int,
         dp: DeviceProfile,
         @StagePosition stagePosition: Int,
-        out: Rect
+        out: Rect,
     )
 
     /**
@@ -128,7 +128,7 @@
         drawableWidth: Int,
         drawableHeight: Int,
         dp: DeviceProfile,
-        @StagePosition stagePosition: Int
+        @StagePosition stagePosition: Int,
     )
 
     /**
@@ -143,7 +143,7 @@
         out: View,
         dp: DeviceProfile,
         splitInstructionsHeight: Int,
-        splitInstructionsWidth: Int
+        splitInstructionsWidth: Int,
     )
 
     /**
@@ -159,7 +159,7 @@
         dp: DeviceProfile,
         @StagePosition stagePosition: Int,
         out1: Rect,
-        out2: Rect
+        out2: Rect,
     )
 
     fun getDefaultSplitPosition(deviceProfile: DeviceProfile): Int
@@ -174,7 +174,7 @@
         dp: DeviceProfile,
         outRect: Rect,
         splitInfo: SplitConfigurationOptions.SplitBounds,
-        @StagePosition desiredStagePosition: Int
+        @StagePosition desiredStagePosition: Int,
     )
 
     fun measureGroupedTaskViewThumbnailBounds(
@@ -185,7 +185,7 @@
         splitBoundsConfig: SplitConfigurationOptions.SplitBounds,
         dp: DeviceProfile,
         isRtl: Boolean,
-        inSplitSelection: Boolean
+        inSplitSelection: Boolean,
     )
 
     /**
@@ -198,7 +198,7 @@
         dp: DeviceProfile,
         splitBoundsConfig: SplitConfigurationOptions.SplitBounds,
         parentWidth: Int,
-        parentHeight: Int
+        parentHeight: Int,
     ): Pair<Point, Point>
 
     // Overview TaskMenuView methods
@@ -208,7 +208,7 @@
         taskIconMargin: Int,
         taskIconHeight: Int,
         thumbnailTopMargin: Int,
-        isRtl: Boolean
+        isRtl: Boolean,
     )
 
     /**
@@ -216,14 +216,14 @@
      */
     fun setIconAppChipChildrenParams(
         iconParams: FrameLayout.LayoutParams,
-        chipChildMarginStart: Int
+        chipChildMarginStart: Int,
     )
 
     fun setIconAppChipMenuParams(
         iconAppChipView: IconAppChipView,
         iconMenuParams: FrameLayout.LayoutParams,
         iconMenuMargin: Int,
-        thumbnailTopMargin: Int
+        thumbnailTopMargin: Int,
     )
 
     fun setSplitIconParams(
@@ -252,7 +252,7 @@
         thumbnailView: View,
         deviceProfile: DeviceProfile,
         taskInsetMargin: Float,
-        taskViewIcon: View
+        taskViewIcon: View,
     ): Float
 
     fun getTaskMenuY(
@@ -261,20 +261,20 @@
         stagePosition: Int,
         taskMenuView: View,
         taskInsetMargin: Float,
-        taskViewIcon: View
+        taskViewIcon: View,
     ): Float
 
     fun getTaskMenuWidth(
         thumbnailView: View,
         deviceProfile: DeviceProfile,
-        @StagePosition stagePosition: Int
+        @StagePosition stagePosition: Int,
     ): Int
 
     fun getTaskMenuHeight(
         taskInsetMargin: Float,
         deviceProfile: DeviceProfile,
         taskMenuX: Float,
-        taskMenuY: Float
+        taskMenuY: Float,
     ): Int
 
     /**
@@ -285,7 +285,7 @@
         deviceProfile: DeviceProfile,
         taskMenuLayout: LinearLayout,
         dividerSpacing: Int,
-        dividerDrawable: ShapeDrawable
+        dividerDrawable: ShapeDrawable,
     )
 
     /**
@@ -295,7 +295,7 @@
     fun setLayoutParamsForTaskMenuOptionItem(
         lp: LinearLayout.LayoutParams,
         viewGroup: LinearLayout,
-        deviceProfile: DeviceProfile
+        deviceProfile: DeviceProfile,
     )
 
     /** Layout a Digital Wellbeing Banner on its parent. TaskView. */
@@ -306,7 +306,7 @@
         deviceProfile: DeviceProfile,
         snapshotViewWidth: Int,
         snapshotViewHeight: Int,
-        banner: View
+        banner: View,
     )
 
     /**
@@ -322,7 +322,7 @@
         deviceProfile: DeviceProfile,
         thumbnailViews: Array<View>,
         desiredTaskId: Int,
-        banner: View
+        banner: View,
     ): Pair<Float, Float>
 
     // The following are only used by TaskViewTouchHandler.
@@ -342,6 +342,15 @@
     /** @return Either 1 or -1, a factor to multiply by so the animation goes the correct way. */
     fun getTaskDragDisplacementFactor(isRtl: Boolean): Int
 
+    /** @return Either 1 or -1, the direction sign towards task dismiss. */
+    fun getTaskDismissVerticalDirection(): Int
+
+    /** @return the length to drag a task off screen for dismiss. */
+    fun getTaskDismissLength(secondaryDimension: Int, taskThumbnailBounds: Rect): Int
+
+    /** @return the length to drag a task to full screen for launch. */
+    fun getTaskLaunchLength(secondaryDimension: Int, taskThumbnailBounds: Rect): Int
+
     /**
      * Maps the velocity from the coordinate plane of the foreground app to that of Launcher's
      * (which now will always be portrait)
@@ -371,7 +380,7 @@
         floatingTask: View,
         onScreenRect: RectF,
         @StagePosition stagePosition: Int,
-        dp: DeviceProfile
+        dp: DeviceProfile,
     ): Float
 
     /**
diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
index 7fea55d..1f9f752 100644
--- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
@@ -344,6 +344,14 @@
 
     override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) -1 else 1
 
+    override fun getTaskDismissVerticalDirection(): Int = -1
+
+    override fun getTaskDismissLength(secondaryDimension: Int, taskThumbnailBounds: Rect): Int =
+        taskThumbnailBounds.right
+
+    override fun getTaskLaunchLength(secondaryDimension: Int, taskThumbnailBounds: Rect): Int =
+        secondaryDimension - taskThumbnailBounds.right
+
     /* -------------------- */
 
     override fun getSplitIconsPosition(
diff --git a/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
index 244c3b2..b83acf0 100644
--- a/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
+++ b/quickstep/src/com/android/quickstep/views/AddDesktopButton.kt
@@ -20,10 +20,12 @@
 import android.graphics.Canvas
 import android.graphics.Rect
 import android.util.AttributeSet
-import android.view.View
+import android.util.FloatProperty
 import android.widget.ImageButton
 import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X
 import com.android.launcher3.R
+import com.android.launcher3.util.KFloatProperty
+import com.android.launcher3.util.MultiPropertyDelegate
 import com.android.launcher3.util.MultiPropertyFactory
 import com.android.launcher3.util.MultiValueAlpha
 import com.android.quickstep.util.BorderAnimator
@@ -36,44 +38,17 @@
 class AddDesktopButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
     ImageButton(context, attrs) {
 
-    private enum class Alpha {
-        CONTENT,
-        VISIBILITY,
-    }
-
     private val addDeskButtonAlpha = MultiValueAlpha(this, Alpha.entries.size)
-
-    var contentAlpha
-        set(value) {
-            addDeskButtonAlpha.get(Alpha.CONTENT.ordinal).value = value
-        }
-        get() = addDeskButtonAlpha.get(Alpha.CONTENT.ordinal).value
-
-    val visibilityAlphaProperty: MultiPropertyFactory<View>.MultiProperty
-        get() = addDeskButtonAlpha.get(Alpha.VISIBILITY.ordinal)
-
-    private enum class TranslationX {
-        GRID,
-        OFFSET,
-    }
+    var contentAlpha by MultiPropertyDelegate(addDeskButtonAlpha, Alpha.CONTENT)
+    var visibilityAlpha by MultiPropertyDelegate(addDeskButtonAlpha, Alpha.VISIBILITY)
 
     private val multiTranslationX =
         MultiPropertyFactory(this, VIEW_TRANSLATE_X, TranslationX.entries.size) { a: Float, b: Float
             ->
             a + b
         }
-
-    var gridTranslationX
-        get() = multiTranslationX[TranslationX.GRID.ordinal].value
-        set(value) {
-            multiTranslationX[TranslationX.GRID.ordinal].value = value
-        }
-
-    var offsetTranslationX
-        get() = multiTranslationX[TranslationX.OFFSET.ordinal].value
-        set(value) {
-            multiTranslationX[TranslationX.OFFSET.ordinal].value = value
-        }
+    var gridTranslationX by MultiPropertyDelegate(multiTranslationX, TranslationX.GRID)
+    var offsetTranslationX by MultiPropertyDelegate(multiTranslationX, TranslationX.OFFSET)
 
     private val focusBorderAnimator: BorderAnimator =
         createSimpleBorderAnimator(
@@ -109,7 +84,7 @@
         }
     }
 
-    protected fun getScrollAdjustment(showAsGrid: Boolean): Int =
+    fun getScrollAdjustment(showAsGrid: Boolean): Int =
         if (showAsGrid) gridTranslationX.toInt() else 0
 
     private fun getBorderBounds(bounds: Rect) {
@@ -123,4 +98,20 @@
         focusBorderAnimator.drawBorder(canvas)
         super.draw(canvas)
     }
+
+    companion object {
+        private enum class Alpha {
+            CONTENT,
+            VISIBILITY,
+        }
+
+        private enum class TranslationX {
+            GRID,
+            OFFSET,
+        }
+
+        @JvmField
+        val VISIBILITY_ALPHA: FloatProperty<AddDesktopButton> =
+            KFloatProperty(AddDesktopButton::visibilityAlpha)
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.kt b/quickstep/src/com/android/quickstep/views/ClearAllButton.kt
index 6420ece..69c85ee 100644
--- a/quickstep/src/com/android/quickstep/views/ClearAllButton.kt
+++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.kt
@@ -19,11 +19,12 @@
 import android.graphics.Canvas
 import android.graphics.Rect
 import android.util.AttributeSet
-import android.view.View
+import android.util.FloatProperty
 import android.widget.Button
 import com.android.launcher3.Flags.enableFocusOutline
 import com.android.launcher3.R
-import com.android.launcher3.util.MultiPropertyFactory
+import com.android.launcher3.util.KFloatProperty
+import com.android.launcher3.util.MultiPropertyDelegate
 import com.android.launcher3.util.MultiValueAlpha
 import com.android.quickstep.util.BorderAnimator
 import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator
@@ -33,13 +34,6 @@
 class ClearAllButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
     Button(context, attrs) {
 
-    private enum class Alpha {
-        SCROLL,
-        CONTENT,
-        VISIBILITY,
-        DISMISS,
-    }
-
     private val clearAllButtonAlpha =
         object : MultiValueAlpha(this, Alpha.entries.size) {
             override fun apply(value: Float) {
@@ -47,32 +41,10 @@
                 isClickable = value >= 1f
             }
         }
-
-    var scrollAlpha
-        set(value) {
-            clearAllButtonAlpha.get(Alpha.SCROLL.ordinal).value = value
-        }
-        get() = clearAllButtonAlpha.get(Alpha.SCROLL.ordinal).value
-
-    var contentAlpha
-        set(value) {
-            clearAllButtonAlpha.get(Alpha.CONTENT.ordinal).value = value
-        }
-        get() = clearAllButtonAlpha.get(Alpha.CONTENT.ordinal).value
-
-    @JvmField
-    val visibilityAlphaProperty: MultiPropertyFactory<View>.MultiProperty =
-        clearAllButtonAlpha.get(Alpha.VISIBILITY.ordinal)
-
-    var dismissAlpha
-        set(value) {
-            dismissAlphaProperty.value = value
-        }
-        get() = dismissAlphaProperty.value
-
-    @JvmField
-    val dismissAlphaProperty: MultiPropertyFactory<View>.MultiProperty =
-        clearAllButtonAlpha.get(Alpha.DISMISS.ordinal)
+    var scrollAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.SCROLL)
+    var contentAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.CONTENT)
+    var visibilityAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.VISIBILITY)
+    var dismissAlpha by MultiPropertyDelegate(clearAllButtonAlpha, Alpha.DISMISS)
 
     var fullscreenProgress = 1f
         set(value) {
@@ -257,4 +229,21 @@
         if (fullscreenProgress > 0) endTranslation else 0f
 
     private fun getGridTrans(endTranslation: Float) = if (gridProgress > 0) endTranslation else 0f
+
+    companion object {
+        private enum class Alpha {
+            SCROLL,
+            CONTENT,
+            VISIBILITY,
+            DISMISS,
+        }
+
+        @JvmField
+        val VISIBILITY_ALPHA: FloatProperty<ClearAllButton> =
+            KFloatProperty(ClearAllButton::visibilityAlpha)
+
+        @JvmField
+        val DISMISS_ALPHA: FloatProperty<ClearAllButton> =
+            KFloatProperty(ClearAllButton::dismissAlpha)
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index e186838..44bf82c 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -66,6 +66,7 @@
 import static com.android.quickstep.BaseContainerInterface.getTaskDimension;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
 import static com.android.quickstep.util.LogUtils.splitFailureMessage;
+import static com.android.quickstep.views.ClearAllButton.DISMISS_ALPHA;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_ACTIONS_IN_MENU;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_DESKTOP;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION;
@@ -73,6 +74,7 @@
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NO_TASKS;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_SPLIT_SELECT_ACTIVE;
 import static com.android.quickstep.views.RecentsViewUtils.DESK_EXPLODE_PROGRESS;
+import static com.android.quickstep.views.TaskView.SPLIT_ALPHA;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -3974,8 +3976,7 @@
 
                 // Change alpha of clear all if translating grid to hide it
                 if (isClearAllHidden) {
-                    anim.setFloat(mClearAllButton.dismissAlphaProperty, MULTI_PROPERTY_VALUE, 0,
-                            LINEAR);
+                    anim.setFloat(mClearAllButton, DISMISS_ALPHA, 0, LINEAR);
                     anim.addListener(new AnimatorListenerAdapter() {
                         @Override
                         public void onAnimationEnd(Animator animation) {
@@ -5354,8 +5355,7 @@
                                 clampToProgress(timings.getDesktopTaskScaleInterpolator(), 0f,
                                         timings.getDesktopFadeSplitAnimationEndOffset()));
                     }
-                    builder.addFloat(taskView.getSplitAlphaProperty(),
-                            MULTI_PROPERTY_VALUE, 1f, 0f,
+                    builder.addFloat(taskView, SPLIT_ALPHA, 1f, 0f,
                             clampToProgress(deskTopFadeInterPolator, 0f,
                                     timings.getDesktopFadeSplitAnimationEndOffset()));
                 }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index 67318ac..31ae890 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -28,6 +28,7 @@
 import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
 import com.android.systemui.shared.recents.model.ThumbnailData
 import java.util.function.BiConsumer
+import kotlin.reflect.KMutableProperty1
 
 /**
  * Helper class for [RecentsView]. This util class contains refactored and extracted functions from
@@ -305,16 +306,19 @@
         }
 
     companion object {
-        @JvmField
-        val DESK_EXPLODE_PROGRESS =
-            object : FloatProperty<RecentsView<*, *>>("deskExplodeProgress") {
-                override fun setValue(recentsView: RecentsView<*, *>, value: Float) {
-                    recentsView.mUtils.deskExplodeProgress = value
-                }
+        class RecentsViewFloatProperty(
+            private val utilsProperty: KMutableProperty1<RecentsViewUtils, Float>
+        ) : FloatProperty<RecentsView<*, *>>(utilsProperty.name) {
+            override fun get(recentsView: RecentsView<*, *>): Float =
+                utilsProperty.get(recentsView.mUtils)
 
-                override fun get(recentsView: RecentsView<*, *>) =
-                    recentsView.mUtils.deskExplodeProgress
+            override fun setValue(recentsView: RecentsView<*, *>, value: Float) {
+                utilsProperty.set(recentsView.mUtils, value)
             }
+        }
+
+        @JvmField
+        val DESK_EXPLODE_PROGRESS = RecentsViewFloatProperty(RecentsViewUtils::deskExplodeProgress)
 
         val TEMP_RECT = Rect()
     }
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index ad60329..276318c 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -61,8 +61,9 @@
 import com.android.launcher3.testing.shared.TestProtocol
 import com.android.launcher3.util.CancellableTask
 import com.android.launcher3.util.Executors
+import com.android.launcher3.util.KFloatProperty
+import com.android.launcher3.util.MultiPropertyDelegate
 import com.android.launcher3.util.MultiPropertyFactory
-import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE
 import com.android.launcher3.util.MultiValueAlpha
 import com.android.launcher3.util.RunnableList
 import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
@@ -440,28 +441,10 @@
             applyTranslationX()
         }
 
-    private val taskViewAlpha = MultiValueAlpha(this, NUM_ALPHA_CHANNELS)
-
-    protected var stableAlpha
-        set(value) {
-            taskViewAlpha.get(ALPHA_INDEX_STABLE).value = value
-        }
-        get() = taskViewAlpha.get(ALPHA_INDEX_STABLE).value
-
-    var attachAlpha
-        set(value) {
-            taskViewAlpha.get(ALPHA_INDEX_ATTACH).value = value
-        }
-        get() = taskViewAlpha.get(ALPHA_INDEX_ATTACH).value
-
-    var splitAlpha
-        set(value) {
-            splitAlphaProperty.value = value
-        }
-        get() = splitAlphaProperty.value
-
-    val splitAlphaProperty: MultiPropertyFactory<View>.MultiProperty
-        get() = taskViewAlpha.get(ALPHA_INDEX_SPLIT)
+    private val taskViewAlpha = MultiValueAlpha(this, Alpha.entries.size)
+    protected var stableAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.STABLE)
+    var attachAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.ATTACH)
+    var splitAlpha by MultiPropertyDelegate(taskViewAlpha, Alpha.SPLIT)
 
     protected var shouldShowScreenshot = false
         get() = !isRunningTask || field
@@ -515,16 +498,16 @@
         MultiPropertyFactory(
             this,
             SETTLED_PROGRESS,
-            SETTLED_PROGRESS_INDEX_COUNT,
+            SettledProgress.entries.size,
             { x: Float, y: Float -> x * y },
             1f,
         )
-    private val settledProgressFullscreen =
-        settledProgressPropertyFactory.get(SETTLED_PROGRESS_INDEX_FULLSCREEN)
-    private val settledProgressGesture =
-        settledProgressPropertyFactory.get(SETTLED_PROGRESS_INDEX_GESTURE)
-    private val settledProgressDismiss =
-        settledProgressPropertyFactory.get(SETTLED_PROGRESS_INDEX_DISMISS)
+    private var settledProgressFullscreen by
+        MultiPropertyDelegate(settledProgressPropertyFactory, SettledProgress.Fullscreen)
+    private var settledProgressGesture by
+        MultiPropertyDelegate(settledProgressPropertyFactory, SettledProgress.Gesture)
+    private var settledProgressDismiss by
+        MultiPropertyDelegate(settledProgressPropertyFactory, SettledProgress.Dismiss)
 
     private var viewModel: TaskViewModel? = null
     private val dispatcherProvider: DispatcherProvider by RecentsDependencies.inject()
@@ -536,7 +519,7 @@
      * interpolator.
      */
     fun getDismissIconFadeInAnimator(): ObjectAnimator =
-        ObjectAnimator.ofFloat(settledProgressDismiss, MULTI_PROPERTY_VALUE, 1f).apply {
+        ObjectAnimator.ofFloat(this, SETTLED_PROGRESS_DISMISS, 1f).apply {
             duration = FADE_IN_ICON_DURATION
             interpolator = FADE_IN_ICON_INTERPOLATOR
         }
@@ -548,8 +531,7 @@
      */
     fun getDismissIconFadeOutAnimator(): ObjectAnimator =
         AnimatedFloat { v ->
-                settledProgressDismiss.value =
-                    SETTLED_PROGRESS_FAST_OUT_INTERPOLATOR.getInterpolation(v)
+                settledProgressDismiss = SETTLED_PROGRESS_FAST_OUT_INTERPOLATOR.getInterpolation(v)
             }
             .animateToValue(1f, 0f)
 
@@ -1603,7 +1585,7 @@
     fun startIconFadeInOnGestureComplete() {
         iconFadeInOnGestureCompleteAnimator?.cancel()
         iconFadeInOnGestureCompleteAnimator =
-            ObjectAnimator.ofFloat(settledProgressGesture, MULTI_PROPERTY_VALUE, 1f).apply {
+            ObjectAnimator.ofFloat(this, SETTLED_PROGRESS_GESTURE, 1f).apply {
                 duration = FADE_IN_ICON_DURATION
                 interpolator = Interpolators.LINEAR
                 addListener(
@@ -1619,7 +1601,7 @@
 
     fun setIconVisibleForGesture(isVisible: Boolean) {
         iconFadeInOnGestureCompleteAnimator?.cancel()
-        settledProgressGesture.value = if (isVisible) 1f else 0f
+        settledProgressGesture = if (isVisible) 1f else 0f
     }
 
     /** Set a color tint on the snapshot and supporting views. */
@@ -1708,7 +1690,7 @@
             it.iconView.setVisibility(if (fullscreenProgress < 1) VISIBLE else INVISIBLE)
             it.overlay.setFullscreenProgress(fullscreenProgress)
         }
-        settledProgressFullscreen.value =
+        settledProgressFullscreen =
             SETTLED_PROGRESS_FAST_OUT_INTERPOLATOR.getInterpolation(1 - fullscreenProgress)
         updateFullscreenParams()
     }
@@ -1766,7 +1748,7 @@
         dismissScale = 1f
         translationZ = 0f
         setIconVisibleForGesture(true)
-        settledProgressDismiss.value = 1f
+        settledProgressDismiss = 1f
         setColorTint(0f, 0)
     }
 
@@ -1788,23 +1770,25 @@
 
     companion object {
         private const val TAG = "TaskView"
+
+        private enum class Alpha {
+            STABLE,
+            ATTACH,
+            SPLIT,
+        }
+
+        private enum class SettledProgress {
+            Fullscreen,
+            Gesture,
+            Dismiss,
+        }
+
         const val FLAG_UPDATE_ICON = 1
         const val FLAG_UPDATE_THUMBNAIL = FLAG_UPDATE_ICON shl 1
         const val FLAG_UPDATE_CORNER_RADIUS = FLAG_UPDATE_THUMBNAIL shl 1
         const val FLAG_UPDATE_ALL =
             (FLAG_UPDATE_ICON or FLAG_UPDATE_THUMBNAIL or FLAG_UPDATE_CORNER_RADIUS)
 
-        const val SETTLED_PROGRESS_INDEX_FULLSCREEN = 0
-        const val SETTLED_PROGRESS_INDEX_GESTURE = 1
-        const val SETTLED_PROGRESS_INDEX_DISMISS = 2
-        const val SETTLED_PROGRESS_INDEX_COUNT = 3
-
-        private const val ALPHA_INDEX_STABLE = 0
-        private const val ALPHA_INDEX_ATTACH = 1
-        private const val ALPHA_INDEX_SPLIT = 2
-
-        private const val NUM_ALPHA_CHANNELS = 3
-
         /** The maximum amount that a task view can be scrimmed, dimmed or tinted. */
         const val MAX_PAGE_SCRIM_ALPHA = 0.4f
         const val FADE_IN_ICON_DURATION: Long = 120
@@ -1821,104 +1805,45 @@
         private val SYSTEM_GESTURE_EXCLUSION_RECT = listOf(Rect())
 
         private val SETTLED_PROGRESS: FloatProperty<TaskView> =
-            object : FloatProperty<TaskView>("settleTransition") {
-                override fun setValue(taskView: TaskView, v: Float) {
-                    taskView.settledProgress = v
-                }
+            KFloatProperty(TaskView::settledProgress)
 
-                override fun get(taskView: TaskView) = taskView.settledProgress
-            }
+        private val SETTLED_PROGRESS_GESTURE: FloatProperty<TaskView> =
+            KFloatProperty(TaskView::settledProgressGesture)
+
+        private val SETTLED_PROGRESS_DISMISS: FloatProperty<TaskView> =
+            KFloatProperty(TaskView::settledProgressDismiss)
 
         private val SPLIT_SELECT_TRANSLATION_X: FloatProperty<TaskView> =
-            object : FloatProperty<TaskView>("splitSelectTranslationX") {
-                override fun setValue(taskView: TaskView, v: Float) {
-                    taskView.splitSelectTranslationX = v
-                }
-
-                override fun get(taskView: TaskView) = taskView.splitSelectTranslationX
-            }
+            KFloatProperty(TaskView::splitSelectTranslationX)
 
         private val SPLIT_SELECT_TRANSLATION_Y: FloatProperty<TaskView> =
-            object : FloatProperty<TaskView>("splitSelectTranslationY") {
-                override fun setValue(taskView: TaskView, v: Float) {
-                    taskView.splitSelectTranslationY = v
-                }
-
-                override fun get(taskView: TaskView) = taskView.splitSelectTranslationY
-            }
+            KFloatProperty(TaskView::splitSelectTranslationY)
 
         private val DISMISS_TRANSLATION_X: FloatProperty<TaskView> =
-            object : FloatProperty<TaskView>("dismissTranslationX") {
-                override fun setValue(taskView: TaskView, v: Float) {
-                    taskView.dismissTranslationX = v
-                }
-
-                override fun get(taskView: TaskView) = taskView.dismissTranslationX
-            }
+            KFloatProperty(TaskView::dismissTranslationX)
 
         private val DISMISS_TRANSLATION_Y: FloatProperty<TaskView> =
-            object : FloatProperty<TaskView>("dismissTranslationY") {
-                override fun setValue(taskView: TaskView, v: Float) {
-                    taskView.dismissTranslationY = v
-                }
-
-                override fun get(taskView: TaskView) = taskView.dismissTranslationY
-            }
+            KFloatProperty(TaskView::dismissTranslationY)
 
         private val TASK_OFFSET_TRANSLATION_X: FloatProperty<TaskView> =
-            object : FloatProperty<TaskView>("taskOffsetTranslationX") {
-                override fun setValue(taskView: TaskView, v: Float) {
-                    taskView.taskOffsetTranslationX = v
-                }
-
-                override fun get(taskView: TaskView) = taskView.taskOffsetTranslationX
-            }
+            KFloatProperty(TaskView::taskOffsetTranslationX)
 
         private val TASK_OFFSET_TRANSLATION_Y: FloatProperty<TaskView> =
-            object : FloatProperty<TaskView>("taskOffsetTranslationY") {
-                override fun setValue(taskView: TaskView, v: Float) {
-                    taskView.taskOffsetTranslationY = v
-                }
-
-                override fun get(taskView: TaskView) = taskView.taskOffsetTranslationY
-            }
+            KFloatProperty(TaskView::taskOffsetTranslationY)
 
         private val TASK_RESISTANCE_TRANSLATION_X: FloatProperty<TaskView> =
-            object : FloatProperty<TaskView>("taskResistanceTranslationX") {
-                override fun setValue(taskView: TaskView, v: Float) {
-                    taskView.taskResistanceTranslationX = v
-                }
-
-                override fun get(taskView: TaskView) = taskView.taskResistanceTranslationX
-            }
+            KFloatProperty(TaskView::taskResistanceTranslationX)
 
         private val TASK_RESISTANCE_TRANSLATION_Y: FloatProperty<TaskView> =
-            object : FloatProperty<TaskView>("taskResistanceTranslationY") {
-                override fun setValue(taskView: TaskView, v: Float) {
-                    taskView.taskResistanceTranslationY = v
-                }
-
-                override fun get(taskView: TaskView) = taskView.taskResistanceTranslationY
-            }
+            KFloatProperty(TaskView::taskResistanceTranslationY)
 
         @JvmField
         val GRID_END_TRANSLATION_X: FloatProperty<TaskView> =
-            object : FloatProperty<TaskView>("gridEndTranslationX") {
-                override fun setValue(taskView: TaskView, v: Float) {
-                    taskView.gridEndTranslationX = v
-                }
-
-                override fun get(taskView: TaskView) = taskView.gridEndTranslationX
-            }
+            KFloatProperty(TaskView::gridEndTranslationX)
 
         @JvmField
-        val DISMISS_SCALE: FloatProperty<TaskView> =
-            object : FloatProperty<TaskView>("dismissScale") {
-                override fun setValue(taskView: TaskView, v: Float) {
-                    taskView.dismissScale = v
-                }
+        val DISMISS_SCALE: FloatProperty<TaskView> = KFloatProperty(TaskView::dismissScale)
 
-                override fun get(taskView: TaskView) = taskView.dismissScale
-            }
+        @JvmField val SPLIT_ALPHA: FloatProperty<TaskView> = KFloatProperty(TaskView::splitAlpha)
     }
 }
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java
index 06a939a..91f9e53 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/AppEventProducerTest.java
@@ -41,7 +41,7 @@
 import com.android.launcher3.model.data.AppInfo;
 import com.android.launcher3.pm.UserCache;
 import com.android.launcher3.util.AllModulesForTest;
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
+import com.android.launcher3.util.SandboxContext;
 import com.android.launcher3.util.UserIconInfo;
 import com.android.systemui.shared.system.SysUiStatsLog;
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt
index adfbca5..8d20ba8 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/model/data/TaskViewItemInfoTest.kt
@@ -28,7 +28,7 @@
 import com.android.launcher3.model.data.TaskViewItemInfo.Companion.createTaskViewAtom
 import com.android.launcher3.pm.UserCache
 import com.android.launcher3.util.AllModulesForTest
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
+import com.android.launcher3.util.SandboxContext
 import com.android.launcher3.util.SplitConfigurationOptions
 import com.android.launcher3.util.TransformingTouchDelegate
 import com.android.launcher3.util.UserIconInfo
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
index 3cf912c..f225807 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRule.kt
@@ -20,7 +20,6 @@
 import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode
 import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
 import com.android.launcher3.util.DisplayController
-import com.android.launcher3.util.MainThreadInitializedObject
 import com.android.launcher3.util.NavigationMode
 import org.junit.rules.TestRule
 import org.junit.runner.Description
@@ -31,8 +30,8 @@
 /**
  * Allows tests to specify which Taskbar [Mode] to run under.
  *
- * [context] should match the test's target context, so that [MainThreadInitializedObject] instances
- * are properly sandboxed.
+ * [context] should match the test's target context, so that Dagger singleton instances are properly
+ * sandboxed.
  *
  * Annotate tests with [TaskbarMode] to set a mode. If the annotation is omitted for any tests, this
  * rule is a no-op.
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
index e6806b7..6d53e8e 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContext.kt
@@ -30,7 +30,6 @@
 import com.android.launcher3.util.DaggerSingletonTracker
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.FakePrefsModule
-import com.android.launcher3.util.MainThreadInitializedObject.ObjectSandbox
 import com.android.launcher3.util.SandboxApplication
 import com.android.launcher3.util.SettingsCache
 import com.android.launcher3.util.SettingsCacheSandbox
@@ -58,7 +57,7 @@
     private val base: SandboxApplication,
     val virtualDisplay: VirtualDisplay,
     private val params: SandboxParams,
-) : ContextWrapper(base), ObjectSandbox by base, TestRule {
+) : ContextWrapper(base), TestRule {
 
     val settingsCacheSandbox = SettingsCacheSandbox()
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
index 99a34ea..b3056f5 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/inputconsumers/NavHandleLongPressInputConsumerTest.java
@@ -55,7 +55,7 @@
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.util.AllModulesForTest;
 import com.android.launcher3.util.DisplayController;
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
+import com.android.launcher3.util.SandboxContext;
 import com.android.quickstep.DeviceConfigWrapper;
 import com.android.quickstep.GestureState;
 import com.android.quickstep.InputConsumer;
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a02516a..56befd6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -293,6 +293,9 @@
     <!-- Description for a new page on homescreen[CHAR_LIMIT=none] -->
     <string name="workspace_new_page">New home screen page</string>
 
+    <string name="app_running_state_description">Active</string>
+    <string name="app_minimized_state_description">Minimized</string>
+
     <!-- Folder accessibility -->
     <!-- The format string for when a folder is opened, speaks the dimensions -->
     <string name="folder_opened">Folder opened, <xliff:g id="width" example="5">%1$d</xliff:g> by <xliff:g id="height" example="3">%2$d</xliff:g></string>
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index bd42b2b..730ad78 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -20,6 +20,9 @@
 import static android.graphics.fonts.FontStyle.FONT_WEIGHT_NORMAL;
 import static android.text.Layout.Alignment.ALIGN_NORMAL;
 
+import static com.android.launcher3.BubbleTextView.RunningAppState.RUNNING;
+import static com.android.launcher3.BubbleTextView.RunningAppState.NOT_RUNNING;
+import static com.android.launcher3.BubbleTextView.RunningAppState.MINIMIZED;
 import static com.android.launcher3.Flags.enableContrastTiles;
 import static com.android.launcher3.Flags.enableCursorHoverStates;
 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
@@ -208,6 +211,9 @@
     private final int mRunningAppIndicatorColor;
     private final int mMinimizedAppIndicatorColor;
 
+    private final String mMinimizedStateDescription;
+    private final String mRunningStateDescription;
+
     /**
      * Various options for the running state of an app.
      */
@@ -240,6 +246,9 @@
         super(context, attrs, defStyle);
         mActivity = ActivityContext.lookupContext(context);
         FastBitmapDrawable.setFlagHoverEnabled(enableCursorHoverStates());
+        mMinimizedStateDescription = getContext().getString(
+                R.string.app_minimized_state_description);
+        mRunningStateDescription = getContext().getString(R.string.app_running_state_description);
 
         TypedArray a = context.obtainStyledAttributes(attrs,
                 R.styleable.BubbleTextView, defStyle, 0);
@@ -432,6 +441,19 @@
         invalidate();
     }
 
+    /**
+     * Returns state description of this icon.
+     */
+    public String getIconStateDescription() {
+        if (mRunningAppState == MINIMIZED) {
+            return mMinimizedStateDescription;
+        } else if (mRunningAppState == RUNNING) {
+            return mRunningStateDescription;
+        } else {
+            return "";
+        }
+    }
+
     protected void setItemInfo(ItemInfoWithIcon itemInfo) {
         setTag(itemInfo);
     }
@@ -768,13 +790,13 @@
 
     /** Draws a line under the app icon if this is representing a running app in Desktop Mode. */
     protected void drawRunningAppIndicatorIfNecessary(Canvas canvas) {
-        if (mRunningAppState == RunningAppState.NOT_RUNNING || mDisplay != DISPLAY_TASKBAR) {
+        if (mRunningAppState == NOT_RUNNING || mDisplay != DISPLAY_TASKBAR) {
             return;
         }
         getIconBounds(mRunningAppIconBounds);
         Utilities.scaleRectAboutCenter(mRunningAppIconBounds, ICON_VISIBLE_AREA_FACTOR);
 
-        final boolean isMinimized = mRunningAppState == RunningAppState.MINIMIZED;
+        final boolean isMinimized = mRunningAppState == MINIMIZED;
         final int indicatorTop = mRunningAppIconBounds.bottom + mRunningAppIndicatorTopMargin;
         final int indicatorWidth =
                 isMinimized ? mMinimizedAppIndicatorWidth : mRunningAppIndicatorWidth;
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 84de1c2..728bc34 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -185,7 +185,6 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.debug.TestEventEmitter;
 import com.android.launcher3.debug.TestEventEmitter.TestEvent;
-import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragView;
@@ -237,6 +236,7 @@
 import com.android.launcher3.util.IntSet;
 import com.android.launcher3.util.ItemInflater;
 import com.android.launcher3.util.KeyboardShortcutsDelegate;
+import com.android.launcher3.util.LauncherBindableItemsContainer;
 import com.android.launcher3.util.LockedUserState;
 import com.android.launcher3.util.MSDLPlayerWrapper;
 import com.android.launcher3.util.PackageUserKey;
@@ -544,7 +544,7 @@
         mItemInflater = new ItemInflater<>(this, mAppWidgetHolder, getItemOnClickListener(),
                 mFocusHandler, new CellLayout(mWorkspace.getContext(), mWorkspace));
 
-        mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
+        mPopupDataProvider = new PopupDataProvider(this);
         mWidgetPickerDataProvider = new WidgetPickerDataProvider();
         PillColorProvider.getInstance(mWorkspace.getContext()).registerObserver();
 
@@ -1598,11 +1598,6 @@
 
     private final ScreenOnListener mScreenOnListener = this::onScreenOnChanged;
 
-    private void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
-        mWorkspace.updateNotificationDots(updatedDots);
-        mAppsView.getAppsStore().updateNotificationDots(updatedDots);
-    }
-
     @Override
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
@@ -3022,11 +3017,6 @@
         return mWidgetPickerDataProvider;
     }
 
-    @Override
-    public DotInfo getDotInfoForItem(ItemInfo info) {
-        return mPopupDataProvider.getDotInfoForItem(info);
-    }
-
     @NonNull
     public LauncherOverlayManager getOverlayManager() {
         return mOverlayManager;
@@ -3041,6 +3031,12 @@
         return mDragLayer;
     }
 
+    @NonNull
+    @Override
+    public LauncherBindableItemsContainer getContent() {
+        return mWorkspace;
+    }
+
     @Override
     public ActivityAllAppsContainerView<Launcher> getAppsView() {
         return mAppsView;
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 94ff441..5595828 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -83,7 +83,6 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.debug.TestEventEmitter;
 import com.android.launcher3.debug.TestEventEmitter.TestEvent;
-import com.android.launcher3.dot.FolderDotInfo;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
@@ -119,7 +118,6 @@
 import com.android.launcher3.util.LauncherBindableItemsContainer;
 import com.android.launcher3.util.MSDLPlayerWrapper;
 import com.android.launcher3.util.OverlayEdgeEffect;
-import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.util.WallpaperOffsetInterpolator;
@@ -3419,38 +3417,6 @@
         return null;
     }
 
-    public void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
-        final PackageUserKey packageUserKey = new PackageUserKey(null, null);
-        Predicate<ItemInfo> matcher = info -> !packageUserKey.updateFromItemInfo(info)
-                || updatedDots.test(packageUserKey);
-
-        ItemOperator op = (info, v) -> {
-            if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView) {
-                if (matcher.test(info)) {
-                    ((BubbleTextView) v).applyDotState(info, true /* animate */);
-                }
-            } else if (info instanceof FolderInfo && v instanceof FolderIcon) {
-                FolderInfo fi = (FolderInfo) info;
-                if (fi.anyMatch(matcher)) {
-                    FolderDotInfo folderDotInfo = new FolderDotInfo();
-                    for (ItemInfo si : fi.getContents()) {
-                        folderDotInfo.addDotInfo(mLauncher.getDotInfoForItem(si));
-                    }
-                    ((FolderIcon) v).setDotInfo(folderDotInfo);
-                }
-            }
-
-            // process all the shortcuts
-            return false;
-        };
-
-        mapOverItems(op);
-        Folder folder = Folder.getOpen(mLauncher);
-        if (folder != null) {
-            folder.iterateOverItems(op);
-        }
-    }
-
     /**
      * Remove workspace icons & widget information related to items in matcher.
      *
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index fb48a4d..0ce7249 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -65,6 +65,7 @@
 import android.widget.TextView;
 
 import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.core.content.res.ResourcesCompat;
@@ -261,7 +262,7 @@
     @Nullable
     private KeyboardInsetAnimationCallback mKeyboardInsetAnimationCallback;
 
-    private GradientDrawable mBackground;
+    private final @NonNull GradientDrawable mBackground;
 
     /**
      * Used to inflate the Workspace from XML.
@@ -283,6 +284,10 @@
         // click).
         setFocusableInTouchMode(true);
 
+        mBackground = (GradientDrawable) Objects.requireNonNull(
+                ResourcesCompat.getDrawable(getResources(),
+                        R.drawable.round_rect_folder, getContext().getTheme()));
+        mBackground.setCallback(this);
     }
 
     @Override
@@ -296,9 +301,6 @@
         final DeviceProfile dp = mActivityContext.getDeviceProfile();
         final int paddingLeftRight = dp.folderContentPaddingLeftRight;
 
-        mBackground = (GradientDrawable) ResourcesCompat.getDrawable(getResources(),
-                R.drawable.round_rect_folder, getContext().getTheme());
-
         mContent = findViewById(R.id.folder_content);
         mContent.setPadding(paddingLeftRight, dp.folderContentPaddingTop, paddingLeftRight, 0);
         mContent.setFolder(this);
@@ -345,6 +347,11 @@
         return true;
     }
 
+    @Override
+    protected boolean verifyDrawable(@NonNull Drawable who) {
+        return super.verifyDrawable(who) || (who == mBackground);
+    }
+
     void callBeginDragShared(View v, DragOptions options) {
         mLauncherDelegate.beginDragShared(v, this, options);
     }
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index 740b87b..3836f7d 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -94,7 +94,7 @@
 import com.android.launcher3.util.DisplayController;
 import com.android.launcher3.util.IntArray;
 import com.android.launcher3.util.IntSet;
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
+import com.android.launcher3.util.SandboxContext;
 import com.android.launcher3.util.WindowBounds;
 import com.android.launcher3.util.window.WindowManagerProxy;
 import com.android.launcher3.views.ActivityContext;
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index 8a5e388..318b3ce 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -23,20 +23,27 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.allapps.ActivityAllAppsContainerView;
 import com.android.launcher3.dot.DotInfo;
+import com.android.launcher3.dot.FolderDotInfo;
+import com.android.launcher3.folder.Folder;
+import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.notification.NotificationKeyData;
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.ShortcutUtil;
+import com.android.launcher3.views.ActivityContext;
 
 import java.io.PrintWriter;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Consumer;
 import java.util.function.Predicate;
 
 /**
@@ -47,19 +54,49 @@
     private static final boolean LOGD = false;
     private static final String TAG = "PopupDataProvider";
 
-    private final Consumer<Predicate<PackageUserKey>> mNotificationDotsChangeListener;
+    private final ActivityContext mContext;
+
+    /** Maps packages to their DotInfo's . */
+    private final Map<PackageUserKey, DotInfo> mPackageUserToDotInfos = new HashMap<>();
 
     /** Maps launcher activity components to a count of how many shortcuts they have. */
     private HashMap<ComponentKey, Integer> mDeepShortcutMap = new HashMap<>();
-    /** Maps packages to their DotInfo's . */
-    private Map<PackageUserKey, DotInfo> mPackageUserToDotInfos = new HashMap<>();
 
-    public PopupDataProvider(Consumer<Predicate<PackageUserKey>> notificationDotsChangeListener) {
-        mNotificationDotsChangeListener = notificationDotsChangeListener;
+    public PopupDataProvider(ActivityContext context) {
+        mContext = context;
     }
 
     private void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
-        mNotificationDotsChangeListener.accept(updatedDots);
+        final PackageUserKey packageUserKey = new PackageUserKey(null, null);
+        Predicate<ItemInfo> matcher = info -> !packageUserKey.updateFromItemInfo(info)
+                || updatedDots.test(packageUserKey);
+
+        ItemOperator op = (info, v) -> {
+            if (v instanceof BubbleTextView && info != null && matcher.test(info)) {
+                ((BubbleTextView) v).applyDotState(info, true /* animate */);
+            } else if (v instanceof FolderIcon icon
+                    && info instanceof FolderInfo fi && fi.anyMatch(matcher)) {
+                FolderDotInfo folderDotInfo = new FolderDotInfo();
+                for (ItemInfo si : fi.getContents()) {
+                    folderDotInfo.addDotInfo(getDotInfoForItem(si));
+                }
+                icon.setDotInfo(folderDotInfo);
+            }
+
+            // process all the shortcuts
+            return false;
+        };
+
+        mContext.getContent().mapOverItems(op);
+        Folder folder = Folder.getOpen(mContext);
+        if (folder != null) {
+            folder.iterateOverItems(op);
+        }
+
+        ActivityAllAppsContainerView<?> appsView = mContext.getAppsView();
+        if (appsView != null) {
+            appsView.getAppsStore().updateNotificationDots(updatedDots);
+        }
     }
 
     @Override
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index 2a21085..fd8b0e7 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -29,6 +29,7 @@
 import android.view.ViewAnimationUtils;
 import android.view.inputmethod.InputMethodManager;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.UiThread;
 
 import com.android.launcher3.AbstractFloatingView;
@@ -129,8 +130,7 @@
         }
 
         mDragController.addDragListener(this);
-        mPopupDataProvider = new PopupDataProvider(
-                mAppsView.getAppsStore()::updateNotificationDots);
+        mPopupDataProvider = new PopupDataProvider(this);
 
         mModel.addCallbacksAndLoad(this);
     }
@@ -293,6 +293,8 @@
         mStringCache = cache;
     }
 
+    @Override
+    @NonNull
     public PopupDataProvider getPopupDataProvider() {
         return mPopupDataProvider;
     }
diff --git a/src/com/android/launcher3/util/DaggerSingletonObject.java b/src/com/android/launcher3/util/DaggerSingletonObject.java
index a245761..a4bd30a 100644
--- a/src/com/android/launcher3/util/DaggerSingletonObject.java
+++ b/src/com/android/launcher3/util/DaggerSingletonObject.java
@@ -24,8 +24,7 @@
 import java.util.function.Function;
 
 /**
- * A class to provide DaggerSingleton objects in a traditional way for
- * {@link MainThreadInitializedObject}.
+ * A class to provide DaggerSingleton objects in a traditional way.
  * We should delete this class at the end and use @Inject to get dagger provided singletons.
  */
 
diff --git a/src/com/android/launcher3/util/DaggerSingletonTracker.java b/src/com/android/launcher3/util/DaggerSingletonTracker.java
index b7a88db..34b3760 100644
--- a/src/com/android/launcher3/util/DaggerSingletonTracker.java
+++ b/src/com/android/launcher3/util/DaggerSingletonTracker.java
@@ -45,7 +45,7 @@
      * Adds the SafeCloseable Singletons to the mLauncherAppSingletons list.
      * This helps to track the singletons and close them appropriately.
      * See {@link DaggerSingletonTracker#close()} and
-     * {@link MainThreadInitializedObject.SandboxContext#onDestroy()}
+     * {@link SandboxContext#onDestroy()}
      */
     public void addCloseable(SafeCloseable closeable) {
         MAIN_EXECUTOR.execute(() -> {
diff --git a/src/com/android/launcher3/util/KFloatProperty.kt b/src/com/android/launcher3/util/KFloatProperty.kt
new file mode 100644
index 0000000..5579241
--- /dev/null
+++ b/src/com/android/launcher3/util/KFloatProperty.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2025 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.util.FloatProperty
+import kotlin.reflect.KMutableProperty1
+
+/** Maps any Kotlin mutable property (var) to [FloatProperty]. */
+class KFloatProperty<T>(private val kProperty: KMutableProperty1<T, Float>) :
+    FloatProperty<T>(kProperty.name) {
+    override fun get(target: T) = kProperty.get(target)
+
+    override fun setValue(target: T, value: Float) {
+        kProperty.set(target, value)
+    }
+}
diff --git a/src/com/android/launcher3/util/LauncherBindableItemsContainer.java b/src/com/android/launcher3/util/LauncherBindableItemsContainer.java
deleted file mode 100644
index 20e3eaf..0000000
--- a/src/com/android/launcher3/util/LauncherBindableItemsContainer.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.util;
-
-import android.view.View;
-
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.apppairs.AppPairIcon;
-import com.android.launcher3.folder.Folder;
-import com.android.launcher3.folder.FolderIcon;
-import com.android.launcher3.model.data.AppPairInfo;
-import com.android.launcher3.model.data.FolderInfo;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.views.ActivityContext;
-import com.android.launcher3.widget.PendingAppWidgetHostView;
-
-import java.util.Set;
-
-/**
- * Interface representing a container which can bind Launcher items with some utility methods
- */
-public interface LauncherBindableItemsContainer {
-
-    /**
-     * Called to update workspace items as a result of
-     * {@link com.android.launcher3.model.BgDataModel.Callbacks#bindItemsUpdated(Set)}
-     */
-    default void updateContainerItems(Set<ItemInfo> updates, ActivityContext context) {
-        ItemOperator op = (info, v) -> {
-            if (v instanceof BubbleTextView shortcut
-                    && info instanceof WorkspaceItemInfo wii
-                    && updates.contains(info)) {
-                shortcut.applyFromWorkspaceItem(wii);
-            } else if (info instanceof FolderInfo && v instanceof FolderIcon folderIcon) {
-                folderIcon.updatePreviewItems(updates::contains);
-            } else if (info instanceof AppPairInfo && v instanceof AppPairIcon appPairIcon) {
-                appPairIcon.maybeRedrawForWorkspaceUpdate(updates::contains);
-            } else if (v instanceof PendingAppWidgetHostView pendingView
-                    && updates.contains(info)) {
-                pendingView.applyState();
-                pendingView.postProviderAvailabilityCheck();
-            }
-
-            // Iterate all items
-            return false;
-        };
-
-        mapOverItems(op);
-        Folder openFolder = Folder.getOpen(context);
-        if (openFolder != null) {
-            openFolder.iterateOverItems(op);
-        }
-    }
-
-    /**
-     * Map the operator over the shortcuts and widgets.
-     *
-     * @param op the operator to map over the shortcuts
-     */
-    void mapOverItems(ItemOperator op);
-
-    interface ItemOperator {
-        /**
-         * Process the next itemInfo, possibly with side-effect on the next item.
-         *
-         * @param info info for the shortcut
-         * @param view view for the shortcut
-         * @return true if done, false to continue the map
-         */
-        boolean evaluate(ItemInfo info, View view);
-    }
-}
diff --git a/src/com/android/launcher3/util/LauncherBindableItemsContainer.kt b/src/com/android/launcher3/util/LauncherBindableItemsContainer.kt
new file mode 100644
index 0000000..1661796
--- /dev/null
+++ b/src/com/android/launcher3/util/LauncherBindableItemsContainer.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.view.View
+import com.android.launcher3.BubbleTextView
+import com.android.launcher3.apppairs.AppPairIcon
+import com.android.launcher3.folder.Folder
+import com.android.launcher3.folder.FolderIcon
+import com.android.launcher3.model.data.AppPairInfo
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.views.ActivityContext
+import com.android.launcher3.widget.PendingAppWidgetHostView
+
+/** Interface representing a container which can bind Launcher items with some utility methods */
+interface LauncherBindableItemsContainer {
+
+    /**
+     * Called to update workspace items as a result of {@link
+     * com.android.launcher3.model.BgDataModel.Callbacks#bindItemsUpdated(Set)}
+     */
+    fun updateContainerItems(updates: Set<ItemInfo>, context: ActivityContext) {
+        val op = ItemOperator { info, v ->
+            when {
+                v is BubbleTextView && info is WorkspaceItemInfo && updates.contains(info) ->
+                    v.applyFromWorkspaceItem(info)
+                v is FolderIcon && info is FolderInfo -> v.updatePreviewItems(updates::contains)
+                v is AppPairIcon && info is AppPairInfo ->
+                    v.maybeRedrawForWorkspaceUpdate(updates::contains)
+                v is PendingAppWidgetHostView && updates.contains(info) -> {
+                    v.applyState()
+                    v.postProviderAvailabilityCheck()
+                }
+            }
+
+            // Iterate all items
+            false
+        }
+
+        mapOverItems(op)
+        Folder.getOpen(context)?.iterateOverItems(op)
+    }
+
+    /** Map the [op] over the shortcuts and widgets. */
+    fun mapOverItems(op: ItemOperator)
+
+    fun interface ItemOperator {
+
+        /**
+         * Process the next itemInfo, possibly with side-effect on the next item.
+         *
+         * @param info info for the shortcut
+         * @param view view for the shortcut
+         * @return true if done, false to continue the map
+         */
+        fun evaluate(info: ItemInfo?, view: View): Boolean
+    }
+}
diff --git a/src/com/android/launcher3/util/MainThreadInitializedObject.java b/src/com/android/launcher3/util/MainThreadInitializedObject.java
deleted file mode 100644
index 356a551..0000000
--- a/src/com/android/launcher3/util/MainThreadInitializedObject.java
+++ /dev/null
@@ -1,214 +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.launcher3.util;
-
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
-
-import android.content.Context;
-import android.os.Looper;
-import android.util.Log;
-
-import androidx.annotation.UiThread;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.launcher3.LauncherApplication;
-import com.android.launcher3.util.ResourceBasedOverride.Overrides;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
-import java.util.function.Consumer;
-
-/**
- * Utility class for defining singletons which are initiated on main thread.
- *
- * TODO(b/361850561): Do not delete MainThreadInitializedObject until we find a way to
- * unregister and understand how singleton objects are destroyed in dagger graph.
- */
-public class MainThreadInitializedObject<T extends SafeCloseable> {
-
-    private final ObjectProvider<T> mProvider;
-    private T mValue;
-
-    public MainThreadInitializedObject(ObjectProvider<T> provider) {
-        mProvider = provider;
-    }
-
-    public T get(Context context) {
-        Context app = context.getApplicationContext();
-        if (app instanceof ObjectSandbox sc) {
-            return sc.getObject(this);
-        }
-
-        if (mValue == null) {
-            if (Looper.myLooper() == Looper.getMainLooper()) {
-                mValue = TraceHelper.allowIpcs("main.thread.object", () -> mProvider.get(app));
-            } else {
-                try {
-                    return MAIN_EXECUTOR.submit(() -> get(context)).get();
-                } catch (InterruptedException|ExecutionException e) {
-                    throw new RuntimeException(e);
-                }
-            }
-        }
-        return mValue;
-    }
-
-    /**
-     * Executes the callback is the value is already created
-     * @return true if the callback was executed, false otherwise
-     */
-    public boolean executeIfCreated(Consumer<T> callback) {
-        T v = mValue;
-        if (v != null) {
-            callback.accept(v);
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    @VisibleForTesting
-    public void initializeForTesting(T value) {
-        mValue = value;
-    }
-
-    /**
-     * Initializes a provider based on resource overrides
-     */
-    public static <T extends ResourceBasedOverride & SafeCloseable> MainThreadInitializedObject<T>
-            forOverride(Class<T> clazz, int resourceId) {
-        return new MainThreadInitializedObject<>(c -> Overrides.getObject(clazz, c, resourceId));
-    }
-
-    public interface ObjectProvider<T> {
-
-        T get(Context context);
-    }
-
-    /** Sandbox for isolating {@link MainThreadInitializedObject} instances from Launcher. */
-    public interface ObjectSandbox {
-
-        /**
-         * Find a cached object from mObjectMap if we have already created one. If not, generate
-         * an object using the provider.
-         */
-        <T extends SafeCloseable> T getObject(MainThreadInitializedObject<T> object);
-
-
-        /**
-         * Put a value into cache, can be used to put mocked MainThreadInitializedObject
-         * instances.
-         */
-        <T extends SafeCloseable> void putObject(MainThreadInitializedObject<T> object, T value);
-
-        /**
-         * Returns whether this sandbox should cleanup all objects when its destroyed or leave it
-         * to the GC.
-         * These objects can have listeners attached to the system server and mey not be able to get
-         * GCed themselves when running on a device.
-         * Some environments like Robolectric tear down the whole system at the end of the test,
-         * so manual cleanup may not be required.
-         */
-        default boolean shouldCleanUpOnDestroy() {
-            return true;
-        }
-
-        @UiThread
-        default <T extends SafeCloseable> T createObject(MainThreadInitializedObject<T> object) {
-            return object.mProvider.get((Context) this);
-        }
-    }
-
-    /**
-     * Abstract Context which allows custom implementations for
-     * {@link MainThreadInitializedObject} providers
-     */
-    public static class SandboxContext extends LauncherApplication implements ObjectSandbox {
-
-        private static final String TAG = "SandboxContext";
-
-        private final Map<MainThreadInitializedObject, Object> mObjectMap = new HashMap<>();
-        private final ArrayList<SafeCloseable> mOrderedObjects = new ArrayList<>();
-
-        private final Object mDestroyLock = new Object();
-        private boolean mDestroyed = false;
-
-        public SandboxContext(Context base) {
-            attachBaseContext(base);
-        }
-
-        @Override
-        public Context getApplicationContext() {
-            return this;
-        }
-
-        @Override
-        public boolean shouldCleanUpOnDestroy() {
-            return (getBaseContext().getApplicationContext() instanceof ObjectSandbox os)
-                    ? os.shouldCleanUpOnDestroy() : true;
-        }
-
-        public void onDestroy() {
-            if (shouldCleanUpOnDestroy()) {
-                cleanUpObjects();
-            }
-        }
-
-        protected void cleanUpObjects() {
-            getAppComponent().getDaggerSingletonTracker().close();
-            synchronized (mDestroyLock) {
-                // Destroy in reverse order
-                for (int i = mOrderedObjects.size() - 1; i >= 0; i--) {
-                    mOrderedObjects.get(i).close();
-                }
-                mDestroyed = true;
-            }
-        }
-
-        @Override
-        public <T extends SafeCloseable> T getObject(MainThreadInitializedObject<T> object) {
-            synchronized (mDestroyLock) {
-                if (mDestroyed) {
-                    Log.e(TAG, "Static object access with a destroyed context");
-                }
-                T t = (T) mObjectMap.get(object);
-                if (t != null) {
-                    return t;
-                }
-                if (Looper.myLooper() == Looper.getMainLooper()) {
-                    t = createObject(object);
-                    mObjectMap.put(object, t);
-                    mOrderedObjects.add(t);
-                    return t;
-                }
-            }
-
-            try {
-                return MAIN_EXECUTOR.submit(() -> getObject(object)).get();
-            } catch (InterruptedException | ExecutionException e) {
-                throw new RuntimeException(e);
-            }
-        }
-
-        @Override
-        public <T extends SafeCloseable> void putObject(
-                MainThreadInitializedObject<T> object, T value) {
-            mObjectMap.put(object, value);
-        }
-    }
-}
diff --git a/src/com/android/launcher3/util/MultiPropertyDelegate.kt b/src/com/android/launcher3/util/MultiPropertyDelegate.kt
new file mode 100644
index 0000000..837a586
--- /dev/null
+++ b/src/com/android/launcher3/util/MultiPropertyDelegate.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2025 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 kotlin.reflect.KProperty
+
+/** Delegate Kotlin mutable property (var) to a property in [MultiPropertyFactory] */
+class MultiPropertyDelegate(private val property: MultiPropertyFactory<*>.MultiProperty) {
+    constructor(factory: MultiPropertyFactory<*>, enum: Enum<*>) : this(factory[enum.ordinal])
+
+    operator fun getValue(thisRef: Any?, kProperty: KProperty<*>): Float = property.value
+
+    operator fun setValue(thisRef: Any?, kProperty: KProperty<*>, value: Float) {
+        property.value = value
+    }
+}
diff --git a/src/com/android/launcher3/util/SandboxContext.kt b/src/com/android/launcher3/util/SandboxContext.kt
new file mode 100644
index 0000000..c6224e2
--- /dev/null
+++ b/src/com/android/launcher3/util/SandboxContext.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.util
+
+import android.content.Context
+import com.android.launcher3.LauncherApplication
+
+/** Abstract Context which allows custom implementations for dagger components. */
+open class SandboxContext(base: Context?) : LauncherApplication() {
+    init {
+        base?.let { attachBaseContext(it) }
+    }
+
+    override fun getApplicationContext(): Context {
+        return this
+    }
+
+    /**
+     * Returns whether this sandbox should cleanup all objects when its destroyed or leave it to the
+     * GC. These objects can have listeners attached to the system server and mey not be able to get
+     * GCed themselves when running on a device. Some environments like Robolectric tear down the
+     * whole system at the end of the test, so manual cleanup may not be required.
+     */
+    open fun shouldCleanUpOnDestroy(): Boolean {
+        return (getBaseContext().getApplicationContext() as? SandboxContext)
+            ?.shouldCleanUpOnDestroy() ?: true
+    }
+
+    fun onDestroy() {
+        if (shouldCleanUpOnDestroy()) {
+            cleanUpObjects()
+        }
+    }
+
+    open protected fun cleanUpObjects() {
+        appComponent.daggerSingletonTracker.close()
+    }
+
+    companion object {
+        private const val TAG = "SandboxContext"
+    }
+}
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index 5a04539..30af586 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -80,6 +80,7 @@
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.util.ActivityOptionsWrapper;
 import com.android.launcher3.util.ApplicationInfoWrapper;
+import com.android.launcher3.util.LauncherBindableItemsContainer;
 import com.android.launcher3.util.Preconditions;
 import com.android.launcher3.util.RunnableList;
 import com.android.launcher3.util.SplitConfigurationOptions;
@@ -101,10 +102,6 @@
         return false;
     }
 
-    default DotInfo getDotInfoForItem(ItemInfo info) {
-        return null;
-    }
-
     default AccessibilityDelegate getAccessibilityDelegate() {
         return null;
     }
@@ -194,6 +191,14 @@
     }
 
     /**
+     * Returns the primary content of this context
+     */
+    @NonNull
+    default LauncherBindableItemsContainer getContent() {
+        return op -> { };
+    }
+
+    /**
      * The all apps container, if it exists in this context.
      */
     default ActivityAllAppsContainerView<?> getAppsView() {
@@ -281,9 +286,13 @@
         return v -> false;
     }
 
-    @Nullable
+    @NonNull
     default PopupDataProvider getPopupDataProvider() {
-        return null;
+        return new PopupDataProvider(this);
+    }
+
+    default DotInfo getDotInfoForItem(ItemInfo info) {
+        return getPopupDataProvider().getDotInfoForItem(info);
     }
 
     /**
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index b07d807..7a44c6a 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -79,7 +79,8 @@
     private Runnable mAutoAdvanceRunnable;
 
     private long mDeferUpdatesUntilMillis = 0;
-    RemoteViews mLastRemoteViews;
+    private RemoteViews mLastRemoteViews;
+    private boolean mReapplyOnResumeUpdates = false;
 
     private boolean mTrackingWidgetUpdate = false;
 
@@ -138,11 +139,11 @@
                     TRACE_METHOD_NAME + getAppWidgetInfo().provider, getAppWidgetId());
             mTrackingWidgetUpdate = false;
         }
-        if (isDeferringUpdates()) {
-            mLastRemoteViews = remoteViews;
+        mLastRemoteViews = remoteViews;
+        mReapplyOnResumeUpdates = isDeferringUpdates();
+        if (mReapplyOnResumeUpdates) {
             return;
         }
-        mLastRemoteViews = null;
 
         super.updateAppWidget(remoteViews);
 
@@ -150,6 +151,18 @@
         checkIfAutoAdvance();
     }
 
+    @Override
+    public void onViewAdded(View child) {
+        super.onViewAdded(child);
+        mReapplyOnResumeUpdates |= isDeferringUpdates();
+    }
+
+    @Override
+    public void onViewRemoved(View child) {
+        super.onViewRemoved(child);
+        mReapplyOnResumeUpdates |= isDeferringUpdates();
+    }
+
     private boolean checkScrollableRecursively(ViewGroup viewGroup) {
         if (viewGroup instanceof AdapterView) {
             return true;
@@ -204,18 +217,16 @@
      * {@link #updateAppWidget} and apply any deferred updates.
      */
     public void endDeferringUpdates() {
-        RemoteViews remoteViews;
         mDeferUpdatesUntilMillis = 0;
-        remoteViews = mLastRemoteViews;
-
-        if (remoteViews != null) {
-            updateAppWidget(remoteViews);
+        if (mReapplyOnResumeUpdates) {
+            updateAppWidget(mLastRemoteViews);
         }
     }
 
+    @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            BaseDragLayer dragLayer = mActivityContext.getDragLayer();
+            BaseDragLayer<?> dragLayer = mActivityContext.getDragLayer();
             if (mIsScrollable) {
                 dragLayer.requestDisallowInterceptTouchEvent(true);
             }
@@ -225,6 +236,7 @@
         return mLongPressHelper.hasPerformedLongPress();
     }
 
+    @Override
     public boolean onTouchEvent(MotionEvent ev) {
         mLongPressHelper.onTouchEvent(ev);
         // We want to keep receiving though events to be able to cancel long press on ACTION_UP
diff --git a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index 3658989..ad6afcf 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -35,8 +35,8 @@
 import com.android.launcher3.util.AllModulesMinusWMProxy
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.FakePrefsModule
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext
 import com.android.launcher3.util.NavigationMode
+import com.android.launcher3.util.SandboxContext
 import com.android.launcher3.util.WindowBounds
 import com.android.launcher3.util.rule.TestStabilityRule
 import com.android.launcher3.util.rule.setFlags
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java
index cee5559..4458e8f 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -56,7 +56,6 @@
 import com.android.launcher3.model.BgDataModel.Callbacks;
 import com.android.launcher3.model.ModelDbController;
 import com.android.launcher3.testing.TestInformationProvider;
-import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt b/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt
index 547d05e..ceefb0d 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/ModelTestExtensions.kt
@@ -1,6 +1,7 @@
 package com.android.launcher3.util
 
 import android.content.ContentValues
+import android.os.Process
 import com.android.launcher3.Flags
 import com.android.launcher3.LauncherModel
 import com.android.launcher3.LauncherSettings.Favorites
@@ -66,7 +67,7 @@
         spanX: Int = 1,
         spanY: Int = 1,
         id: Int = 0,
-        profileId: Int = 0,
+        profileId: Int = Process.myUserHandle().identifier,
         tableName: String = Favorites.TABLE_NAME,
         appWidgetId: Int = -1,
         appWidgetSource: Int = -1,
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt
index 0da8891..2fa4cad 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplication.kt
@@ -27,7 +27,6 @@
 import android.view.Display
 import androidx.test.core.app.ApplicationProvider
 import com.android.launcher3.util.LauncherModelHelper.SandboxModelContext
-import com.android.launcher3.util.MainThreadInitializedObject.ObjectSandbox
 import org.junit.Rule
 import org.junit.rules.ExternalResource
 import org.junit.rules.TestRule
@@ -69,7 +68,7 @@
         // Defer to the true application to decide whether to clean up. For instance, we do not want
         // to cleanup under Robolectric.
         val app = ApplicationProvider.getApplicationContext<Context>()
-        return if (app is ObjectSandbox) app.shouldCleanUpOnDestroy() else true
+        return (app as? SandboxContext)?.shouldCleanUpOnDestroy() ?: true
     }
 
     override fun apply(statement: Statement, description: Description): Statement {
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplicationTest.kt b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplicationTest.kt
index d87a406..8a21cff 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplicationTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/util/SandboxApplicationTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.launcher3.util
 
-import android.content.Context
 import android.hardware.display.DisplayManager
 import android.view.Display
 import android.view.Display.DEFAULT_DISPLAY
@@ -63,16 +62,4 @@
             onDestroy()
         }
     }
-
-    @Test
-    fun testGetObject_objectCreatesDisplayContext_isSandboxed() {
-        class TestSingleton(context: Context) : SafeCloseable {
-            override fun close() = Unit
-
-            val displayContext = context.createDisplayContext(display)
-        }
-
-        val displayContext = MainThreadInitializedObject { TestSingleton(it) }[app].displayContext
-        assertThat(displayContext.applicationContext).isEqualTo(app)
-    }
 }
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java b/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java
index 393282f..8be1341 100644
--- a/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java
+++ b/tests/multivalentTests/src/com/android/launcher3/util/TestSandboxModelContextWrapper.java
@@ -18,10 +18,9 @@
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
-import static com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
-
 import android.content.ContextWrapper;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.test.platform.app.InstrumentationRegistry;
 
@@ -44,7 +43,7 @@
  * There are 2 constructors in this class. The base context can be {@link SandboxContext} or
  * Instrumentation target context.
  * Using {@link SandboxContext} as base context allows custom implementations for
- * MainThreadInitializedObject providers.
+ * providing objects in Dagger components.
  */
 
 public class TestSandboxModelContextWrapper extends ActivityContextWrapper implements
@@ -57,7 +56,7 @@
 
     protected ActivityAllAppsContainerView<ActivityContextWrapper> mAppsView;
 
-    private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {});
+    private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(this);
     private final WidgetPickerDataProvider mWidgetPickerDataProvider =
             new WidgetPickerDataProvider();
     protected final UserCache mUserCache;
@@ -80,7 +79,7 @@
         mAllAppsStore = mAppsView.getAppsStore();
     }
 
-    @Nullable
+    @NonNull
     @Override
     public PopupDataProvider getPopupDataProvider() {
         return mPopupDataProvider;