Merge "Integrating MagneticNotificationRowManager with NSSL controller." into main
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 90bda09..f4181e1 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1962,13 +1962,10 @@
 }
 
 flag {
-   name: "magnetic_notification_horizontal_swipe"
+   name: "magnetic_notification_swipes"
    namespace: "systemui"
    description: "Add support for magnetic behavior on horizontal notification swipes."
    bug: "390179908"
-   metadata {
-        purpose: PURPOSE_BUGFIX
-   }
 }
 
 flag {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 20cd6c7..4ce1380 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -165,6 +165,8 @@
     @Mock
     private SensitiveNotificationProtectionController mSensitiveNotificationProtectionController;
     @Mock private ExpandHelper mExpandHelper;
+    @Mock private MagneticNotificationRowManager mMagneticNotificationRowManager;
+    @Mock private NotificationSectionsManager mSectionsManager;
 
     @Captor
     private ArgumentCaptor<Runnable> mSensitiveStateListenerArgumentCaptor;
@@ -798,7 +800,9 @@
                 mActivityStarter,
                 new ResourcesSplitShadeStateController(),
                 mSensitiveNotificationProtectionController,
-                mWallpaperInteractor);
+                mWallpaperInteractor,
+                mMagneticNotificationRowManager,
+                mSectionsManager);
     }
 
     static class LogMatcher implements ArgumentMatcher<LogMaker> {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
index 789701f5..766ae73 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
@@ -405,7 +405,7 @@
         doNothing().when(mSwipeHelper).superSnapChild(mNotificationRow, 0, 0);
         mSwipeHelper.snapChild(mNotificationRow, 0, 0);
 
-        verify(mCallback, times(1)).onDragCancelled(mNotificationRow);
+        verify(mCallback, times(1)).onDragCancelledWithVelocity(mNotificationRow, 0);
         verify(mSwipeHelper, times(1)).superSnapChild(mNotificationRow, 0, 0);
         verify(mSwipeHelper, times(1)).handleMenuCoveredOrDismissed();
     }
@@ -416,7 +416,7 @@
         doNothing().when(mSwipeHelper).superSnapChild(mNotificationRow, 10, 0);
         mSwipeHelper.snapChild(mNotificationRow, 10, 0);
 
-        verify(mCallback, times(1)).onDragCancelled(mNotificationRow);
+        verify(mCallback, times(1)).onDragCancelledWithVelocity(mNotificationRow, 0);
         verify(mSwipeHelper, times(1)).superSnapChild(mNotificationRow, 10, 0);
         verify(mSwipeHelper, times(0)).handleMenuCoveredOrDismissed();
     }
@@ -426,7 +426,7 @@
         doNothing().when(mSwipeHelper).superSnapChild(mView, 10, 0);
         mSwipeHelper.snapChild(mView, 10, 0);
 
-        verify(mCallback).onDragCancelled(mView);
+        verify(mCallback).onDragCancelledWithVelocity(mView, 0);
         verify(mSwipeHelper, never()).superSnapChild(mView, 10, 0);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index 13cd2c5..19b2920 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -19,6 +19,7 @@
 import static androidx.dynamicanimation.animation.DynamicAnimation.TRANSLATION_X;
 import static androidx.dynamicanimation.animation.FloatPropertyCompat.createFloatPropertyCompat;
 
+import static com.android.systemui.Flags.magneticNotificationSwipes;
 import static com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS;
 import static com.android.systemui.statusbar.notification.NotificationUtils.logKey;
 
@@ -76,8 +77,7 @@
 
     protected final Handler mHandler;
 
-    private final SpringConfig mSnapBackSpringConfig =
-            new SpringConfig(SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+    private final SpringConfig mSnapBackSpringConfig;
 
     private final FlingAnimationUtils mFlingAnimationUtils;
     private float mPagingTouchSlop;
@@ -153,6 +153,12 @@
                 R.bool.config_fadeDependingOnAmountSwiped);
         mFalsingManager = falsingManager;
         mFeatureFlags = featureFlags;
