Merge changes from topic "predback-qs-dialog" into tm-qpr-dev

* changes:
  Add Predictive Back animation from Dialog to QS tile
  Add a flag for predictive back animation in QS Dialog in Flags.kt. Add an AnimationFeatureFlags interface to access flags from AnimationLib module
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt
new file mode 100644
index 0000000..1c9dabb
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/AnimationFeatureFlags.kt
@@ -0,0 +1,6 @@
+package com.android.systemui.animation
+
+interface AnimationFeatureFlags {
+    val isPredictiveBackQsDialogAnim: Boolean
+        get() = false
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
index a3ed085..e91a671 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt
@@ -33,8 +33,13 @@
 import android.view.WindowManager
 import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
 import android.widget.FrameLayout
+import android.window.OnBackInvokedDispatcher
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.jank.InteractionJankMonitor.CujType
+import com.android.systemui.animation.back.BackAnimationSpec
+import com.android.systemui.animation.back.applyTo
+import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi
+import com.android.systemui.animation.back.onBackAnimationCallbackFrom
 import kotlin.math.roundToInt
 
 private const val TAG = "DialogLaunchAnimator"
@@ -55,8 +60,9 @@
 constructor(
     private val callback: Callback,
     private val interactionJankMonitor: InteractionJankMonitor,
+    private val featureFlags: AnimationFeatureFlags,
     private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
-    private val isForTesting: Boolean = false
+    private val isForTesting: Boolean = false,
 ) {
     private companion object {
         private val TIMINGS = ActivityLaunchAnimator.TIMINGS
@@ -273,15 +279,16 @@
 
         val animatedDialog =
             AnimatedDialog(
-                launchAnimator,
-                callback,
-                interactionJankMonitor,
-                controller,
+                launchAnimator = launchAnimator,
+                callback = callback,
+                interactionJankMonitor = interactionJankMonitor,
+                controller = controller,
                 onDialogDismissed = { openedDialogs.remove(it) },
                 dialog = dialog,
-                animateBackgroundBoundsChange,
-                animatedParent,
-                isForTesting,
+                animateBackgroundBoundsChange = animateBackgroundBoundsChange,
+                parentAnimatedDialog = animatedParent,
+                forceDisableSynchronization = isForTesting,
+                featureFlags = featureFlags,
             )
 
         openedDialogs.add(animatedDialog)
@@ -517,6 +524,7 @@
      * Whether synchronization should be disabled, which can be useful if we are running in a test.
      */
     private val forceDisableSynchronization: Boolean,
+    private val featureFlags: AnimationFeatureFlags,
 ) {
     /**
      * The DecorView of this dialog window.
@@ -778,12 +786,45 @@
         // the dialog.
         dialog.setDismissOverride(this::onDialogDismissed)
 
+        if (featureFlags.isPredictiveBackQsDialogAnim) {
+            // TODO(b/265923095) Improve animations for QS dialogs on configuration change
+            registerOnBackInvokedCallback(targetView = dialogContentWithBackground)
+        }
+
         // Show the dialog.
         dialog.show()
-
         moveSourceDrawingToDialog()
     }
 
+    private fun registerOnBackInvokedCallback(targetView: View) {
+        val metrics = targetView.resources.displayMetrics
+
+        val onBackAnimationCallback =
+            onBackAnimationCallbackFrom(
+                backAnimationSpec = BackAnimationSpec.floatingSystemSurfacesForSysUi(metrics),
+                displayMetrics = metrics, // TODO(b/265060720): We could remove this
+                onBackProgressed = { backTransformation -> backTransformation.applyTo(targetView) },
+                onBackInvoked = { dialog.dismiss() },
+            )
+
+        val dispatcher = dialog.onBackInvokedDispatcher
+        targetView.addOnAttachStateChangeListener(
+            object : View.OnAttachStateChangeListener {
+                override fun onViewAttachedToWindow(v: View) {
+                    dispatcher.registerOnBackInvokedCallback(
+                        OnBackInvokedDispatcher.PRIORITY_DEFAULT,
+                        onBackAnimationCallback
+                    )
+                }
+
+                override fun onViewDetachedFromWindow(v: View) {
+                    targetView.removeOnAttachStateChangeListener(this)
+                    dispatcher.unregisterOnBackInvokedCallback(onBackAnimationCallback)
+                }
+            }
+        )
+    }
+
     private fun moveSourceDrawingToDialog() {
         if (decorView.viewRootImpl == null) {
             // Make sure that we have access to the dialog view root to move the drawing to the
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpec.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpec.kt
new file mode 100644
index 0000000..f3d8b17
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpec.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation.back
+
+import android.util.DisplayMetrics
+import android.view.animation.Interpolator
+import android.window.BackEvent
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.util.dpToPx
+
+/** Used to convert [BackEvent] into a [BackTransformation]. */
+fun interface BackAnimationSpec {
+
+    /** Computes transformation based on a [backEvent] and sets it to [result]. */
+    fun getBackTransformation(
+        backEvent: BackEvent,
+        progressY: Float, // TODO(b/265060720): Remove progressY. Could be retrieved from backEvent
+        result: BackTransformation,
+    )
+
+    companion object
+}
+
+/** Create a [BackAnimationSpec] from [displayMetrics] and design specs. */
+fun BackAnimationSpec.Companion.createFloatingSurfaceAnimationSpec(
+    displayMetrics: DisplayMetrics,
+    maxMarginXdp: Float,
+    maxMarginYdp: Float,
+    minScale: Float,
+    translateXEasing: Interpolator = Interpolators.STANDARD_DECELERATE,
+    translateYEasing: Interpolator = Interpolators.LINEAR,
+    scaleEasing: Interpolator = Interpolators.STANDARD_DECELERATE,
+): BackAnimationSpec {
+    val screenWidthPx = displayMetrics.widthPixels
+    val screenHeightPx = displayMetrics.heightPixels
+
+    val maxMarginXPx = maxMarginXdp.dpToPx(displayMetrics)
+    val maxMarginYPx = maxMarginYdp.dpToPx(displayMetrics)
+    val maxTranslationXByScale = (screenWidthPx - screenWidthPx * minScale) / 2
+    val maxTranslationX = maxTranslationXByScale - maxMarginXPx
+    val maxTranslationYByScale = (screenHeightPx - screenHeightPx * minScale) / 2
+    val maxTranslationY = maxTranslationYByScale - maxMarginYPx
+    val minScaleReversed = 1f - minScale
+
+    return BackAnimationSpec { backEvent, progressY, result ->
+        val direction = if (backEvent.swipeEdge == BackEvent.EDGE_LEFT) 1 else -1
+        val progressX = backEvent.progress
+
+        val ratioTranslateX = translateXEasing.getInterpolation(progressX)
+        val ratioTranslateY = translateYEasing.getInterpolation(progressY)
+        val ratioScale = scaleEasing.getInterpolation(progressX)
+
+        result.apply {
+            translateX = ratioTranslateX * direction * maxTranslationX
+            translateY = ratioTranslateY * maxTranslationY
+            scale = 1f - (ratioScale * minScaleReversed)
+        }
+    }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
new file mode 100644
index 0000000..c6b7073
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation.back
+
+import android.util.DisplayMetrics
+
+/**
+ * SysUI transitions - Dismiss app (ST1) Return to launching surface or place of origin
+ * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-1-dismiss-app
+ */
+fun BackAnimationSpec.Companion.dismissAppForSysUi(
+    displayMetrics: DisplayMetrics,
+): BackAnimationSpec =
+    BackAnimationSpec.createFloatingSurfaceAnimationSpec(
+        displayMetrics = displayMetrics,
+        maxMarginXdp = 8f,
+        maxMarginYdp = 8f,
+        minScale = 0.8f,
+    )
+
+/**
+ * SysUI transitions - Cross task (ST2) Return to previous task/app, keeping the current one open
+ * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-2-cross-task
+ */
+fun BackAnimationSpec.Companion.crossTaskForSysUi(
+    displayMetrics: DisplayMetrics,
+): BackAnimationSpec =
+    BackAnimationSpec.createFloatingSurfaceAnimationSpec(
+        displayMetrics = displayMetrics,
+        maxMarginXdp = 8f,
+        maxMarginYdp = 8f,
+        minScale = 0.8f,
+    )
+
+/**
+ * SysUI transitions - Inner area dismiss (ST3) Dismiss non-detachable surface
+ * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-3-inner-area-dismiss
+ */
+fun BackAnimationSpec.Companion.innerAreaDismissForSysUi(
+    displayMetrics: DisplayMetrics,
+): BackAnimationSpec =
+    BackAnimationSpec.createFloatingSurfaceAnimationSpec(
+        displayMetrics = displayMetrics,
+        maxMarginXdp = 0f,
+        maxMarginYdp = 0f,
+        minScale = 0.9f,
+    )
+
+/**
+ * SysUI transitions - Floating system surfaces (ST4)
+ * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-4-floating-system-surfaces
+ */
+fun BackAnimationSpec.Companion.floatingSystemSurfacesForSysUi(
+    displayMetrics: DisplayMetrics,
+): BackAnimationSpec =
+    BackAnimationSpec.createFloatingSurfaceAnimationSpec(
+        displayMetrics = displayMetrics,
+        maxMarginXdp = 8f,
+        maxMarginYdp = 8f,
+        minScale = 0.8f,
+    )
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt
new file mode 100644
index 0000000..49d1fb4
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation.back
+
+import android.view.View
+
+/**
+ * This object that represents the transformation to apply to the target. The properties of this
+ * object are mutable for performance reasons (avoid recreating this object)
+ */
+data class BackTransformation(
+    var translateX: Float = Float.NaN,
+    var translateY: Float = Float.NaN,
+    var scale: Float = Float.NaN,
+)
+
+/** Apply the transformation to the [targetView] */
+fun BackTransformation.applyTo(targetView: View) {
+    if (translateX.isFinite()) targetView.translationX = translateX
+    if (translateY.isFinite()) targetView.translationY = translateY
+    if (scale.isFinite()) {
+        targetView.scaleX = scale
+        targetView.scaleY = scale
+    }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt
new file mode 100644
index 0000000..33d14b1
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtension.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation.back
+
+import android.util.DisplayMetrics
+import android.window.BackEvent
+import android.window.OnBackAnimationCallback
+
+/**
+ * Generates an [OnBackAnimationCallback] given a [backAnimationSpec]. [onBackProgressed] will be
+ * called on each update passing the current [BackTransformation].
+ *
+ * Optionally, you can specify [onBackStarted], [onBackInvoked], and [onBackCancelled] callbacks.
+ */
+fun onBackAnimationCallbackFrom(
+    backAnimationSpec: BackAnimationSpec,
+    displayMetrics: DisplayMetrics, // TODO(b/265060720): We could remove this
+    onBackProgressed: (BackTransformation) -> Unit,
+    onBackStarted: (BackEvent) -> Unit = {},
+    onBackInvoked: () -> Unit = {},
+    onBackCancelled: () -> Unit = {},
+): OnBackAnimationCallback {
+    return object : OnBackAnimationCallback {
+        private var initialY = 0f
+        private val lastTransformation = BackTransformation()
+
+        override fun onBackStarted(backEvent: BackEvent) {
+            initialY = backEvent.touchY
+            onBackStarted(backEvent)
+        }
+
+        override fun onBackProgressed(backEvent: BackEvent) {
+            val progressY = (backEvent.touchY - initialY) / displayMetrics.heightPixels
+
+            backAnimationSpec.getBackTransformation(
+                backEvent = backEvent,
+                progressY = progressY,
+                result = lastTransformation,
+            )
+
+            onBackProgressed(lastTransformation)
+        }
+
+        override fun onBackInvoked() {
+            onBackInvoked()
+        }
+
+        override fun onBackCancelled() {
+            onBackCancelled()
+        }
+    }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/util/Dimension.kt b/packages/SystemUI/animation/src/com/android/systemui/util/Dimension.kt
new file mode 100644
index 0000000..4bc9972
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/util/Dimension.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util
+
+import android.content.Context
+import android.content.res.Resources
+import android.util.DisplayMetrics
+import android.util.TypedValue
+
+/** Convert [this] number of dps to device pixels. */
+fun Number.dpToPx(context: Context): Float = dpToPx(resources = context.resources)
+
+/** Convert [this] number of dps to device pixels. */
+fun Number.dpToPx(resources: Resources): Float = dpToPx(displayMetrics = resources.displayMetrics)
+
+/** Convert [this] number of dps to device pixels. */
+fun Number.dpToPx(displayMetrics: DisplayMetrics): Float =
+    TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, toFloat(), displayMetrics)
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index b954331..0749bcf 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -65,8 +65,7 @@
     val FSI_ON_DND_UPDATE = unreleasedFlag(259130119, "fsi_on_dnd_update", teamfood = true)
 
     // TODO(b/265804648): Tracking Bug
-    @JvmField
-    val DISABLE_FSI = unreleasedFlag(265804648, "disable_fsi")
+    @JvmField val DISABLE_FSI = unreleasedFlag(265804648, "disable_fsi")
 
     // TODO(b/254512538): Tracking Bug
     val INSTANT_VOICE_REPLY = unreleasedFlag(111, "instant_voice_reply", teamfood = true)
@@ -483,6 +482,11 @@
     val WM_SHADE_ANIMATE_BACK_GESTURE =
         unreleasedFlag(1208, "persist.wm.debug.shade_animate_back_gesture", teamfood = true)
 
+    // TODO(b/265639042): Tracking Bug
+    @JvmField
+    val WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM =
+        unreleasedFlag(1209, "persist.wm.debug.predictive_back_qs_dialog_anim", teamfood = true)
+
     // 1300 - screenshots
     // TODO(b/254513155): Tracking Bug
     @JvmField
@@ -573,6 +577,5 @@
 
     // 2600 - keyboard shortcut
     // TODO(b/259352579): Tracking Bug
-    @JvmField
-    val SHORTCUT_LIST_SEARCH_LAYOUT = unreleasedFlag(2600, "shortcut_list_search_layout")
+    @JvmField val SHORTCUT_LIST_SEARCH_LAYOUT = unreleasedFlag(2600, "shortcut_list_search_layout")
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 9a65e34..098c617 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -25,12 +25,15 @@
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.animation.AnimationFeatureFlags;
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpHandler;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.media.controls.pipeline.MediaDataManager;
 import com.android.systemui.plugins.ActivityStarter;
@@ -281,7 +284,8 @@
     static DialogLaunchAnimator provideDialogLaunchAnimator(IDreamManager dreamManager,
             KeyguardStateController keyguardStateController,
             Lazy<AlternateBouncerInteractor> alternateBouncerInteractor,
-            InteractionJankMonitor interactionJankMonitor) {
+            InteractionJankMonitor interactionJankMonitor,
+            AnimationFeatureFlags animationFeatureFlags) {
         DialogLaunchAnimator.Callback callback = new DialogLaunchAnimator.Callback() {
             @Override
             public boolean isDreaming() {
@@ -303,6 +307,19 @@
                 return alternateBouncerInteractor.get().canShowAlternateBouncerForFingerprint();
             }
         };
-        return new DialogLaunchAnimator(callback, interactionJankMonitor);
+        return new DialogLaunchAnimator(callback, interactionJankMonitor, animationFeatureFlags);
+    }
+
+    /**
+     */
+    @Provides
+    @SysUISingleton
+    static AnimationFeatureFlags provideAnimationFeatureFlags(FeatureFlags featureFlags) {
+        return new AnimationFeatureFlags() {
+            @Override
+            public boolean isPredictiveBackQsDialogAnim() {
+                return featureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_QS_DIALOG_ANIM);
+            }
+        };
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
new file mode 100644
index 0000000..3bdbf97
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
@@ -0,0 +1,87 @@
+package com.android.systemui.animation.back
+
+import android.util.DisplayMetrics
+import android.window.BackEvent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+private data class BackInput(val progressX: Float, val progressY: Float, val edge: Int)
+
+@SmallTest
+@RunWith(JUnit4::class)
+class BackAnimationSpecTest : SysuiTestCase() {
+    private var displayMetrics =
+        DisplayMetrics().apply {
+            widthPixels = 100
+            heightPixels = 200
+            density = 3f
+        }
+
+    @Test
+    fun sysUi_floatingSystemSurfaces_animationValues() {
+        val maxX = 14.0f
+        val maxY = 4.0f
+        val minScale = 0.8f
+
+        val backAnimationSpec = BackAnimationSpec.floatingSystemSurfacesForSysUi(displayMetrics)
+
+        assertBackTransformation(
+            backAnimationSpec = backAnimationSpec,
+            backInput = BackInput(progressX = 0f, progressY = 0f, edge = BackEvent.EDGE_LEFT),
+            expected = BackTransformation(translateX = 0f, translateY = 0f, scale = 1f),
+        )
+        assertBackTransformation(
+            backAnimationSpec = backAnimationSpec,
+            backInput = BackInput(progressX = 1f, progressY = 0f, edge = BackEvent.EDGE_LEFT),
+            expected = BackTransformation(translateX = -maxX, translateY = 0f, scale = minScale),
+        )
+        assertBackTransformation(
+            backAnimationSpec = backAnimationSpec,
+            backInput = BackInput(progressX = 1f, progressY = 0f, edge = BackEvent.EDGE_RIGHT),
+            expected = BackTransformation(translateX = maxX, translateY = 0f, scale = minScale),
+        )
+        assertBackTransformation(
+            backAnimationSpec = backAnimationSpec,
+            backInput = BackInput(progressX = 1f, progressY = 1f, edge = BackEvent.EDGE_LEFT),
+            expected = BackTransformation(translateX = -maxX, translateY = -maxY, scale = minScale),
+        )
+        assertBackTransformation(
+            backAnimationSpec = backAnimationSpec,
+            backInput = BackInput(progressX = 0f, progressY = 1f, edge = BackEvent.EDGE_LEFT),
+            expected = BackTransformation(translateX = 0f, translateY = -maxY, scale = 1f),
+        )
+        assertBackTransformation(
+            backAnimationSpec = backAnimationSpec,
+            backInput = BackInput(progressX = 0f, progressY = -1f, edge = BackEvent.EDGE_LEFT),
+            expected = BackTransformation(translateX = 0f, translateY = maxY, scale = 1f),
+        )
+    }
+}
+
+private fun assertBackTransformation(
+    backAnimationSpec: BackAnimationSpec,
+    backInput: BackInput,
+    expected: BackTransformation,
+) {
+    val actual = BackTransformation()
+    backAnimationSpec.getBackTransformation(
+        backEvent =
+            BackEvent(
+                /* touchX = */ 0f,
+                /* touchY = */ 0f,
+                /* progress = */ backInput.progressX,
+                /* swipeEdge = */ backInput.edge,
+            ),
+        progressY = backInput.progressY,
+        result = actual
+    )
+
+    val tolerance = 0f
+    assertThat(actual.translateX).isWithin(tolerance).of(expected.translateX)
+    assertThat(actual.translateY).isWithin(tolerance).of(expected.translateY)
+    assertThat(actual.scale).isWithin(tolerance).of(expected.scale)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt
new file mode 100644
index 0000000..190b3d2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt
@@ -0,0 +1,80 @@
+package com.android.systemui.animation.back
+
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(JUnit4::class)
+class BackTransformationTest : SysuiTestCase() {
+    private val targetView: View = mock()
+
+    @Test
+    fun defaultValue_noTransformation() {
+        val transformation = BackTransformation()
+
+        assertThat(transformation.translateX).isNaN()
+        assertThat(transformation.translateY).isNaN()
+        assertThat(transformation.scale).isNaN()
+    }
+
+    @Test
+    fun applyTo_targetView_translateX_Y_Scale() {
+        val transformation = BackTransformation(translateX = 0f, translateY = 0f, scale = 1f)
+
+        transformation.applyTo(targetView = targetView)
+
+        verify(targetView).translationX = 0f
+        verify(targetView).translationY = 0f
+        verify(targetView).scaleX = 1f
+        verify(targetView).scaleY = 1f
+        verifyNoMoreInteractions(targetView)
+    }
+
+    @Test
+    fun applyTo_targetView_translateX() {
+        val transformation = BackTransformation(translateX = 1f)
+
+        transformation.applyTo(targetView = targetView)
+
+        verify(targetView).translationX = 1f
+        verifyNoMoreInteractions(targetView)
+    }
+
+    @Test
+    fun applyTo_targetView_translateY() {
+        val transformation = BackTransformation(translateY = 2f)
+
+        transformation.applyTo(targetView = targetView)
+
+        verify(targetView).translationY = 2f
+        verifyNoMoreInteractions(targetView)
+    }
+
+    @Test
+    fun applyTo_targetView_scale() {
+        val transformation = BackTransformation(scale = 3f)
+
+        transformation.applyTo(targetView = targetView)
+
+        verify(targetView).scaleX = 3f
+        verify(targetView).scaleY = 3f
+        verifyNoMoreInteractions(targetView)
+    }
+
+    @Test
+    fun applyTo_targetView_noTransformation() {
+        val transformation = BackTransformation()
+
+        transformation.applyTo(targetView = targetView)
+
+        verifyNoMoreInteractions(targetView)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt
new file mode 100644
index 0000000..921f9a8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt
@@ -0,0 +1,63 @@
+package com.android.systemui.animation.back
+
+import android.util.DisplayMetrics
+import android.window.BackEvent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.mock
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(JUnit4::class)
+class OnBackAnimationCallbackExtensionTest : SysuiTestCase() {
+    private val onBackProgress: (BackTransformation) -> Unit = mock()
+    private val onBackStart: (BackEvent) -> Unit = mock()
+    private val onBackInvoke: () -> Unit = mock()
+    private val onBackCancel: () -> Unit = mock()
+
+    private val displayMetrics =
+        DisplayMetrics().apply {
+            widthPixels = 100
+            heightPixels = 100
+            density = 1f
+        }
+
+    private val onBackAnimationCallback =
+        onBackAnimationCallbackFrom(
+            backAnimationSpec = BackAnimationSpec.floatingSystemSurfacesForSysUi(displayMetrics),
+            displayMetrics = displayMetrics,
+            onBackProgressed = onBackProgress,
+            onBackStarted = onBackStart,
+            onBackInvoked = onBackInvoke,
+            onBackCancelled = onBackCancel,
+        )
+
+    @Test
+    fun onBackProgressed_shouldInvoke_onBackProgress() {
+        val backEvent = BackEvent(0f, 0f, 0f, BackEvent.EDGE_LEFT)
+        onBackAnimationCallback.onBackStarted(backEvent)
+
+        onBackAnimationCallback.onBackProgressed(backEvent)
+
+        verify(onBackProgress).invoke(BackTransformation(0f, 0f, 1f))
+    }
+
+    @Test
+    fun onBackStarted_shouldInvoke_onBackStart() {
+        val backEvent = BackEvent(0f, 0f, 0f, BackEvent.EDGE_LEFT)
+
+        onBackAnimationCallback.onBackStarted(backEvent)
+
+        verify(onBackStart).invoke(backEvent)
+    }
+
+    @Test
+    fun onBackInvoked_shouldInvoke_onBackInvoke() {
+        onBackAnimationCallback.onBackInvoked()
+
+        verify(onBackInvoke).invoke()
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt
index 990db77..f723a9e5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogLaunchAnimator.kt
@@ -23,14 +23,20 @@
     isUnlocked: Boolean = true,
     isShowingAlternateAuthOnUnlock: Boolean = false,
     interactionJankMonitor: InteractionJankMonitor = mock(InteractionJankMonitor::class.java),
+    isPredictiveBackQsDialogAnim: Boolean = false,
 ): DialogLaunchAnimator {
     return DialogLaunchAnimator(
-        FakeCallback(
-            isUnlocked = isUnlocked,
-            isShowingAlternateAuthOnUnlock = isShowingAlternateAuthOnUnlock,
-        ),
-        interactionJankMonitor,
-        fakeLaunchAnimator(),
+        callback =
+            FakeCallback(
+                isUnlocked = isUnlocked,
+                isShowingAlternateAuthOnUnlock = isShowingAlternateAuthOnUnlock,
+            ),
+        interactionJankMonitor = interactionJankMonitor,
+        featureFlags =
+            object : AnimationFeatureFlags {
+                override val isPredictiveBackQsDialogAnim = isPredictiveBackQsDialogAnim
+            },
+        launchAnimator = fakeLaunchAnimator(),
         isForTesting = true,
     )
 }