[flexiglass] Unfold transition support in the lockscreen scene.

When a foldable is fully unfolded and then the user begins to fold it
up, there's a subtle animation that happens to elements across the
screen. Left-hand side elements move to the right and right-hand side
elements move to the left, seeming to gently float towards the fold
hinge.

This CL adds that for Flexiglass, only for the split lockscreen scene.

Test: added integration test for the new code that exposes the
unfoldTranslations in the view-model
Test: manually verified that gently folding up the device correctly
slides the elements of the split lockscreen scene into the center (both left-hand
side elements and the NSSL on the right)
Flag: ACONFIG com.android.systemui.scene_container DEVELOPMENT
Bug: 330483283

Change-Id: I56fd548c8cd46f33094de6058c2fa2830dcce005
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
index 28e92aa..e499c69 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -26,9 +26,11 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.unit.IntRect
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.padding
 import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
 import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
 import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
@@ -43,6 +45,7 @@
 import dagger.multibindings.IntoSet
 import java.util.Optional
 import javax.inject.Inject
+import kotlin.math.roundToInt
 
 /**
  * Renders the lockscreen scene when showing with the default layout (e.g. vertical phone form
@@ -68,6 +71,7 @@
         val isUdfpsVisible = viewModel.isUdfpsVisible
         val shouldUseSplitNotificationShade by
             viewModel.shouldUseSplitNotificationShade.collectAsState()
+        val unfoldTranslations by viewModel.unfoldTranslations.collectAsState()
 
         LockscreenLongPress(
             viewModel = viewModel.longPress,
@@ -79,10 +83,25 @@
                     Column(
                         modifier = Modifier.fillMaxSize(),
                     ) {
-                        with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
+                        with(statusBarSection) {
+                            StatusBar(
+                                modifier =
+                                    Modifier.fillMaxWidth()
+                                        .padding(
+                                            horizontal = { unfoldTranslations.start.roundToInt() },
+                                        )
+                            )
+                        }
 
                         Box {
-                            with(topAreaSection) { DefaultClockLayout() }
+                            with(topAreaSection) {
+                                DefaultClockLayout(
+                                    modifier =
+                                        Modifier.graphicsLayer {
+                                            translationX = unfoldTranslations.start
+                                        }
+                                )
+                            }
                             if (shouldUseSplitNotificationShade) {
                                 with(notificationSection) {
                                     Notifications(
@@ -127,8 +146,18 @@
 
                     // Aligned to bottom and NOT constrained by the lock icon.
                     with(bottomAreaSection) {
-                        Shortcut(isStart = true, applyPadding = true)
-                        Shortcut(isStart = false, applyPadding = true)
+                        Shortcut(
+                            isStart = true,
+                            applyPadding = true,
+                            modifier =
+                                Modifier.graphicsLayer { translationX = unfoldTranslations.start },
+                        )
+                        Shortcut(
+                            isStart = false,
+                            applyPadding = true,
+                            modifier =
+                                Modifier.graphicsLayer { translationX = unfoldTranslations.end },
+                        )
                     }
                     with(settingsMenuSection) { SettingsMenu(onSettingsMenuPlaced) }
                 },
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
index b8f00dc..9d31955 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
@@ -26,9 +26,11 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.unit.IntRect
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.padding
 import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
 import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
 import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
@@ -43,6 +45,7 @@
 import dagger.multibindings.IntoSet
 import java.util.Optional
 import javax.inject.Inject
+import kotlin.math.roundToInt
 
 /**
  * Renders the lockscreen scene when showing with the default layout (e.g. vertical phone form
@@ -68,6 +71,7 @@
         val isUdfpsVisible = viewModel.isUdfpsVisible
         val shouldUseSplitNotificationShade by
             viewModel.shouldUseSplitNotificationShade.collectAsState()
+        val unfoldTranslations by viewModel.unfoldTranslations.collectAsState()
 
         LockscreenLongPress(
             viewModel = viewModel.longPress,
@@ -79,10 +83,25 @@
                     Column(
                         modifier = Modifier.fillMaxSize(),
                     ) {
-                        with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
+                        with(statusBarSection) {
+                            StatusBar(
+                                modifier =
+                                    Modifier.fillMaxWidth()
+                                        .padding(
+                                            horizontal = { unfoldTranslations.start.roundToInt() },
+                                        )
+                            )
+                        }
 
                         Box {
-                            with(topAreaSection) { DefaultClockLayout() }
+                            with(topAreaSection) {
+                                DefaultClockLayout(
+                                    modifier =
+                                        Modifier.graphicsLayer {
+                                            translationX = unfoldTranslations.start
+                                        },
+                                )
+                            }
                             if (shouldUseSplitNotificationShade) {
                                 with(notificationSection) {
                                     Notifications(
@@ -111,12 +130,26 @@
                     }
 
                     // Constrained to the left of the lock icon (in left-to-right layouts).
-                    with(bottomAreaSection) { Shortcut(isStart = true, applyPadding = false) }
+                    with(bottomAreaSection) {
+                        Shortcut(
+                            isStart = true,
+                            applyPadding = false,
+                            modifier =
+                                Modifier.graphicsLayer { translationX = unfoldTranslations.start },
+                        )
+                    }
 
                     with(lockSection) { LockIcon() }
 
                     // Constrained to the right of the lock icon (in left-to-right layouts).
-                    with(bottomAreaSection) { Shortcut(isStart = false, applyPadding = false) }
+                    with(bottomAreaSection) {
+                        Shortcut(
+                            isStart = false,
+                            applyPadding = false,
+                            modifier =
+                                Modifier.graphicsLayer { translationX = unfoldTranslations.end },
+                        )
+                    }
 
                     // Aligned to bottom and constrained to below the lock icon.
                     Column(modifier = Modifier.fillMaxWidth()) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
index e9a8257..3497183 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
@@ -21,17 +21,21 @@
 import com.android.keyguard.KeyguardClockSwitch
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.authController
+import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.testKosmos
+import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import java.util.Locale
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -137,4 +141,47 @@
                     .isFalse()
             }
         }
+
+    @Test
+    fun unfoldTranslations() =
+        with(kosmos) {
+            testScope.runTest {
+                val maxTranslation = prepareConfiguration()
+                val translations by collectLastValue(underTest.unfoldTranslations)
+
+                val unfoldProvider = fakeUnfoldTransitionProgressProvider
+                unfoldProvider.onTransitionStarted()
+                assertThat(translations?.start).isEqualTo(0f)
+                assertThat(translations?.end).isEqualTo(-0f)
+
+                repeat(10) { repetition ->
+                    val transitionProgress = 0.1f * (repetition + 1)
+                    unfoldProvider.onTransitionProgress(transitionProgress)
+                    assertThat(translations?.start)
+                        .isEqualTo((1 - transitionProgress) * maxTranslation)
+                    assertThat(translations?.end)
+                        .isEqualTo(-(1 - transitionProgress) * maxTranslation)
+                }
+
+                unfoldProvider.onTransitionFinishing()
+                assertThat(translations?.start).isEqualTo(0f)
+                assertThat(translations?.end).isEqualTo(-0f)
+
+                unfoldProvider.onTransitionFinished()
+                assertThat(translations?.start).isEqualTo(0f)
+                assertThat(translations?.end).isEqualTo(-0f)
+            }
+        }
+
+    private fun prepareConfiguration(): Int {
+        val configuration = context.resources.configuration
+        configuration.setLayoutDirection(Locale.US)
+        kosmos.fakeConfigurationRepository.onConfigurationChange(configuration)
+        val maxTranslation = 10
+        kosmos.fakeConfigurationRepository.setDimensionPixelSize(
+            R.dimen.notification_side_paddings,
+            maxTranslation,
+        )
+        return maxTranslation
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
index 36896f9..ecad148 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
@@ -46,6 +47,7 @@
     val longPress: KeyguardLongPressViewModel,
     val shadeInteractor: ShadeInteractor,
     @Application private val applicationScope: CoroutineScope,
+    private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
 ) {
     private val clockSize = clockInteractor.clockSize
 
@@ -75,6 +77,23 @@
                 initialValue = false,
             )
 
+    /** Amount of horizontal translation that should be applied to elements in the scene. */
+    val unfoldTranslations: StateFlow<UnfoldTranslations> =
+        combine(
+                unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = true),
+                unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = false),
+            ) { start, end ->
+                UnfoldTranslations(
+                    start = start,
+                    end = end,
+                )
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = UnfoldTranslations(),
+            )
+
     fun getSmartSpacePaddingTop(resources: Resources): Int {
         return if (isLargeClockVisible) {
             resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) +
@@ -94,4 +113,20 @@
                 initialValue = interactor.getCurrentBlueprint().id,
             )
     }
+
+    data class UnfoldTranslations(
+
+        /**
+         * Amount of horizontal translation to apply to elements that are aligned to the start side
+         * (left in left-to-right layouts). Can also be used as horizontal padding for elements that
+         * need horizontal padding on both side. In pixels.
+         */
+        val start: Float = 0f,
+
+        /**
+         * Amount of horizontal translation to apply to elements that are aligned to the end side
+         * (right in left-to-right layouts). In pixels.
+         */
+        val end: Float = 0f,
+    )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
index 1e25f7f..30a4f21 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor
 
 val Kosmos.lockscreenContentViewModel by
     Kosmos.Fixture {
@@ -32,5 +33,6 @@
             longPress = keyguardLongPressViewModel,
             shadeInteractor = shadeInteractor,
             applicationScope = applicationCoroutineScope,
+            unfoldTransitionInteractor = unfoldTransitionInteractor,
         )
     }