+        if (magneticNotificationSwipes()) {
+            mSnapBackSpringConfig = new SpringConfig(550f /*stiffness*/, 0.52f /*dampingRatio*/);
+        } else {
+            mSnapBackSpringConfig = new SpringConfig(
+                    SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+        }
         mFlingAnimationUtils = new FlingAnimationUtils(resources.getDisplayMetrics(),
                 getMaxEscapeAnimDuration() / 1000f);
     }
@@ -718,7 +724,7 @@
                         dismissChild(mTouchedView, velocity,
                                 !swipedFastEnough() /* useAccelerateInterpolator */);
                     } else {
-                        mCallback.onDragCancelled(mTouchedView);
+                        mCallback.onDragCancelledWithVelocity(mTouchedView, velocity);
                         snapChild(mTouchedView, 0 /* leftTarget */, velocity);
                     }
                     mTouchedView = null;
@@ -925,6 +931,15 @@
         void onDragCancelled(View v);
 
         /**
+         * A drag operation has been cancelled on a view with a final velocity.
+         * @param v View that was dragged.
+         * @param finalVelocity Final velocity of the drag.
+         */
+        default void onDragCancelledWithVelocity(View v, float finalVelocity) {
+            onDragCancelled(v);
+        }
+
+        /**
          * Called when the child is long pressed and available to start drag and drop.
          *
          * @param v the view that was long pressed.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index 0346108..3dbf069 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -27,6 +27,7 @@
 import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor;
 import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.systemui.CoreStartable;
+import com.android.systemui.Flags;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Application;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -84,6 +85,8 @@
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback;
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule;
+import com.android.systemui.statusbar.notification.stack.MagneticNotificationRowManager;
+import com.android.systemui.statusbar.notification.stack.MagneticNotificationRowManagerImpl;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
 import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
@@ -322,4 +325,19 @@
             return (entry, recoveredBuilder) -> null;
         }
     }
+
+    /**
+     * Provide an implementation of {@link MagneticNotificationRowManager} based on its flag.
+     */
+    @Provides
+    @SysUISingleton
+    static MagneticNotificationRowManager provideMagneticNotificationRowManager(
+            Provider<MagneticNotificationRowManagerImpl> implProvider
+    ) {
+        if (Flags.magneticNotificationSwipes()) {
+            return implProvider.get();
+        } else {
+            return MagneticNotificationRowManager.getEmpty();
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index ba6f8f4..bd13dcd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -20,8 +20,8 @@
 import static android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY;
 import static android.service.notification.NotificationListenerService.REASON_CANCEL;
 
-import static com.android.systemui.flags.Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE;
 import static com.android.systemui.Flags.notificationsPinnedHunInShade;
+import static com.android.systemui.flags.Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE;
 import static com.android.systemui.statusbar.notification.collection.NotificationEntry.DismissState.PARENT_DISMISSED;
 import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP;
 import static com.android.systemui.statusbar.policy.RemoteInputView.FOCUS_ANIMATION_MIN_SCALE;
@@ -142,7 +142,6 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 import java.util.function.BooleanSupplier;
 import java.util.function.Consumer;
@@ -366,6 +365,7 @@
             this, FloatPropertyCompat.createFloatPropertyCompat(TRANSLATE_CONTENT));
 
     private final MagneticRowListener mMagneticRowListener = new MagneticRowListener() {
+
         @Override
         public void setMagneticTranslation(float translation) {
             if (mMagneticAnimator.isRunning()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManager.kt
index 345d59a..02336e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManager.kt
@@ -96,4 +96,35 @@
      * [onMagneticInteractionEnd] will not be called from the lifecycle of the user gesture.
      */
     fun reset()
+
+    companion object {
+        /** Detaching threshold in dp */
+        const val MAGNETIC_DETACH_THRESHOLD_DP = 56
+
+        /* An empty implementation of a manager */
+        @JvmStatic
+        val Empty: MagneticNotificationRowManager
+            get() =
+                object : MagneticNotificationRowManager {
+                    override fun setSwipeThresholdPx(thresholdPx: Float) {}
+
+                    override fun setMagneticAndRoundableTargets(
+                        swipingRow: ExpandableNotificationRow,
+                        stackScrollLayout: NotificationStackScrollLayout,
+                        sectionsManager: NotificationSectionsManager,
+                    ) {}
+
+                    override fun setMagneticRowTranslation(
+                        row: ExpandableNotificationRow,
+                        translation: Float,
+                    ): Boolean = false
+
+                    override fun onMagneticInteractionEnd(
+                        row: ExpandableNotificationRow,
+                        velocity: Float?,
+                    ) {}
+
+                    override fun reset() {}
+                }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index bf24c87..04862c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -24,6 +24,7 @@
 import static com.android.app.tracing.TrackGroupUtils.trackGroup;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_SHADE_CLEAR_ALL;
+import static com.android.systemui.Flags.magneticNotificationSwipes;
 import static com.android.systemui.Flags.notificationOverExpansionClippingFix;
 import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_SILENT;
 import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_SWIPE;
@@ -5787,17 +5788,21 @@
                 getChildrenWithBackground()
         );
 
-        RoundableTargets targets = mController.getNotificationTargetsHelper().findRoundableTargets(
-                (ExpandableNotificationRow) viewSwiped,
-                this,
-                mSectionsManager
-        );
+        if (!magneticNotificationSwipes()) {
+            RoundableTargets targets = mController
+                    .getNotificationTargetsHelper()
+                    .findRoundableTargets(
+                            (ExpandableNotificationRow) viewSwiped,
+                            this,
+                            mSectionsManager);
 
-        mController.getNotificationRoundnessManager()
-                .setViewsAffectedBySwipe(
-                        targets.getBefore(),
-                        targets.getSwiped(),
-                        targets.getAfter());
+            mController.getNotificationRoundnessManager()
+                    .setViewsAffectedBySwipe(
+                            targets.getBefore(),
+                            targets.getSwiped(),
+                            targets.getAfter());
+
+        }
 
         updateFirstAndLastBackgroundViews();
         requestDisallowInterceptTouchEvent(true);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 8eaef36..8048245 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -188,6 +188,8 @@
     private final NotificationStackSizeCalculator mNotificationStackSizeCalculator;
     private final StackStateLogger mStackStateLogger;
     private final NotificationStackScrollLogger mLogger;
+    private final MagneticNotificationRowManager mMagneticNotificationRowManager;
+    private final NotificationSectionsManager mSectionsManager;
 
     private final GroupExpansionManager mGroupExpansionManager;
     private NotificationStackScrollLayout mView;
@@ -465,6 +467,28 @@
                 }
 
                 @Override
+                public void onDensityScaleChange(float density) {
+                    mMagneticNotificationRowManager.setSwipeThresholdPx(
+                            density * MagneticNotificationRowManager.MAGNETIC_DETACH_THRESHOLD_DP
+                    );
+                }
+
+                @Override
+                public boolean handleSwipeableViewTranslation(SwipeableView view, float translate) {
+                    if (view instanceof ExpandableNotificationRow row) {
+                        return mMagneticNotificationRowManager
+                                .setMagneticRowTranslation(row, translate);
+                    } else {
+                        return false;
+                    }
+                }
+
+                @Override
+                public void resetMagneticStates() {
+                    mMagneticNotificationRowManager.reset();
+                }
+
+                @Override
                 public void onSnooze(StatusBarNotification sbn,
                         NotificationSwipeActionHelper.SnoozeOption snoozeOption) {
                     mNotificationsController.setNotificationSnoozed(sbn, snoozeOption);
@@ -479,6 +503,14 @@
                 public void onDragCancelled(View v) {
                 }
 
+                @Override
+                public void onDragCancelledWithVelocity(View v, float finalVelocity) {
+                    if (v instanceof ExpandableNotificationRow row) {
+                        mMagneticNotificationRowManager.onMagneticInteractionEnd(
+                                row, finalVelocity);
+                    }
+                }
+
                 /**
                  * Handles cleanup after the given {@code view} has been fully swiped out (including
                  * re-invoking dismiss logic in case the notification has not made its way out yet).
@@ -506,6 +538,10 @@
                  */
 
                 public void handleChildViewDismissed(View view) {
+                    if (view instanceof ExpandableNotificationRow row) {
+                        mMagneticNotificationRowManager.onMagneticInteractionEnd(
+                                row, null /* velocity */);
+                    }
                     // The View needs to clean up the Swipe states, e.g. roundness.
                     mView.onSwipeEnd();
                     if (mView.getClearAllInProgress()) {
@@ -577,6 +613,10 @@
 
                 @Override
                 public void onBeginDrag(View v) {
+                    if (v instanceof ExpandableNotificationRow row) {
+                        mMagneticNotificationRowManager.setMagneticAndRoundableTargets(
+                                row, mView, mSectionsManager);
+                    }
                     mView.onSwipeBegin(v);
                 }
 
@@ -691,7 +731,9 @@
             ActivityStarter activityStarter,
             SplitShadeStateController splitShadeStateController,
             SensitiveNotificationProtectionController sensitiveNotificationProtectionController,
-            WallpaperInteractor wallpaperInteractor) {
+            WallpaperInteractor wallpaperInteractor,
+            MagneticNotificationRowManager magneticNotificationRowManager,
+            NotificationSectionsManager sectionsManager) {
         mView = view;
         mViewBinder = viewBinder;
         mStackStateLogger = stackLogger;
@@ -742,6 +784,8 @@
         mSensitiveNotificationProtectionController = sensitiveNotificationProtectionController;
         mWallpaperInteractor = wallpaperInteractor;
         mView.passSplitShadeStateController(splitShadeStateController);
+        mMagneticNotificationRowManager = magneticNotificationRowManager;
+        mSectionsManager = sectionsManager;
         if (SceneContainerFlag.isEnabled()) {
             mWakeUpCoordinator.setStackScroller(this);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
index 5045744..d476d48 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
@@ -34,7 +34,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.systemui.SwipeHelper;
-import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.plugins.FalsingManager;
@@ -363,7 +362,7 @@
             superSnapChild(animView, targetLeft, velocity);
         }
 
-        mCallback.onDragCancelled(animView);
+        mCallback.onDragCancelledWithVelocity(animView, velocity);
         if (targetLeft == 0) {
             handleMenuCoveredOrDismissed();
         }
@@ -404,7 +403,11 @@
     @Override
     public void setTranslation(View v, float translate) {
         if (v instanceof SwipeableView) {
-            ((SwipeableView) v).setTranslation(translate);
+            boolean setTranslationHandled =
+                    mCallback.handleSwipeableViewTranslation((SwipeableView) v, translate);
+            if (!setTranslationHandled) {
+                ((SwipeableView) v).setTranslation(translate);
+            }
         }
     }
 
@@ -529,6 +532,18 @@
         mPulsing = pulsing;
     }
 
+    @Override
+    public void setDensityScale(float densityScale) {
+        super.setDensityScale(densityScale);
+        mCallback.onDensityScaleChange(densityScale);
+    }
+
+    @Override
+    public void resetTouchState() {
+        super.resetTouchState();
+        mCallback.resetMagneticStates();
+    }
+
     public interface NotificationCallback extends SwipeHelper.Callback{
         /**
          * @return if the view should be dismissed as soon as the touch is released, otherwise its
@@ -548,6 +563,13 @@
          * @param animView the view to ask about
          */
         float getTotalTranslationLength(View animView);
+
+        void onDensityScaleChange(float density);
+
+        boolean handleSwipeableViewTranslation(SwipeableView view, float translate);
+
+        // Reset any ongoing magnetic interactions
+        void resetMagneticStates();
     }
 
     static class Builder {