Temporary hide notifications when folding/unfolding

This should reduce latency with complex notifications
by postponing the relayout.

Bug: 293824309
Test: atest HideNotificationsInteractorTest
Test: atest ConfigurationInteractorTest
Test: atest UnfoldTransitionInteractorTest
Flag: ACONFIG notifications_hide_on_display_switch DISABLED
Change-Id: I4cd6b718adc15b6c8261afe90437e7644702f56e
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 8f350a7..04c96ab 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -38,6 +38,13 @@
 }
 
 flag {
+    name: "notifications_hide_on_display_switch"
+    namespace: "systemui"
+    description: "Temporary hides notifications when folding/unfolding to reduce unfold latency"
+    bug: "293824309"
+}
+
+flag {
     name: "notification_lifetime_extension_refactor"
     namespace: "systemui"
     description: "Enables moving notification lifetime extension management from SystemUI to "
diff --git a/packages/SystemUI/src/com/android/systemui/common/CommonModule.kt b/packages/SystemUI/src/com/android/systemui/common/CommonModule.kt
new file mode 100644
index 0000000..5e6caf0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/CommonModule.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.common
+
+import com.android.systemui.common.domain.interactor.ConfigurationInteractor
+import com.android.systemui.common.domain.interactor.ConfigurationInteractorImpl
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+@Module
+abstract class CommonModule {
+    @Binds abstract fun bindRepository(impl: ConfigurationRepositoryImpl): ConfigurationRepository
+
+    @Binds abstract fun bindInteractor(impl: ConfigurationInteractorImpl): ConfigurationInteractor
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/domain/interactor/ConfigurationInteractor.kt b/packages/SystemUI/src/com/android/systemui/common/domain/interactor/ConfigurationInteractor.kt
new file mode 100644
index 0000000..89053d1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/domain/interactor/ConfigurationInteractor.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.common.domain.interactor
+
+import android.content.res.Configuration
+import android.graphics.Rect
+import android.view.Surface
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+interface ConfigurationInteractor {
+    /**
+     * Returns screen size adjusted to rotation, so returned screen sizes are stable across all
+     * rotations, could be useful if you need to react to screen resize (e.g. fold/unfold on
+     * foldable devices)
+     */
+    val naturalMaxBounds: Flow<Rect>
+}
+
+class ConfigurationInteractorImpl
+@Inject
+constructor(private val repository: ConfigurationRepository) : ConfigurationInteractor {
+
+    override val naturalMaxBounds: Flow<Rect>
+        get() = repository.configurationValues.map { it.naturalScreenBounds }.distinctUntilChanged()
+
+    /**
+     * Returns screen size adjusted to rotation, so returned screen size is stable across all
+     * rotations
+     */
+    private val Configuration.naturalScreenBounds: Rect
+        get() {
+            val rotation = windowConfiguration.displayRotation
+            val maxBounds = windowConfiguration.maxBounds
+            return if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
+                Rect(0, 0, maxBounds.width(), maxBounds.height())
+            } else {
+                Rect(0, 0, maxBounds.height(), maxBounds.width())
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/data/CommonUiDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/common/ui/data/CommonUiDataLayerModule.kt
deleted file mode 100644
index b0e6931..0000000
--- a/packages/SystemUI/src/com/android/systemui/common/ui/data/CommonUiDataLayerModule.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * 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.common.ui.data
-
-import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryModule
-import dagger.Module
-
-@Module(includes = [ConfigurationRepositoryModule::class]) object CommonUiDataLayerModule
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
index 7fa762a..2052c70 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt
@@ -22,7 +22,7 @@
 import android.view.DisplayInfo
 import androidx.annotation.DimenRes
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -49,6 +49,7 @@
     val onConfigurationChange: Flow<Unit>
 
     val scaleForResolution: Flow<Float>
+    val configurationValues: Flow<Configuration>
 
     fun getResolutionScale(): Float
 
@@ -68,7 +69,7 @@
     private val displayInfo = MutableStateFlow(DisplayInfo())
 
     override val onAnyConfigurationChange: Flow<Unit> =
-        ConflatedCallbackFlow.conflatedCallbackFlow {
+        conflatedCallbackFlow {
             val callback =
                 object : ConfigurationController.ConfigurationListener {
                     override fun onUiModeChanged() {
@@ -92,7 +93,7 @@
         }
 
     override val onConfigurationChange: Flow<Unit> =
-        ConflatedCallbackFlow.conflatedCallbackFlow {
+        conflatedCallbackFlow {
             val callback =
                 object : ConfigurationController.ConfigurationListener {
                     override fun onConfigChanged(newConfig: Configuration) {
@@ -103,6 +104,20 @@
             awaitClose { configurationController.removeCallback(callback) }
         }
 
+    override val configurationValues: Flow<Configuration> =
+            conflatedCallbackFlow {
+                val callback =
+                        object : ConfigurationController.ConfigurationListener {
+                            override fun onConfigChanged(newConfig: Configuration) {
+                                trySend(newConfig)
+                            }
+                        }
+
+                trySend(context.resources.configuration)
+                configurationController.addCallback(callback)
+                awaitClose { configurationController.removeCallback(callback) }
+            }
+
     override val scaleForResolution: StateFlow<Float> =
         onConfigurationChange
             .mapLatest { getResolutionScale() }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index b34b459..9fc86ad 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -41,9 +41,9 @@
 import com.android.systemui.bouncer.ui.BouncerViewModule;
 import com.android.systemui.classifier.FalsingModule;
 import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule;
-import com.android.systemui.common.ui.data.CommonUiDataLayerModule;
 import com.android.systemui.communal.dagger.CommunalModule;
 import com.android.systemui.complication.dagger.ComplicationComponent;
+import com.android.systemui.common.CommonModule;
 import com.android.systemui.controls.dagger.ControlsModule;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.SystemUser;
@@ -172,8 +172,8 @@
         BouncerViewModule.class,
         ClipboardOverlayModule.class,
         ClockRegistryModule.class,
-        CommonUiDataLayerModule.class,
         CommunalModule.class,
+        CommonModule.class,
         ConnectivityModule.class,
         ControlsModule.class,
         CoroutinesModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 823caa0..285cb5a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -2673,7 +2673,7 @@
                 && !mQsController.getFullyExpanded()) {
             alpha *= mClockPositionResult.clockAlpha;
         }
-        mNotificationStackScrollLayoutController.setAlpha(alpha);
+        mNotificationStackScrollLayoutController.setMaxAlphaForExpansion(alpha);
     }
 
     private float getFadeoutAlpha() {
@@ -4697,7 +4697,7 @@
             NotificationStackScrollLayoutController stackScroller) {
         return (Float alpha) -> {
             mKeyguardStatusViewController.setAlpha(alpha);
-            stackScroller.setAlpha(alpha);
+            stackScroller.setMaxAlphaForExpansion(alpha);
 
             if (keyguardBottomAreaRefactor()) {
                 mKeyguardInteractor.setAlpha(alpha);
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 a0ad560..2f8a375 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
@@ -1157,6 +1157,20 @@
         return mSpeedBumpIndex;
     }
 
+    private boolean mSuppressChildrenMeasureAndLayout = false;
+
+    /**
+     * Similar to {@link ViewGroup#suppressLayout} but still performs layout of
+     * the container itself and suppresses only measure and layout calls to children.
+     */
+    public void suppressChildrenMeasureAndLayout(boolean suppress) {
+        mSuppressChildrenMeasureAndLayout = suppress;
+
+        if (!suppress) {
+            requestLayout();
+        }
+    }
+
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         Trace.beginSection("NotificationStackScrollLayout#onMeasure");
@@ -1169,6 +1183,12 @@
 
         int width = MeasureSpec.getSize(widthMeasureSpec);
         updateSidePadding(width);
+
+        if (mSuppressChildrenMeasureAndLayout) {
+            Trace.endSection();
+            return;
+        }
+
         int childWidthSpec = MeasureSpec.makeMeasureSpec(width - mSidePaddings * 2,
                 MeasureSpec.getMode(widthMeasureSpec));
         // Don't constrain the height of the children so we know how big they'd like to be
@@ -1192,18 +1212,21 @@
 
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        // we layout all our children centered on the top
-        float centerX = getWidth() / 2.0f;
-        for (int i = 0; i < getChildCount(); i++) {
-            View child = getChildAt(i);
-            // We need to layout all children even the GONE ones, such that the heights are
-            // calculated correctly as they are used to calculate how many we can fit on the screen
-            float width = child.getMeasuredWidth();
-            float height = child.getMeasuredHeight();
-            child.layout((int) (centerX - width / 2.0f),
-                    0,
-                    (int) (centerX + width / 2.0f),
-                    (int) height);
+        if (!mSuppressChildrenMeasureAndLayout) {
+            // we layout all our children centered on the top
+            float centerX = getWidth() / 2.0f;
+            for (int i = 0; i < getChildCount(); i++) {
+                View child = getChildAt(i);
+                // We need to layout all children even the GONE ones, such that the heights are
+                // calculated correctly as they are used to calculate how many we can fit on
+                // the screen
+                float width = child.getMeasuredWidth();
+                float height = child.getMeasuredHeight();
+                child.layout((int) (centerX - width / 2.0f),
+                        0,
+                        (int) (centerX + width / 2.0f),
+                        (int) height);
+            }
         }
         setMaxLayoutHeight(getHeight());
         updateContentHeight();
@@ -5097,6 +5120,7 @@
             println(pw, "qsClipDismiss", mDismissUsingRowTranslationX);
             println(pw, "visibility", visibilityString(getVisibility()));
             println(pw, "alpha", getAlpha());
+            println(pw, "suppressChildrenMeasureLayout", mSuppressChildrenMeasureAndLayout);
             println(pw, "scrollY", mAmbientState.getScrollY());
             println(pw, "maxTopPadding", mMaxTopPadding);
             println(pw, "showShelfOnly", mShouldShowShelfOnly);
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 44140b9..99b3a00 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
@@ -18,6 +18,8 @@
 
 import static android.service.notification.NotificationStats.DISMISSAL_SHADE;
 import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
+
+import static com.android.app.animation.Interpolators.STANDARD;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
 import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
@@ -27,8 +29,10 @@
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_HIGH_PRIORITY;
 import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.SelectedRows;
+import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_STANDARD;
 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
+import android.animation.ObjectAnimator;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Point;
@@ -38,6 +42,7 @@
 import android.service.notification.StatusBarNotification;
 import android.util.Log;
 import android.util.Pair;
+import android.util.Property;
 import android.view.Display;
 import android.view.MotionEvent;
 import android.view.View;
@@ -53,6 +58,8 @@
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.view.OneShotPreDrawListener;
+import com.android.systemui.Dumpable;
 import com.android.systemui.ExpandHelper;
 import com.android.systemui.Gefingerpoken;
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
@@ -133,6 +140,7 @@
 import com.android.systemui.util.Compile;
 import com.android.systemui.util.settings.SecureSettings;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.BiConsumer;
@@ -145,7 +153,7 @@
  * Controller for {@link NotificationStackScrollLayout}.
  */
 @SysUISingleton
-public class NotificationStackScrollLayoutController {
+public class NotificationStackScrollLayoutController implements Dumpable {
     private static final String TAG = "StackScrollerController";
     private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
     private static final String HIGH_PRIORITY = "high_priority";
@@ -190,7 +198,6 @@
     private final GroupExpansionManager mGroupExpansionManager;
     private final SeenNotificationsInteractor mSeenNotificationsInteractor;
     private final KeyguardTransitionRepository mKeyguardTransitionRepo;
-
     private NotificationStackScrollLayout mView;
     private NotificationSwipeHelper mSwipeHelper;
     @Nullable
@@ -240,6 +247,22 @@
                 }
             };
 
+    private static final Property<NotificationStackScrollLayoutController, Float>
+            HIDE_ALPHA_PROPERTY = new Property<>(Float.class, "HideNotificationsAlpha") {
+                @Override
+                public Float get(NotificationStackScrollLayoutController object) {
+                    return object.mMaxAlphaForUnhide;
+                }
+
+                @Override
+                public void set(NotificationStackScrollLayoutController object, Float value) {
+                    object.setMaxAlphaForUnhide(value);
+                }
+            };
+
+    @Nullable
+    private ObjectAnimator mHideAlphaAnimator = null;
+
     private final DeviceProvisionedListener mDeviceProvisionedListener =
             new DeviceProvisionedListener() {
                 @Override
@@ -302,6 +325,8 @@
     };
 
     private NotifStats mNotifStats = NotifStats.getEmpty();
+    private float mMaxAlphaForExpansion = 1.0f;
+    private float mMaxAlphaForUnhide = 1.0f;
 
     private final NotificationListViewBinder mViewBinder;
 
@@ -713,6 +738,7 @@
         mDismissibilityProvider = dismissibilityProvider;
         mActivityStarter = activityStarter;
         mView.passSplitShadeStateController(splitShadeStateController);
+        mDumpManager.registerDumpable(this);
         updateResources();
         setUpView();
     }
@@ -818,7 +844,7 @@
         mGroupExpansionManager.registerGroupExpansionChangeListener(
                 (changedRow, expanded) -> mView.onGroupExpandChanged(changedRow, expanded));
 
-        mViewBinder.bind(mView);
+        mViewBinder.bind(mView, this);
 
         collectFlow(mView, mKeyguardTransitionRepo.getTransitions(),
                 this::onKeyguardTransitionChanged);
@@ -875,6 +901,10 @@
         mView.requestLayout();
     }
 
+    public void addOneShotPreDrawListener(Runnable runnable) {
+        OneShotPreDrawListener.add(mView, runnable);
+    }
+
     public Display getDisplay() {
         return mView.getDisplay();
     }
@@ -1157,12 +1187,49 @@
         return mView.getEmptyShadeViewHeight();
     }
 
-    public void setAlpha(float alpha) {
+    public void setMaxAlphaForExpansion(float alpha) {
+        mMaxAlphaForExpansion = alpha;
+        updateAlpha();
+    }
+
+    private void setMaxAlphaForUnhide(float alpha) {
+        mMaxAlphaForUnhide = alpha;
+        updateAlpha();
+    }
+
+    private void updateAlpha() {
         if (mView != null) {
-            mView.setAlpha(alpha);
+            mView.setAlpha(Math.min(mMaxAlphaForExpansion, mMaxAlphaForUnhide));
         }
     }
 
+    public void setSuppressChildrenMeasureAndLayout(boolean suppressLayout) {
+        mView.suppressChildrenMeasureAndLayout(suppressLayout);
+    }
+
+    public void updateNotificationsContainerVisibility(boolean visible, boolean animate) {
+        if (mHideAlphaAnimator != null) {
+            mHideAlphaAnimator.cancel();
+        }
+
+        final float targetAlpha = visible ? 1f : 0f;
+
+        if (animate) {
+            mHideAlphaAnimator = createAlphaAnimator(targetAlpha);
+            mHideAlphaAnimator.start();
+        } else {
+            HIDE_ALPHA_PROPERTY.set(this, targetAlpha);
+        }
+    }
+
+    private ObjectAnimator createAlphaAnimator(float targetAlpha) {
+        final ObjectAnimator objectAnimator = ObjectAnimator
+                .ofFloat(this, HIDE_ALPHA_PROPERTY, targetAlpha);
+        objectAnimator.setInterpolator(STANDARD);
+        objectAnimator.setDuration(ANIMATION_DURATION_STANDARD);
+        return objectAnimator;
+    }
+
     public float calculateAppearFraction(float height) {
         return mView.calculateAppearFraction(height);
     }
@@ -1635,6 +1702,12 @@
         }
     }
 
+    @Override
+    public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+        pw.println("mMaxAlphaForExpansion=" + mMaxAlphaForExpansion);
+        pw.println("mMaxAlphaForUnhide=" + mMaxAlphaForUnhide);
+    }
+
     /**
      * Enum for UiEvent logged from this class
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractor.kt
new file mode 100644
index 0000000..4de3a7f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractor.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.statusbar.notification.stack.domain.interactor
+
+import android.graphics.Rect
+import android.util.Log
+import com.android.app.tracing.FlowTracing.traceEach
+import com.android.systemui.common.domain.interactor.ConfigurationInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_ON
+import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
+import com.android.systemui.util.animation.data.repository.AnimationStatusRepository
+import com.android.systemui.util.kotlin.WithPrev
+import com.android.systemui.util.kotlin.area
+import com.android.systemui.util.kotlin.pairwise
+import com.android.systemui.util.kotlin.race
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.withTimeout
+import javax.inject.Inject
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class HideNotificationsInteractor
+@Inject
+constructor(
+    private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
+    private val configurationInteractor: ConfigurationInteractor,
+    private val animationsStatus: AnimationStatusRepository,
+    private val powerInteractor: PowerInteractor
+) {
+
+    val shouldHideNotifications: Flow<Boolean>
+        get() =
+            if (!unfoldTransitionInteractor.isAvailable) {
+                // Do nothing on non-foldable devices
+                emptyFlow()
+            } else {
+                screenSizeChangesFlow
+                    .flatMapLatest {
+                        flow {
+                            // Hide notifications on each display resize
+                            emit(true)
+                            try {
+                                waitForDisplaySwitchFinish(it)
+                            } catch (_: TimeoutCancellationException) {
+                                Log.e(TAG, "Timed out waiting for display switch")
+                            } finally {
+                                emit(false)
+                            }
+                        }
+                    }
+                    .distinctUntilChanged()
+                    .traceEach(HIDE_STATUS_TRACK_NAME, logcat = true) { shouldHide ->
+                        if (shouldHide) "hidden" else "visible"
+                    }
+            }
+
+    private suspend fun waitForDisplaySwitchFinish(screenSizeChange: WithPrev<Rect, Rect>) {
+        withTimeout(timeMillis = DISPLAY_SWITCH_TIMEOUT_MILLIS) {
+            val waitForDisplaySwitchOrAnimation: suspend () -> Unit = {
+                if (shouldWaitForAnimationEnd(screenSizeChange)) {
+                    unfoldTransitionInteractor.waitForTransitionFinish()
+                } else {
+                    waitForScreenTurnedOn()
+                }
+            }
+
+            race({ waitForDisplaySwitchOrAnimation() }, { waitForGoingToSleep() })
+        }
+    }
+
+    private suspend fun shouldWaitForAnimationEnd(screenSizeChange: WithPrev<Rect, Rect>): Boolean =
+        animationsStatus.areAnimationsEnabled().first() && screenSizeChange.isUnfold
+
+    private suspend fun waitForScreenTurnedOn() =
+        powerInteractor.screenPowerState.filter { it == SCREEN_ON }.first()
+
+    private suspend fun waitForGoingToSleep() =
+        powerInteractor.detailedWakefulness.filter { it.isAsleep() }.first()
+
+    private val screenSizeChangesFlow: Flow<WithPrev<Rect, Rect>>
+        get() = configurationInteractor.naturalMaxBounds.pairwise()
+
+    private val WithPrev<Rect, Rect>.isUnfold: Boolean
+        get() = newValue.area > previousValue.area
+
+    private companion object {
+        private const val TAG = "DisplaySwitchNotificationsHideInteractor"
+        private const val HIDE_STATUS_TRACK_NAME = "NotificationsHiddenForDisplayChange"
+        private const val DISPLAY_SWITCH_TIMEOUT_MILLIS = 5_000L
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt
new file mode 100644
index 0000000..98c1734
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/DisplaySwitchNotificationsHiderFlag.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.statusbar.notification.stack.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the DisplaySwitchNotificationsHider flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object DisplaySwitchNotificationsHiderFlag {
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_HIDE_ON_DISPLAY_SWITCH
+
+    /** Is the hiding enabled? */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.notificationsHideOnDisplaySwitch()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
new file mode 100644
index 0000000..274bf94
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.statusbar.notification.stack.ui.viewbinder
+
+import androidx.core.view.doOnDetach
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
+import kotlinx.coroutines.launch
+
+/**
+ * Binds a [NotificationStackScrollLayoutController] to its [view model][NotificationListViewModel].
+ */
+object HideNotificationsBinder {
+    fun bindHideList(
+        viewController: NotificationStackScrollLayoutController,
+        viewModel: NotificationListViewModel
+    ) {
+        viewController.view.repeatWhenAttached {
+            lifecycleScope.launch {
+                viewModel.hideListViewModel.shouldHideListForPerformance.collect { shouldHide ->
+                    viewController.bindHideState(shouldHide)
+                }
+            }
+        }
+
+        viewController.view.doOnDetach { viewController.bindHideState(shouldHide = false) }
+    }
+
+    private fun NotificationStackScrollLayoutController.bindHideState(shouldHide: Boolean) {
+        if (shouldHide) {
+            updateNotificationsContainerVisibility(/* visible= */ false, /* animate=*/ false)
+            setSuppressChildrenMeasureAndLayout(true)
+        } else {
+            setSuppressChildrenMeasureAndLayout(false)
+
+            // Show notifications back only after layout has finished because we need
+            // to wait until they have resized to the new display size
+            addOneShotPreDrawListener {
+                updateNotificationsContainerVisibility(/* visible= */ true, /* animate=*/ true)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index d55c0de..6cf5610 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -29,6 +29,8 @@
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore
 import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -46,9 +48,13 @@
     private val shelfIconViewStore: ShelfNotificationIconViewStore,
 ) {
 
-    fun bind(view: NotificationStackScrollLayout) {
+    fun bind(
+        view: NotificationStackScrollLayout,
+        viewController: NotificationStackScrollLayoutController
+    ) {
         bindShelf(view)
         bindFooter(view)
+        bindHideList(viewController, viewModel)
     }
 
     private fun bindShelf(parentView: NotificationStackScrollLayout) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HideListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HideListViewModel.kt
new file mode 100644
index 0000000..e1d14d1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HideListViewModel.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.statusbar.notification.stack.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.stack.domain.interactor.HideNotificationsInteractor
+import com.android.systemui.statusbar.notification.stack.shared.DisplaySwitchNotificationsHiderFlag
+import javax.inject.Inject
+import javax.inject.Provider
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+
+@SysUISingleton
+class HideListViewModel
+@Inject
+constructor(
+    private val hideNotificationsInteractor: Provider<HideNotificationsInteractor>,
+) {
+    /**
+     * Emits `true` whenever we want to hide the notifications list for performance reasons, then it
+     * emits 'false' to show notifications back. This is used on foldable devices and emits
+     * *nothing* on other devices.
+     */
+    val shouldHideListForPerformance: Flow<Boolean>
+        get() =
+            if (DisplaySwitchNotificationsHiderFlag.isEnabled) {
+                hideNotificationsInteractor.get().shouldHideNotifications
+            } else emptyFlow()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index f01245f..4f76680 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -26,5 +26,6 @@
 @Inject
 constructor(
     val shelf: NotificationShelfViewModel,
-    val footer: Optional<FooterViewModel>,
+    val hideListViewModel: HideListViewModel,
+    val footer: Optional<FooterViewModel>
 )
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
index 71314f1..7b628f8 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldTransitionModule.kt
@@ -24,6 +24,10 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.LifecycleScreenStatusProvider
 import com.android.systemui.unfold.config.UnfoldTransitionConfig
+import com.android.systemui.unfold.data.repository.UnfoldTransitionRepository
+import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
+import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
+import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractorImpl
 import com.android.systemui.unfold.system.SystemUnfoldSharedModule
 import com.android.systemui.unfold.updates.FoldProvider
 import com.android.systemui.unfold.updates.FoldStateProvider
@@ -149,8 +153,7 @@
 
         return resultingProvider?.get()?.orElse(null)?.let { unfoldProgressProvider ->
             UnfoldProgressProvider(unfoldProgressProvider, foldProvider)
-        }
-            ?: ShellUnfoldProgressProvider.NO_PROVIDER
+        } ?: ShellUnfoldProgressProvider.NO_PROVIDER
     }
 
     @Provides
@@ -162,6 +165,10 @@
         @IntoMap
         @ClassKey(UnfoldTraceLogger::class)
         fun bindUnfoldTraceLogger(impl: UnfoldTraceLogger): CoreStartable
+
+        @Binds fun bindRepository(impl: UnfoldTransitionRepositoryImpl): UnfoldTransitionRepository
+
+        @Binds fun bindInteractor(impl: UnfoldTransitionInteractorImpl): UnfoldTransitionInteractor
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt
new file mode 100644
index 0000000..0d3682c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/data/repository/UnfoldTransitionRepository.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.unfold.data.repository
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished
+import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionStarted
+import com.android.systemui.util.kotlin.getOrNull
+import java.util.Optional
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+
+/** Repository for fold/unfold transitions */
+interface UnfoldTransitionRepository {
+    /** Returns false if fold/unfold transitions are not available on this device */
+    val isAvailable: Boolean
+
+    /**
+     * Emits current transition state on each transition change such as transition start or finish
+     * [UnfoldTransitionStatus]
+     */
+    val transitionStatus: Flow<UnfoldTransitionStatus>
+}
+
+/** Transition event of fold/unfold transition */
+sealed class UnfoldTransitionStatus {
+    /** Status that is sent when fold or unfold transition is in started state */
+    data object TransitionStarted : UnfoldTransitionStatus()
+    /** Status that is sent when fold or unfold transition is finished */
+    data object TransitionFinished : UnfoldTransitionStatus()
+}
+
+class UnfoldTransitionRepositoryImpl
+@Inject
+constructor(
+    private val unfoldProgressProvider: Optional<UnfoldTransitionProgressProvider>,
+) : UnfoldTransitionRepository {
+
+    override val isAvailable: Boolean
+        get() = unfoldProgressProvider.isPresent
+
+    override val transitionStatus: Flow<UnfoldTransitionStatus>
+        get() {
+            val provider = unfoldProgressProvider.getOrNull() ?: return emptyFlow()
+
+            return conflatedCallbackFlow {
+                val callback =
+                    object : UnfoldTransitionProgressProvider.TransitionProgressListener {
+                        override fun onTransitionStarted() {
+                            trySend(TransitionStarted)
+                        }
+
+                        override fun onTransitionFinished() {
+                            trySend(TransitionFinished)
+                        }
+                    }
+                provider.addCallback(callback)
+                awaitClose { provider.removeCallback(callback) }
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt
new file mode 100644
index 0000000..a2e77af
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractor.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.unfold.domain.interactor
+
+import com.android.systemui.unfold.data.repository.UnfoldTransitionStatus.TransitionFinished
+import com.android.systemui.unfold.data.repository.UnfoldTransitionRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+
+interface UnfoldTransitionInteractor {
+    val isAvailable: Boolean
+
+    suspend fun waitForTransitionFinish()
+}
+
+class UnfoldTransitionInteractorImpl
+@Inject
+constructor(private val repository: UnfoldTransitionRepository) : UnfoldTransitionInteractor {
+
+    override val isAvailable: Boolean
+        get() = repository.isAvailable
+
+    override suspend fun waitForTransitionFinish() {
+        repository.transitionStatus.filter { it is TransitionFinished }.first()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt b/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt
new file mode 100644
index 0000000..adae782
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.animation.data.repository
+
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.os.Handler
+import android.provider.Settings
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.unfold.util.ScaleAwareTransitionProgressProvider.Companion.areAnimationsEnabled
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.withContext
+
+/** Utility class that could give information about if animation are enabled in the system */
+interface AnimationStatusRepository {
+    fun areAnimationsEnabled(): Flow<Boolean>
+}
+
+class AnimationStatusRepositoryImpl
+@Inject
+constructor(
+    private val resolver: ContentResolver,
+    @Background private val backgroundHandler: Handler,
+    @Background private val backgroundDispatcher: CoroutineDispatcher
+) : AnimationStatusRepository {
+
+    /**
+     * Emits true if animations are enabled in the system, after subscribing it immediately emits
+     * the current state
+     */
+    override fun areAnimationsEnabled(): Flow<Boolean> = conflatedCallbackFlow {
+        val initialValue = withContext(backgroundDispatcher) { resolver.areAnimationsEnabled() }
+        trySend(initialValue)
+
+        val observer =
+            object : ContentObserver(backgroundHandler) {
+                override fun onChange(selfChange: Boolean) {
+                    val updatedValue = resolver.areAnimationsEnabled()
+                    trySend(updatedValue)
+                }
+            }
+
+        resolver.registerContentObserver(
+            Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE),
+            /* notifyForDescendants= */ false,
+            observer
+        )
+
+        awaitClose { resolver.unregisterContentObserver(observer) }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java b/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java
index 981bf01..9c8a481 100644
--- a/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java
+++ b/packages/SystemUI/src/com/android/systemui/util/dagger/UtilModule.java
@@ -18,6 +18,8 @@
 
 import com.android.systemui.util.RingerModeTracker;
 import com.android.systemui.util.RingerModeTrackerImpl;
+import com.android.systemui.util.animation.data.repository.AnimationStatusRepository;
+import com.android.systemui.util.animation.data.repository.AnimationStatusRepositoryImpl;
 import com.android.systemui.util.wrapper.UtilWrapperModule;
 
 import dagger.Binds;
@@ -31,4 +33,8 @@
     /** */
     @Binds
     RingerModeTracker provideRingerModeTracker(RingerModeTrackerImpl ringerModeTrackerImpl);
+
+    @Binds
+    AnimationStatusRepository provideAnimationStatus(
+            AnimationStatusRepositoryImpl ringerModeTrackerImpl);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Rect.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Rect.kt
new file mode 100644
index 0000000..bcbc89c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Rect.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.kotlin
+
+import android.graphics.Rect
+
+/** Returns the area of this rectangle */
+val Rect.area: Long
+    get() = width().toLong() * height().toLong()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/common/domain/interactor/ConfigurationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/common/domain/interactor/ConfigurationInteractorTest.kt
new file mode 100644
index 0000000..bfa3641
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/common/domain/interactor/ConfigurationInteractorTest.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.common.domain.interactor
+
+import android.content.res.Configuration
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.view.Surface.ROTATION_0
+import android.view.Surface.ROTATION_90
+import android.view.Surface.Rotation
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.statusbar.policy.FakeConfigurationController
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+open class ConfigurationInteractorTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+
+    private val configurationController = FakeConfigurationController()
+    private val configurationRepository =
+        ConfigurationRepositoryImpl(
+            configurationController,
+            context,
+            testScope.backgroundScope,
+            mock()
+        )
+
+    private lateinit var configuration: Configuration
+    private lateinit var underTest: ConfigurationInteractor
+
+    @Before
+    fun setUp() {
+        configuration = context.resources.configuration
+
+        val testableResources = context.getOrCreateTestableResources()
+        testableResources.overrideConfiguration(configuration)
+
+        underTest = ConfigurationInteractorImpl(configurationRepository)
+    }
+
+    @Test
+    fun maxBoundsChange_emitsMaxBoundsChange() =
+        testScope.runTest {
+            val values by collectValues(underTest.naturalMaxBounds)
+
+            updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
+            runCurrent()
+            updateDisplay(width = DISPLAY_WIDTH * 2, height = DISPLAY_HEIGHT * 3)
+            runCurrent()
+
+            assertThat(values)
+                .containsExactly(
+                    Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT),
+                    Rect(0, 0, DISPLAY_WIDTH * 2, DISPLAY_HEIGHT * 3),
+                )
+                .inOrder()
+        }
+
+    @Test
+    fun maxBoundsSameOnConfigChange_doesNotEmitMaxBoundsChange() =
+        testScope.runTest {
+            val values by collectValues(underTest.naturalMaxBounds)
+
+            updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
+            runCurrent()
+            updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
+            runCurrent()
+
+            assertThat(values).containsExactly(Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT))
+        }
+
+    @Test
+    fun firstMaxBoundsChange_emitsMaxBoundsChange() =
+        testScope.runTest {
+            val values by collectValues(underTest.naturalMaxBounds)
+
+            updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
+            runCurrent()
+
+            assertThat(values).containsExactly(Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT))
+        }
+
+    @Test
+    fun displayRotatedButMaxBoundsTheSame_doesNotEmitNewMaxBoundsChange() =
+        testScope.runTest {
+            val values by collectValues(underTest.naturalMaxBounds)
+
+            updateDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
+            runCurrent()
+            updateDisplay(width = DISPLAY_HEIGHT, height = DISPLAY_WIDTH, rotation = ROTATION_90)
+            runCurrent()
+
+            assertThat(values).containsExactly(Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT))
+        }
+
+    private fun updateDisplay(
+        width: Int = DISPLAY_WIDTH,
+        height: Int = DISPLAY_HEIGHT,
+        @Rotation rotation: Int = ROTATION_0
+    ) {
+        configuration.windowConfiguration.maxBounds.set(Rect(0, 0, width, height))
+        configuration.windowConfiguration.displayRotation = rotation
+
+        configurationController.onConfigurationChanged(configuration)
+    }
+
+    private companion object {
+        private const val DISPLAY_WIDTH = 100
+        private const val DISPLAY_HEIGHT = 200
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 1a8d4f9..d573764 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -793,7 +793,7 @@
         // We are interested in the last value of the stack alpha.
         ArgumentCaptor<Float> alphaCaptor = ArgumentCaptor.forClass(Float.class);
         verify(mNotificationStackScrollLayoutController, atLeastOnce())
-                .setAlpha(alphaCaptor.capture());
+                .setMaxAlphaForExpansion(alphaCaptor.capture());
         assertThat(alphaCaptor.getValue()).isEqualTo(1.0f);
     }
 
@@ -814,7 +814,7 @@
         // We are interested in the last value of the stack alpha.
         ArgumentCaptor<Float> alphaCaptor = ArgumentCaptor.forClass(Float.class);
         verify(mNotificationStackScrollLayoutController, atLeastOnce())
-                .setAlpha(alphaCaptor.capture());
+                .setMaxAlphaForExpansion(alphaCaptor.capture());
         assertThat(alphaCaptor.getValue()).isEqualTo(0.0f);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt
new file mode 100644
index 0000000..46e8453
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt
@@ -0,0 +1,302 @@
+/*
+ * 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.statusbar.notification.stack.domain.interactor
+
+import android.content.res.Configuration
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.view.Surface
+import android.view.Surface.ROTATION_0
+import android.view.Surface.ROTATION_90
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.domain.interactor.ConfigurationInteractorImpl
+import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.ScreenPowerState.SCREEN_ON
+import com.android.systemui.power.shared.model.WakefulnessState.STARTING_TO_SLEEP
+import com.android.systemui.statusbar.policy.FakeConfigurationController
+import com.android.systemui.unfold.TestUnfoldTransitionProvider
+import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
+import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractorImpl
+import com.android.systemui.util.animation.FakeAnimationStatusRepository
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+import java.time.Duration
+import java.util.Optional
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+open class HideNotificationsInteractorTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+
+    private val animationStatus = FakeAnimationStatusRepository()
+    private val configurationController = FakeConfigurationController()
+    private val unfoldTransitionProgressProvider = TestUnfoldTransitionProvider()
+    private val powerRepository = FakePowerRepository()
+    private val powerInteractor =
+        PowerInteractor(
+            repository = powerRepository,
+            falsingCollector = mock(),
+            screenOffAnimationController = mock(),
+            statusBarStateController = mock()
+        )
+
+    private val unfoldTransitionRepository =
+        UnfoldTransitionRepositoryImpl(Optional.of(unfoldTransitionProgressProvider))
+    private val unfoldTransitionInteractor =
+        UnfoldTransitionInteractorImpl(unfoldTransitionRepository)
+
+    private val configurationRepository =
+        ConfigurationRepositoryImpl(
+            configurationController,
+            context,
+            testScope.backgroundScope,
+            mock()
+        )
+    private val configurationInteractor = ConfigurationInteractorImpl(configurationRepository)
+
+    private lateinit var configuration: Configuration
+    private lateinit var underTest: HideNotificationsInteractor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        configuration = context.resources.configuration
+
+        val testableResources = context.getOrCreateTestableResources()
+        testableResources.overrideConfiguration(configuration)
+
+        updateDisplay()
+
+        underTest =
+            HideNotificationsInteractor(
+                unfoldTransitionInteractor,
+                configurationInteractor,
+                animationStatus,
+                powerInteractor
+            )
+    }
+
+    @Test
+    fun displaySwitch_hidesNotifications() =
+        testScope.runTest {
+            val values by collectValues(hideNotificationsFlow)
+
+            runCurrent()
+            updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+            runCurrent()
+
+            assertThat(values).containsExactly(true).inOrder()
+        }
+
+    @Test
+    fun displaySwitch_sizeIsTheSame_noChangesToNotifications() =
+        testScope.runTest {
+            val values by collectValues(hideNotificationsFlow)
+
+            runCurrent()
+            updateDisplay(width = INITIAL_DISPLAY_WIDTH)
+            runCurrent()
+
+            assertThat(values).isEmpty()
+        }
+
+    @Test
+    fun displaySwitch_sizeIsTheSameAfterRotation_noChangesToNotifications() =
+        testScope.runTest {
+            val values by collectValues(hideNotificationsFlow)
+
+            runCurrent()
+            updateDisplay(
+                width = INITIAL_DISPLAY_HEIGHT,
+                height = INITIAL_DISPLAY_WIDTH,
+                rotation = ROTATION_90
+            )
+            runCurrent()
+
+            assertThat(values).isEmpty()
+        }
+
+    @Test
+    fun displaySwitch_noAnimations_screenTurnedOn_showsNotificationsBack() =
+        testScope.runTest {
+            givenAnimationsEnabled(false)
+            val values by collectValues(hideNotificationsFlow)
+
+            runCurrent()
+            updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+            runCurrent()
+            powerRepository.setScreenPowerState(SCREEN_ON)
+            runCurrent()
+
+            assertThat(values).containsExactly(true, false).inOrder()
+        }
+
+    @Test
+    fun displaySwitchUnfold_animationsEnabled_screenTurnedOn_doesNotShowNotifications() =
+        testScope.runTest {
+            givenAnimationsEnabled(true)
+            val values by collectValues(hideNotificationsFlow)
+
+            runCurrent()
+            updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+            runCurrent()
+            powerRepository.setScreenPowerState(SCREEN_ON)
+            runCurrent()
+
+            assertThat(values).containsExactly(true).inOrder()
+        }
+
+    @Test
+    fun displaySwitchFold_animationsEnabled_screenTurnedOn_showsNotifications() =
+        testScope.runTest {
+            givenAnimationsEnabled(true)
+            val values by collectValues(hideNotificationsFlow)
+
+            runCurrent()
+            updateDisplay(width = INITIAL_DISPLAY_WIDTH / 2)
+            runCurrent()
+            powerRepository.setScreenPowerState(SCREEN_ON)
+            runCurrent()
+
+            assertThat(values).containsExactly(true, false).inOrder()
+        }
+
+    @Test
+    fun displaySwitch_noAnimations_screenGoesToSleep_showsNotificationsBack() =
+        testScope.runTest {
+            givenAnimationsEnabled(false)
+            val values by collectValues(hideNotificationsFlow)
+
+            runCurrent()
+            updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+            runCurrent()
+            powerRepository.updateWakefulness(STARTING_TO_SLEEP)
+            runCurrent()
+
+            assertThat(values).containsExactly(true, false).inOrder()
+        }
+
+    @Test
+    fun displaySwitch_animationsEnabled_screenGoesToSleep_showsNotificationsBack() =
+        testScope.runTest {
+            givenAnimationsEnabled(true)
+            val values by collectValues(hideNotificationsFlow)
+
+            runCurrent()
+            updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+            runCurrent()
+            powerRepository.updateWakefulness(STARTING_TO_SLEEP)
+            runCurrent()
+
+            assertThat(values).containsExactly(true, false).inOrder()
+        }
+
+    @Test
+    fun displaySwitch_animationsEnabled_unfoldAnimationNotFinished_notificationsHidden() =
+        testScope.runTest {
+            givenAnimationsEnabled(true)
+            val values by collectValues(hideNotificationsFlow)
+
+            runCurrent()
+            updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+            runCurrent()
+
+            assertThat(values).containsExactly(true).inOrder()
+        }
+
+    @Test
+    fun displaySwitch_animationsEnabled_unfoldAnimationFinishes_showsNotificationsBack() =
+        testScope.runTest {
+            givenAnimationsEnabled(true)
+            val values by collectValues(hideNotificationsFlow)
+
+            runCurrent()
+            updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+            runCurrent()
+            unfoldTransitionProgressProvider.onTransitionFinished()
+            runCurrent()
+
+            assertThat(values).containsExactly(true, false).inOrder()
+        }
+
+    @Test
+    fun displaySwitch_noEvents_afterTimeout_showsNotificationsBack() =
+        testScope.runTest {
+            givenAnimationsEnabled(true)
+            val values by collectValues(hideNotificationsFlow)
+
+            runCurrent()
+            updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+            runCurrent()
+            advanceTimeBy(Duration.ofMillis(10_000).toMillis())
+
+            assertThat(values).containsExactly(true, false).inOrder()
+        }
+
+    @Test
+    fun displaySwitch_noEvents_beforeTimeout_doesNotShowNotifications() =
+        testScope.runTest {
+            givenAnimationsEnabled(true)
+            val values by collectValues(hideNotificationsFlow)
+
+            runCurrent()
+            updateDisplay(width = INITIAL_DISPLAY_WIDTH * 2)
+            runCurrent()
+            advanceTimeBy(Duration.ofMillis(500).toMillis())
+
+            assertThat(values).containsExactly(true).inOrder()
+        }
+
+    private val hideNotificationsFlow: Flow<Boolean>
+        get() = underTest.shouldHideNotifications
+
+    private fun updateDisplay(
+        width: Int = INITIAL_DISPLAY_WIDTH,
+        height: Int = INITIAL_DISPLAY_HEIGHT,
+        @Surface.Rotation rotation: Int = ROTATION_0
+    ) {
+        configuration.windowConfiguration.maxBounds.set(Rect(0, 0, width, height))
+        configuration.windowConfiguration.displayRotation = rotation
+
+        configurationController.onConfigurationChanged(configuration)
+    }
+
+    private fun givenAnimationsEnabled(enabled: Boolean) {
+        animationStatus.onAnimationStatusChanged(enabled)
+    }
+
+    private companion object {
+        private const val INITIAL_DISPLAY_WIDTH = 100
+        private const val INITIAL_DISPLAY_HEIGHT = 200
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractorTest.kt
new file mode 100644
index 0000000..6a801e0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/domain/interactor/UnfoldTransitionInteractorTest.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.unfold.domain.interactor
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.unfold.TestUnfoldTransitionProvider
+import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+open class UnfoldTransitionInteractorTest : SysuiTestCase() {
+
+    private val testScope = TestScope()
+
+    private val unfoldTransitionProgressProvider = TestUnfoldTransitionProvider()
+    private val unfoldTransitionRepository =
+        UnfoldTransitionRepositoryImpl(Optional.of(unfoldTransitionProgressProvider))
+
+    private lateinit var underTest: UnfoldTransitionInteractor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest = UnfoldTransitionInteractorImpl(unfoldTransitionRepository)
+    }
+
+    @Test
+    fun waitForTransitionFinish_noEvents_doesNotComplete() =
+        testScope.runTest {
+            val deferred = async { underTest.waitForTransitionFinish() }
+
+            runCurrent()
+
+            assertThat(deferred.isCompleted).isFalse()
+            deferred.cancel()
+        }
+
+    @Test
+    fun waitForTransitionFinish_finishEvent_completes() =
+        testScope.runTest {
+            val deferred = async { underTest.waitForTransitionFinish() }
+
+            runCurrent()
+            unfoldTransitionProgressProvider.onTransitionFinished()
+            runCurrent()
+
+            assertThat(deferred.isCompleted).isTrue()
+            deferred.cancel()
+        }
+
+    @Test
+    fun waitForTransitionFinish_otherEvent_doesNotComplete() =
+        testScope.runTest {
+            val deferred = async { underTest.waitForTransitionFinish() }
+
+            runCurrent()
+            unfoldTransitionProgressProvider.onTransitionStarted()
+            runCurrent()
+
+            assertThat(deferred.isCompleted).isFalse()
+            deferred.cancel()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/animation/FakeAnimationStatusRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/util/animation/FakeAnimationStatusRepository.kt
new file mode 100644
index 0000000..e72235c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/animation/FakeAnimationStatusRepository.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.animation
+
+import com.android.systemui.util.animation.data.repository.AnimationStatusRepository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+
+class FakeAnimationStatusRepository : AnimationStatusRepository {
+
+    // Replay 1 element as real repository always emits current status as a first element
+    private val animationsEnabled: MutableSharedFlow<Boolean> = MutableSharedFlow(replay = 1)
+
+    override fun areAnimationsEnabled(): Flow<Boolean> = animationsEnabled
+
+    fun onAnimationStatusChanged(enabled: Boolean) {
+        animationsEnabled.tryEmit(enabled)
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
index d0fa27e..6b38d6e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.common.ui.data.repository
 
+import android.content.res.Configuration
 import com.android.systemui.dagger.SysUISingleton
 import dagger.Binds
 import dagger.Module
@@ -36,6 +37,10 @@
         MutableSharedFlow<Unit>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
     override val onConfigurationChange: Flow<Unit> = _onConfigurationChange.asSharedFlow()
 
+    private val _configurationChangeValues = MutableSharedFlow<Configuration>()
+    override val configurationValues: Flow<Configuration> =
+        _configurationChangeValues.asSharedFlow()
+
     private val _scaleForResolution = MutableStateFlow(1f)
     override val scaleForResolution: Flow<Float> = _scaleForResolution.asStateFlow()
 
@@ -49,6 +54,11 @@
         _onConfigurationChange.tryEmit(Unit)
     }
 
+    fun onConfigurationChange(configChange: Configuration) {
+        _configurationChangeValues.tryEmit(configChange)
+        onAnyConfigurationChange()
+    }
+
     fun setScaleForResolution(scale: Float) {
         _scaleForResolution.value = scale
     }