Merge "Allow the NAS to set a type for notifications." into main
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index fbfe050..4dc801c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -602,10 +602,14 @@
     removeEnabled: Boolean,
     onRemoveClicked: () -> Unit,
     setToolbarSize: (toolbarSize: IntSize) -> Unit,
-    setRemoveButtonCoordinates: (coordinates: LayoutCoordinates) -> Unit,
+    setRemoveButtonCoordinates: (coordinates: LayoutCoordinates?) -> Unit,
     onOpenWidgetPicker: () -> Unit,
     onEditDone: () -> Unit
 ) {
+    if (!removeEnabled) {
+        // Clear any existing coordinates when remove is not enabled.
+        setRemoveButtonCoordinates(null)
+    }
     val removeButtonAlpha: Float by
         animateFloatAsState(
             targetValue = if (removeEnabled) 1f else 0.5f,
@@ -645,7 +649,13 @@
                 contentPadding = Dimensions.ButtonPadding,
                 modifier =
                     Modifier.graphicsLayer { alpha = removeButtonAlpha }
-                        .onGloballyPositioned { setRemoveButtonCoordinates(it) }
+                        .onGloballyPositioned {
+                            // It's possible for this callback to fire after remove has been
+                            // disabled. Check enabled state before setting.
+                            if (removeEnabled) {
+                                setRemoveButtonCoordinates(it)
+                            }
+                        }
             ) {
                 Row(
                     horizontalArrangement =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index bb17024..837c292a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -543,7 +543,8 @@
             _toScene = old._toScene,
             userActionDistanceScope = old.userActionDistanceScope,
             orientation = old.orientation,
-            isUpOrLeft = old.isUpOrLeft
+            isUpOrLeft = old.isUpOrLeft,
+            lastDistance = old.lastDistance,
         )
         .apply {
             _currentScene = old._currentScene
@@ -561,6 +562,7 @@
     val userActionDistanceScope: UserActionDistanceScope,
     override val orientation: Orientation,
     override val isUpOrLeft: Boolean,
+    var lastDistance: Float = DistanceUnspecified,
 ) :
     TransitionState.Transition(_fromScene.key, _toScene.key),
     TransitionState.HasOverscrollProperties {
@@ -620,8 +622,6 @@
                 get() = distance().absoluteValue
         }
 
-    private var lastDistance = DistanceUnspecified
-
     /** Whether [TransitionState.Transition.finish] was called on this transition. */
     var isFinishing = false
         private set
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index b925130..33063c8 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -461,7 +461,7 @@
     val transitionKey: TransitionKey? = null,
 )
 
-interface UserActionDistance {
+fun interface UserActionDistance {
     /**
      * Return the **absolute** distance of the user action given the size of the scene we are
      * animating from and the [orientation].
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index f532e2e..a6e52c2 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -1195,4 +1195,23 @@
         assertThat(transition).hasProgress(0f)
         assertThat(transition).hasOverscrollSpec()
     }
+
+    @Test
+    fun interceptingTransitionKeepsDistance() = runGestureTest {
+        var swipeDistance = 75f
+        layoutState.transitions = transitions {
+            from(SceneA, to = SceneB) { distance = UserActionDistance { _, _ -> swipeDistance } }
+        }
+
+        // Start transition.
+        val controller = onDragStarted(overSlop = -50f)
+        assertTransition(fromScene = SceneA, toScene = SceneB, progress = 50f / 75f)
+
+        // Intercept the transition and change the swipe distance. The original distance and
+        // progress should be the same.
+        swipeDistance = 50f
+        controller.onDragStopped(0f)
+        onDragStartedImmediately()
+        assertTransition(fromScene = SceneA, toScene = SceneB, progress = 50f / 75f)
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/StatusBarStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/StatusBarStartableTest.kt
new file mode 100644
index 0000000..9601f20
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/StatusBarStartableTest.kt
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.scene.domain.startable
+
+import android.app.StatusBarManager
+import android.provider.DeviceConfig
+import android.view.WindowManagerPolicyConstants
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.SceneKey
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
+import com.android.internal.statusbar.statusBarService
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.navigationbar.NavigationModeController
+import com.android.systemui.navigationbar.navigationModeController
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.scene.data.repository.setSceneTransition
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+import com.android.systemui.testKosmos
+import com.android.systemui.util.fakeDeviceConfigProxy
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlin.reflect.full.memberProperties
+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.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import platform.test.runner.parameterized.Parameter
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+@EnableSceneContainer
+class StatusBarStartableTest : SysuiTestCase() {
+
+    companion object {
+        @Parameters(name = "{0}")
+        @JvmStatic
+        fun testSpecs(): List<TestSpec> {
+            return listOf(
+                TestSpec(
+                    id = 0,
+                    expectedFlags = StatusBarManager.DISABLE_NONE,
+                    Preconditions(
+                        isForceHideHomeAndRecents = false,
+                        isKeyguardShowing = false,
+                        isPowerGestureIntercepted = false,
+                    ),
+                ),
+                TestSpec(
+                    id = 1,
+                    expectedFlags = StatusBarManager.DISABLE_NONE,
+                    Preconditions(
+                        isForceHideHomeAndRecents = false,
+                        isKeyguardShowing = true,
+                        isOccluded = true,
+                        isPowerGestureIntercepted = false,
+                    ),
+                ),
+                TestSpec(
+                    id = 2,
+                    expectedFlags = StatusBarManager.DISABLE_NONE,
+                    Preconditions(
+                        isForceHideHomeAndRecents = false,
+                        isKeyguardShowing = false,
+                        isPowerGestureIntercepted = true,
+                        isOccluded = false,
+                    ),
+                ),
+                TestSpec(
+                    id = 3,
+                    expectedFlags = StatusBarManager.DISABLE_NONE,
+                    Preconditions(
+                        isForceHideHomeAndRecents = false,
+                        isKeyguardShowing = true,
+                        isOccluded = true,
+                        isPowerGestureIntercepted = true,
+                        isAuthenticationMethodSecure = false,
+                    ),
+                ),
+                TestSpec(
+                    id = 4,
+                    expectedFlags = StatusBarManager.DISABLE_NONE,
+                    Preconditions(
+                        isForceHideHomeAndRecents = false,
+                        isKeyguardShowing = true,
+                        isOccluded = true,
+                        isPowerGestureIntercepted = true,
+                        isAuthenticationMethodSecure = true,
+                        isFaceEnrolledAndEnabled = false,
+                    ),
+                ),
+                TestSpec(
+                    id = 5,
+                    expectedFlags = StatusBarManager.DISABLE_RECENT,
+                    Preconditions(
+                        isForceHideHomeAndRecents = false,
+                        isKeyguardShowing = true,
+                        isOccluded = true,
+                        isPowerGestureIntercepted = true,
+                        isAuthenticationMethodSecure = true,
+                        isFaceEnrolledAndEnabled = true,
+                    ),
+                ),
+                TestSpec(
+                    id = 6,
+                    expectedFlags = StatusBarManager.DISABLE_RECENT,
+                    Preconditions(
+                        isForceHideHomeAndRecents = true,
+                        isShowHomeOverLockscreen = true,
+                        isGesturalMode = true,
+                        isPowerGestureIntercepted = false,
+                    ),
+                ),
+                TestSpec(
+                    id = 7,
+                    expectedFlags = StatusBarManager.DISABLE_RECENT,
+                    Preconditions(
+                        isForceHideHomeAndRecents = false,
+                        isKeyguardShowing = true,
+                        isOccluded = false,
+                        isShowHomeOverLockscreen = true,
+                        isGesturalMode = true,
+                        isPowerGestureIntercepted = false,
+                    ),
+                ),
+                TestSpec(
+                    id = 8,
+                    expectedFlags =
+                        StatusBarManager.DISABLE_RECENT or StatusBarManager.DISABLE_HOME,
+                    Preconditions(
+                        isForceHideHomeAndRecents = true,
+                        isShowHomeOverLockscreen = true,
+                        isGesturalMode = false,
+                        isPowerGestureIntercepted = false,
+                    ),
+                ),
+                TestSpec(
+                    id = 9,
+                    expectedFlags =
+                        StatusBarManager.DISABLE_RECENT or StatusBarManager.DISABLE_HOME,
+                    Preconditions(
+                        isForceHideHomeAndRecents = false,
+                        isKeyguardShowing = true,
+                        isOccluded = false,
+                        isShowHomeOverLockscreen = false,
+                        isPowerGestureIntercepted = false,
+                    ),
+                ),
+            )
+        }
+
+        @BeforeClass
+        @JvmStatic
+        fun setUpClass() {
+            val seenIds = mutableSetOf<Int>()
+            testSpecs().forEach { testSpec ->
+                assertWithMessage("Duplicate TestSpec id=${testSpec.id}")
+                    .that(seenIds)
+                    .doesNotContain(testSpec.id)
+                seenIds.add(testSpec.id)
+            }
+        }
+    }
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val statusBarServiceMock = kosmos.statusBarService
+    private val flagsCaptor = argumentCaptor<Int>()
+
+    private val navigationModeControllerMock = kosmos.navigationModeController
+    private var currentNavigationMode = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON
+        set(value) {
+            field = value
+            modeChangedListeners.forEach { listener -> listener.onNavigationModeChanged(field) }
+        }
+
+    private val modeChangedListeners = mutableListOf<NavigationModeController.ModeChangedListener>()
+
+    private val underTest = kosmos.statusBarStartable
+
+    @JvmField @Parameter(0) var testSpec: TestSpec? = null
+
+    @Before
+    fun setUp() {
+        whenever(navigationModeControllerMock.addListener(any())).thenAnswer { invocation ->
+            val listener = invocation.arguments[0] as NavigationModeController.ModeChangedListener
+            modeChangedListeners.add(listener)
+            currentNavigationMode
+        }
+
+        underTest.start()
+    }
+
+    @Test
+    fun test() =
+        testScope.runTest {
+            val preconditions = checkNotNull(testSpec).preconditions
+            preconditions.assertValid()
+
+            setUpWith(preconditions)
+
+            runCurrent()
+
+            verify(statusBarServiceMock, atLeastOnce())
+                .disableForUser(flagsCaptor.capture(), any(), any(), anyInt())
+            assertThat(flagsCaptor.lastValue).isEqualTo(checkNotNull(testSpec).expectedFlags)
+        }
+
+    /** Sets up the state to match what's specified in the given [preconditions]. */
+    private fun TestScope.setUpWith(
+        preconditions: Preconditions,
+    ) {
+        if (!preconditions.isKeyguardShowing) {
+            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+                SuccessFingerprintAuthenticationStatus(0, true)
+            )
+        }
+        if (preconditions.isForceHideHomeAndRecents) {
+            whenIdle(Scenes.Bouncer)
+        } else if (preconditions.isKeyguardShowing) {
+            whenIdle(Scenes.Lockscreen)
+        } else {
+            whenIdle(Scenes.Gone)
+        }
+        runCurrent()
+
+        kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
+            showWhenLockedActivityOnTop = preconditions.isOccluded,
+            taskInfo = if (preconditions.isOccluded) mock() else null,
+        )
+
+        kosmos.fakeDeviceConfigProxy.setProperty(
+            DeviceConfig.NAMESPACE_SYSTEMUI,
+            SystemUiDeviceConfigFlags.NAV_BAR_HANDLE_SHOW_OVER_LOCKSCREEN,
+            preconditions.isShowHomeOverLockscreen.toString(),
+            /* makeDefault= */ false,
+        )
+        kosmos.fakeExecutor.runAllReady()
+
+        currentNavigationMode =
+            if (preconditions.isGesturalMode) {
+                WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL
+            } else {
+                WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON
+            }
+
+        kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+            if (preconditions.isAuthenticationMethodSecure) {
+                AuthenticationMethodModel.Pin
+            } else {
+                AuthenticationMethodModel.None
+            }
+        )
+
+        kosmos.fakePowerRepository.updateWakefulness(
+            rawState =
+                if (preconditions.isPowerGestureIntercepted) WakefulnessState.AWAKE
+                else WakefulnessState.ASLEEP,
+            lastWakeReason = WakeSleepReason.POWER_BUTTON,
+            lastSleepReason = WakeSleepReason.POWER_BUTTON,
+            powerButtonLaunchGestureTriggered = preconditions.isPowerGestureIntercepted,
+        )
+
+        kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(
+            preconditions.isFaceEnrolledAndEnabled
+        )
+
+        runCurrent()
+    }
+
+    /** Sets up an idle state on the given [on] scene. */
+    private fun whenIdle(on: SceneKey) {
+        kosmos.setSceneTransition(ObservableTransitionState.Idle(on))
+        kosmos.sceneInteractor.changeScene(on, "")
+    }
+
+    data class Preconditions(
+        val isForceHideHomeAndRecents: Boolean = false,
+        val isKeyguardShowing: Boolean = true,
+        val isOccluded: Boolean = false,
+        val isPowerGestureIntercepted: Boolean = false,
+        val isShowHomeOverLockscreen: Boolean = false,
+        val isGesturalMode: Boolean = true,
+        val isAuthenticationMethodSecure: Boolean = true,
+        val isFaceEnrolledAndEnabled: Boolean = false,
+    ) {
+        override fun toString(): String {
+            // Only include values set to true:
+            return buildString {
+                append("(")
+                append(
+                    Preconditions::class
+                        .memberProperties
+                        .filter { it.get(this@Preconditions) == true }
+                        .joinToString(", ") { "${it.name}=true" }
+                )
+                append(")")
+            }
+        }
+
+        fun assertValid() {
+            assertWithMessage(
+                    "isForceHideHomeAndRecents means that the bouncer is showing so keyguard must" +
+                        " be showing"
+                )
+                .that(!isForceHideHomeAndRecents || isKeyguardShowing)
+                .isTrue()
+            assertWithMessage("Cannot be occluded if the keyguard isn't showing")
+                .that(!isOccluded || isKeyguardShowing)
+                .isTrue()
+        }
+    }
+
+    data class TestSpec(
+        val id: Int,
+        val expectedFlags: Int,
+        val preconditions: Preconditions,
+    ) {
+        override fun toString(): String {
+            return "id=$id, expected=$expectedFlags, preconditions=$preconditions"
+        }
+    }
+}
diff --git a/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml b/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml
index cf9ca15..c9850f2 100644
--- a/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml
+++ b/packages/SystemUI/res-keyguard/layout/alternate_bouncer.xml
@@ -19,8 +19,6 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:sysui="http://schemas.android.com/apk/res-auto"
     android:id="@+id/alternate_bouncer"
-    android:focusable="true"
-    android:clickable="true"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
diff --git a/packages/SystemUI/res/drawable/placeholder_touchpad_back_gesture.png b/packages/SystemUI/res/drawable/placeholder_touchpad_back_gesture.png
new file mode 100644
index 0000000..526b585
--- /dev/null
+++ b/packages/SystemUI/res/drawable/placeholder_touchpad_back_gesture.png
Binary files differ
diff --git a/packages/SystemUI/res/drawable/placeholder_touchpad_tablet_back_gesture.png b/packages/SystemUI/res/drawable/placeholder_touchpad_tablet_back_gesture.png
new file mode 100644
index 0000000..cba2d20
--- /dev/null
+++ b/packages/SystemUI/res/drawable/placeholder_touchpad_tablet_back_gesture.png
Binary files differ
diff --git a/packages/SystemUI/res/layout/sidefps_view.xml b/packages/SystemUI/res/layout/sidefps_view.xml
index fc4bf8a..e80ed26 100644
--- a/packages/SystemUI/res/layout/sidefps_view.xml
+++ b/packages/SystemUI/res/layout/sidefps_view.xml
@@ -22,5 +22,4 @@
     android:layout_height="wrap_content"
     app:lottie_autoPlay="true"
     app:lottie_loop="true"
-    app:lottie_rawRes="@raw/sfps_pulse"
-    android:importantForAccessibility="no"/>
\ No newline at end of file
+    app:lottie_rawRes="@raw/sfps_pulse"/>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 8381812..82dafc3 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3577,6 +3577,12 @@
     <string name="touchpad_tutorial_action_key_button">Action key</string>
     <!-- Label for button finishing touchpad tutorial [CHAR LIMIT=NONE] -->
     <string name="touchpad_tutorial_done_button">Done</string>
+    <!-- Touchpad back gesture action name in tutorial [CHAR LIMIT=NONE] -->
+    <string name="touchpad_back_gesture_action_title">Go back</string>
+    <!-- Touchpad back gesture guidance in gestures tutorial [CHAR LIMIT=NONE] -->
+    <string name="touchpad_back_gesture_guidance">To go back, swipe left or right using three fingers anywhere on the touchpad.</string>
+    <string name="touchpad_back_gesture_animation_content_description">Touchpad showing three fingers moving right and left</string>
+    <string name="touchpad_back_gesture_screen_animation_content_description">Device screen showing animation for back gesture</string>
 
     <!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] -->
     <string name="keyboard_backlight_dialog_title">Keyboard backlight</string>
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
index 9cc4650..9578da4 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
@@ -139,6 +139,11 @@
         overlayView!!.visibility = View.INVISIBLE
         Log.d(TAG, "show(): adding overlayView $overlayView")
         windowManager.get().addView(overlayView, overlayViewModel.defaultOverlayViewParams)
+        overlayView!!.announceForAccessibility(
+            applicationContext.resources.getString(
+                R.string.accessibility_side_fingerprint_indicator_label
+            )
+        )
     }
 
     /** Hide the side fingerprint sensor indicator */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 2d60fcc..b70dbe2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -151,6 +151,7 @@
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.shade.ShadeExpansionStateManager;
@@ -178,6 +179,8 @@
 
 import dagger.Lazy;
 
+import kotlinx.coroutines.CoroutineDispatcher;
+
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -187,8 +190,6 @@
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
-import kotlinx.coroutines.CoroutineDispatcher;
-
 /**
  * Mediates requests related to the keyguard.  This includes queries about the
  * state of the keyguard, power management events that effect whether the keyguard
@@ -3502,12 +3503,14 @@
                         +  " --> flags=0x" + Integer.toHexString(flags));
             }
 
-            try {
-                mStatusBarService.disableForUser(flags, mStatusBarDisableToken,
-                        mContext.getPackageName(),
-                        mSelectedUserInteractor.getSelectedUserId(true));
-            } catch (RemoteException e) {
-                Log.d(TAG, "Failed to set disable flags: " + flags, e);
+            if (!SceneContainerFlag.isEnabled()) {
+                try {
+                    mStatusBarService.disableForUser(flags, mStatusBarDisableToken,
+                            mContext.getPackageName(),
+                            mSelectedUserInteractor.getSelectedUserId(true));
+                } catch (RemoteException e) {
+                    Log.d(TAG, "Failed to set disable flags: " + flags, e);
+                }
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
index 6550937..f8063c9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
@@ -86,7 +86,10 @@
                     privateFlags =
                         WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY or
                             WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
+                    // Avoid announcing window title.
+                    accessibilityTitle = " "
                 }
+
     private var alternateBouncerView: ConstraintLayout? = null
 
     override fun start() {
@@ -304,6 +307,7 @@
             }
         }
     }
+
     companion object {
         private const val TAG = "AlternateBouncerViewBinder"
         private const val swipeTag = "AlternateBouncer-SWIPE"
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index 323ca87..08462d7 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.scene.domain.resolver.QuickSettingsSceneFamilyResolverModule
 import com.android.systemui.scene.domain.startable.SceneContainerStartable
 import com.android.systemui.scene.domain.startable.ScrimStartable
+import com.android.systemui.scene.domain.startable.StatusBarStartable
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.shared.flag.DualShade
@@ -66,6 +67,11 @@
 
     @Binds
     @IntoMap
+    @ClassKey(StatusBarStartable::class)
+    fun statusBarStartable(impl: StatusBarStartable): CoreStartable
+
+    @Binds
+    @IntoMap
     @ClassKey(WindowRootViewVisibilityInteractor::class)
     fun bindWindowRootViewVisibilityInteractor(
         impl: WindowRootViewVisibilityInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index 4691eba..17dc9a5 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.scene.domain.resolver.QuickSettingsSceneFamilyResolverModule
 import com.android.systemui.scene.domain.startable.SceneContainerStartable
 import com.android.systemui.scene.domain.startable.ScrimStartable
+import com.android.systemui.scene.domain.startable.StatusBarStartable
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.shared.flag.DualShade
@@ -72,6 +73,11 @@
 
     @Binds
     @IntoMap
+    @ClassKey(StatusBarStartable::class)
+    fun statusBarStartable(impl: StatusBarStartable): CoreStartable
+
+    @Binds
+    @IntoMap
     @ClassKey(WindowRootViewVisibilityInteractor::class)
     fun bindWindowRootViewVisibilityInteractor(
         impl: WindowRootViewVisibilityInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/StatusBarStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/StatusBarStartable.kt
new file mode 100644
index 0000000..893f030
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/StatusBarStartable.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.scene.domain.startable
+
+import android.annotation.SuppressLint
+import android.app.StatusBarManager
+import android.content.Context
+import android.os.Binder
+import android.os.IBinder
+import android.os.RemoteException
+import android.provider.DeviceConfig
+import android.util.Log
+import com.android.compose.animation.scene.SceneKey
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
+import com.android.internal.statusbar.IStatusBarService
+import com.android.systemui.CoreStartable
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.deviceconfig.domain.interactor.DeviceConfigInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.navigation.domain.interactor.NavigationInteractor
+import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessModel
+import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class StatusBarStartable
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    @Application private val applicationContext: Context,
+    private val selectedUserInteractor: SelectedUserInteractor,
+    private val sceneInteractor: SceneInteractor,
+    private val deviceEntryInteractor: DeviceEntryInteractor,
+    private val sceneContainerOcclusionInteractor: SceneContainerOcclusionInteractor,
+    private val deviceConfigInteractor: DeviceConfigInteractor,
+    private val navigationInteractor: NavigationInteractor,
+    private val authenticationInteractor: AuthenticationInteractor,
+    private val powerInteractor: PowerInteractor,
+    private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
+    private val statusBarService: IStatusBarService,
+) : CoreStartable {
+
+    private val disableToken: IBinder = Binder()
+
+    override fun start() {
+        if (!SceneContainerFlag.isEnabled) {
+            return
+        }
+
+        applicationScope.launch {
+            combine(
+                    selectedUserInteractor.selectedUser,
+                    sceneInteractor.currentScene,
+                    deviceEntryInteractor.isDeviceEntered,
+                    sceneContainerOcclusionInteractor.invisibleDueToOcclusion,
+                    deviceConfigInteractor.property(
+                        namespace = DeviceConfig.NAMESPACE_SYSTEMUI,
+                        name = SystemUiDeviceConfigFlags.NAV_BAR_HANDLE_SHOW_OVER_LOCKSCREEN,
+                        default = true,
+                    ),
+                    navigationInteractor.isGesturalMode,
+                    authenticationInteractor.authenticationMethod,
+                    powerInteractor.detailedWakefulness,
+                ) { values ->
+                    val selectedUserId = values[0] as Int
+                    val currentScene = values[1] as SceneKey
+                    val isDeviceEntered = values[2] as Boolean
+                    val isOccluded = values[3] as Boolean
+                    val isShowHomeOverLockscreen = values[4] as Boolean
+                    val isGesturalMode = values[5] as Boolean
+                    val authenticationMethod = values[6] as AuthenticationMethodModel
+                    val wakefulnessModel = values[7] as WakefulnessModel
+
+                    val isForceHideHomeAndRecents = currentScene == Scenes.Bouncer
+                    val isKeyguardShowing = !isDeviceEntered
+                    val isPowerGestureIntercepted =
+                        with(wakefulnessModel) {
+                            isAwake() &&
+                                powerButtonLaunchGestureTriggered &&
+                                lastSleepReason == WakeSleepReason.POWER_BUTTON
+                        }
+
+                    var flags = StatusBarManager.DISABLE_NONE
+
+                    if (isForceHideHomeAndRecents || (isKeyguardShowing && !isOccluded)) {
+                        if (!isShowHomeOverLockscreen || !isGesturalMode) {
+                            flags = flags or StatusBarManager.DISABLE_HOME
+                        }
+                        flags = flags or StatusBarManager.DISABLE_RECENT
+                    }
+
+                    if (
+                        isPowerGestureIntercepted &&
+                            isOccluded &&
+                            authenticationMethod.isSecure &&
+                            deviceEntryFaceAuthInteractor.isFaceAuthEnabledAndEnrolled()
+                    ) {
+                        flags = flags or StatusBarManager.DISABLE_RECENT
+                    }
+
+                    selectedUserId to flags
+                }
+                .distinctUntilChanged()
+                .collect { (selectedUserId, flags) ->
+                    @SuppressLint("WrongConstant", "NonInjectedService")
+                    if (applicationContext.getSystemService(Context.STATUS_BAR_SERVICE) == null) {
+                        Log.w(TAG, "Could not get status bar manager")
+                        return@collect
+                    }
+
+                    withContext(backgroundDispatcher) {
+                        try {
+                            statusBarService.disableForUser(
+                                flags,
+                                disableToken,
+                                applicationContext.packageName,
+                                selectedUserId,
+                            )
+                        } catch (e: RemoteException) {
+                            Log.d(TAG, "Failed to set disable flags: $flags", e)
+                        }
+                    }
+                }
+        }
+    }
+
+    override fun onBootCompleted() {
+        applicationScope.launch(backgroundDispatcher) {
+            try {
+                statusBarService.disableForUser(
+                    StatusBarManager.DISABLE_NONE,
+                    disableToken,
+                    applicationContext.packageName,
+                    selectedUserInteractor.getSelectedUserId(true),
+                )
+            } catch (e: RemoteException) {
+                Log.d(TAG, "Failed to clear flags", e)
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "StatusBarStartable"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/BackGestureTutorialScreen.kt
new file mode 100644
index 0000000..2460761c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/BackGestureTutorialScreen.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 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.touchpad.tutorial.ui.view
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.android.systemui.res.R
+
+@Composable
+fun BackGestureTutorialScreen(
+    onDoneButtonClicked: () -> Unit,
+    onBack: () -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    BackHandler { onBack() }
+    Column(
+        verticalArrangement = Arrangement.Center,
+        modifier =
+            modifier
+                .background(color = MaterialTheme.colorScheme.surfaceContainer)
+                .padding(start = 48.dp, top = 124.dp, end = 48.dp, bottom = 48.dp)
+                .fillMaxSize()
+    ) {
+        Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
+            TutorialDescription(modifier = Modifier.weight(1f))
+            Spacer(modifier = Modifier.width(76.dp))
+            TutorialAnimation(modifier = Modifier.weight(1f).padding(top = 24.dp))
+        }
+        DoneButton(onDoneButtonClicked = onDoneButtonClicked)
+    }
+}
+
+@Composable
+fun TutorialDescription(modifier: Modifier = Modifier) {
+    Column(verticalArrangement = Arrangement.Top, modifier = modifier) {
+        Text(
+            text = stringResource(id = R.string.touchpad_back_gesture_action_title),
+            style = MaterialTheme.typography.displayLarge
+        )
+        Spacer(modifier = Modifier.height(16.dp))
+        Text(
+            text = stringResource(id = R.string.touchpad_back_gesture_guidance),
+            style = MaterialTheme.typography.bodyLarge
+        )
+    }
+}
+
+@Composable
+fun TutorialAnimation(modifier: Modifier = Modifier) {
+    // below are just placeholder images, will be substituted by animations soon
+    Column(modifier = modifier.fillMaxWidth()) {
+        Image(
+            painter = painterResource(id = R.drawable.placeholder_touchpad_tablet_back_gesture),
+            contentDescription =
+                stringResource(
+                    id = R.string.touchpad_back_gesture_screen_animation_content_description
+                ),
+            modifier = Modifier.fillMaxWidth()
+        )
+        Spacer(modifier = Modifier.height(24.dp))
+        Image(
+            painter = painterResource(id = R.drawable.placeholder_touchpad_back_gesture),
+            contentDescription =
+                stringResource(id = R.string.touchpad_back_gesture_animation_content_description),
+            modifier = Modifier.fillMaxWidth()
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
index b7629c7..bbd50ef 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
@@ -27,7 +27,6 @@
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.viewmodel.compose.viewModel
 import com.android.compose.theme.PlatformTheme
-import com.android.systemui.touchpad.tutorial.ui.BackGestureTutorialViewModel
 import com.android.systemui.touchpad.tutorial.ui.GestureViewModelFactory
 import com.android.systemui.touchpad.tutorial.ui.HomeGestureTutorialViewModel
 import com.android.systemui.touchpad.tutorial.ui.Screen.BACK_GESTURE
@@ -63,17 +62,16 @@
                 onActionKeyTutorialClicked = {},
                 onDoneButtonClicked = closeTutorial
             )
-        BACK_GESTURE -> BackGestureTutorialScreen()
+        BACK_GESTURE ->
+            BackGestureTutorialScreen(
+                onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) },
+                onBack = { vm.goTo(TUTORIAL_SELECTION) }
+            )
         HOME_GESTURE -> HomeGestureTutorialScreen()
     }
 }
 
 @Composable
-fun BackGestureTutorialScreen() {
-    val vm = viewModel<BackGestureTutorialViewModel>(factory = GestureViewModelFactory())
-}
-
-@Composable
 fun HomeGestureTutorialScreen() {
     val vm = viewModel<HomeGestureTutorialViewModel>(factory = GestureViewModelFactory())
 }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TutorialComponents.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TutorialComponents.kt
new file mode 100644
index 0000000..16fa91d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TutorialComponents.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 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.touchpad.tutorial.ui.view
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.android.systemui.res.R
+
+@Composable
+fun DoneButton(onDoneButtonClicked: () -> Unit, modifier: Modifier = Modifier) {
+    Row(
+        horizontalArrangement = Arrangement.End,
+        verticalAlignment = Alignment.CenterVertically,
+        modifier = modifier.fillMaxWidth()
+    ) {
+        Button(onClick = onDoneButtonClicked) {
+            Text(stringResource(R.string.touchpad_tutorial_done_button))
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TutorialSelectionScreen.kt
index 532eb1b..877bbe1 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TutorialSelectionScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TutorialSelectionScreen.kt
@@ -22,7 +22,6 @@
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.Button
@@ -114,16 +113,3 @@
         Text(text = text, style = MaterialTheme.typography.headlineLarge)
     }
 }
-
-@Composable
-private fun DoneButton(onDoneButtonClicked: () -> Unit, modifier: Modifier = Modifier) {
-    Row(
-        horizontalArrangement = Arrangement.End,
-        verticalAlignment = Alignment.CenterVertically,
-        modifier = modifier.fillMaxWidth()
-    ) {
-        Button(onClick = onDoneButtonClicked) {
-            Text(stringResource(R.string.touchpad_tutorial_done_button))
-        }
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
index 4238254..7fa165c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
@@ -73,6 +73,7 @@
 import org.mockito.Mockito.`when`
 import org.mockito.junit.MockitoJUnit
 import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.argumentCaptor
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -218,6 +219,13 @@
 
             verify(kosmos.windowManager).addView(any(), any())
 
+            var viewCaptor = argumentCaptor<View>()
+            verify(kosmos.windowManager).addView(viewCaptor.capture(), any())
+            verify(viewCaptor.firstValue)
+                .announceForAccessibility(
+                    mContext.getText(R.string.accessibility_side_fingerprint_indicator_label)
+                )
+
             // Hide alternate bouncer
             kosmos.keyguardBouncerRepository.setAlternateVisible(false)
             runCurrent()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/StatusBarStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/StatusBarStartableKosmos.kt
new file mode 100644
index 0000000..ee69c30
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/StatusBarStartableKosmos.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.scene.domain.startable
+
+import android.content.applicationContext
+import com.android.internal.statusbar.statusBarService
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.deviceconfig.domain.interactor.deviceConfigInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.navigation.domain.interactor.navigationInteractor
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.statusBarStartable by Fixture {
+    StatusBarStartable(
+        applicationScope = applicationCoroutineScope,
+        backgroundDispatcher = testDispatcher,
+        applicationContext = applicationContext,
+        selectedUserInteractor = selectedUserInteractor,
+        sceneInteractor = sceneInteractor,
+        deviceEntryInteractor = deviceEntryInteractor,
+        sceneContainerOcclusionInteractor = sceneContainerOcclusionInteractor,
+        deviceConfigInteractor = deviceConfigInteractor,
+        navigationInteractor = navigationInteractor,
+        authenticationInteractor = authenticationInteractor,
+        powerInteractor = powerInteractor,
+        deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
+        statusBarService = statusBarService,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index 1f2ecb7..ed335f9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -39,7 +39,7 @@
         // User id to represent a non system (human) user id. We presume this is the main user.
         const val MAIN_USER_ID = 10
 
-        private const val DEFAULT_SELECTED_USER = 0
+        const val DEFAULT_SELECTED_USER = 0
         private val DEFAULT_SELECTED_USER_INFO =
             UserInfo(
                 /* id= */ DEFAULT_SELECTED_USER,
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index 5567707..d7649dc 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -17,6 +17,7 @@
 package com.android.server.appwidget;
 
 import static android.appwidget.flags.Flags.removeAppWidgetServiceIoFromCriticalPath;
+import static android.appwidget.flags.Flags.supportResumeRestoreAfterReboot;
 import static android.content.Context.KEYGUARD_SERVICE;
 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
@@ -166,6 +167,8 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
 import java.util.function.LongSupplier;
 
 class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBackupProvider,
@@ -457,7 +460,7 @@
                 break;
             case Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE:
                 added = true;
-                // Follow through
+                // fall through
             case Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE:
                 pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
                 break;
@@ -3571,6 +3574,13 @@
             }
 
             out.endTag(null, "gs");
+
+            if (supportResumeRestoreAfterReboot()
+                    && mBackupRestoreController.requiresPersistenceLocked()) {
+                AppWidgetXmlUtil.writeBackupRestoreControllerState(
+                        out, mBackupRestoreController.getStateLocked(userId));
+            }
+
             out.endDocument();
             return true;
         } catch (IOException e) {
@@ -3717,6 +3727,32 @@
                         LoadedWidgetState loadedWidgets = new LoadedWidgetState(widget,
                                 hostTag, providerTag);
                         outLoadedWidgets.add(loadedWidgets);
+                    } else if (supportResumeRestoreAfterReboot()
+                            && AppWidgetXmlUtil.TAG_BACKUP_RESTORE_CONTROLLER_STATE.equals(tag)) {
+                        final BackupRestoreController.State s =
+                                AppWidgetXmlUtil.readBackupRestoreControllerState(parser);
+                        if (s == null) {
+                            continue;
+                        }
+                        final Set<String> prunedAppsInFile = s.getPrunedApps();
+                        if (prunedAppsInFile != null) {
+                            final Set<String> prunedAppsInMemory = mBackupRestoreController
+                                    .mPrunedAppsPerUser.get(userId);
+                            if (prunedAppsInMemory == null) {
+                                mBackupRestoreController.mPrunedAppsPerUser.put(
+                                        userId, prunedAppsInFile);
+                            } else {
+                                prunedAppsInMemory.addAll(prunedAppsInFile);
+                            }
+                        }
+                        loadUpdateRecords(s.getUpdatesByProvider(),
+                                this::findProviderByTag,
+                                mBackupRestoreController.mUpdatesByProvider::get,
+                                mBackupRestoreController.mUpdatesByProvider::put);
+                        loadUpdateRecords(s.getUpdatesByHost(),
+                                this::findHostByTag,
+                                mBackupRestoreController.mUpdatesByHost::get,
+                                mBackupRestoreController.mUpdatesByHost::put);
                     }
                 }
             } while (type != XmlPullParser.END_DOCUMENT);
@@ -3732,6 +3768,36 @@
         return version;
     }
 
+    private <T> void loadUpdateRecords(
+            @Nullable final SparseArray<
+                    List<BackupRestoreController.RestoreUpdateRecord>> updatesOnFile,
+            @NonNull final Function<Integer, T> findKeyByTagCb,
+            @NonNull final Function<T, List<
+                    BackupRestoreController.RestoreUpdateRecord>> findRecordsCb,
+            @NonNull final BiConsumer<T, List<
+                    BackupRestoreController.RestoreUpdateRecord>> newRecordsCb) {
+        if (updatesOnFile == null) {
+            return;
+        }
+        for (int i = 0; i < updatesOnFile.size(); i++) {
+            final int tag = updatesOnFile.keyAt(i);
+            final List<
+                    BackupRestoreController.RestoreUpdateRecord
+                    > recordsOnFile = updatesOnFile.get(tag);
+            if (recordsOnFile == null || recordsOnFile.isEmpty()) {
+                continue;
+            }
+            final T key = findKeyByTagCb.apply(tag);
+            final List<BackupRestoreController.RestoreUpdateRecord> recordsInMemory =
+                    findRecordsCb.apply(key);
+            if (recordsInMemory != null) {
+                recordsInMemory.addAll(recordsOnFile);
+            } else  {
+                newRecordsCb.accept(key, recordsOnFile);
+            }
+        }
+    }
+
     private void performUpgradeLocked(int fromVersion) {
         if (fromVersion < CURRENT_VERSION) {
             Slog.v(TAG, "Upgrading widget database from " + fromVersion + " to "
@@ -4674,7 +4740,7 @@
         }
     }
 
-    private static final class Provider {
+    static final class Provider {
 
         ProviderId id;
         AppWidgetProviderInfo info;
@@ -4931,7 +4997,7 @@
         }
     }
 
-    private static final class Host {
+    static final class Host {
         HostId id;
         ArrayList<Widget> widgets = new ArrayList<>();
         IAppWidgetHost callbacks;
@@ -5250,10 +5316,10 @@
     /**
      * This class encapsulates the backup and restore logic for a user group state.
      */
-    private final class BackupRestoreController {
+    final class BackupRestoreController {
         private static final String TAG = "BackupRestoreController";
 
-        private static final boolean DEBUG = true;
+        private static final boolean DEBUG = AppWidgetServiceImpl.DEBUG;
 
         // Version of backed-up widget state.
         private static final int WIDGET_STATE_VERSION = 2;
@@ -5262,16 +5328,31 @@
         // a given package.  Keep track of what we've done so far here; the list is
         // cleared at the start of every system restore pass, but preserved through
         // any install-time restore operations.
+        @GuardedBy("AppWidgetServiceImpl.this.mLock")
         private final SparseArray<Set<String>> mPrunedAppsPerUser = new SparseArray<>();
 
-        private final HashMap<Provider, ArrayList<RestoreUpdateRecord>> mUpdatesByProvider =
-                new HashMap<>();
-        private final HashMap<Host, ArrayList<RestoreUpdateRecord>> mUpdatesByHost =
-                new HashMap<>();
+        @GuardedBy("AppWidgetServiceImpl.this.mLock")
+        final Map<Provider, List<RestoreUpdateRecord>> mUpdatesByProvider =
+                new ArrayMap<>();
 
-        @GuardedBy("mLock")
+        @GuardedBy("AppWidgetServiceImpl.this.mLock")
+        private final Map<Host, List<RestoreUpdateRecord>> mUpdatesByHost =
+                new ArrayMap<>();
+
+        @GuardedBy("AppWidgetServiceImpl.this.mLock")
         private boolean mHasSystemRestoreFinished;
 
+        @GuardedBy("AppWidgetServiceImpl.this.mLock")
+        public boolean requiresPersistenceLocked() {
+            if (mHasSystemRestoreFinished) {
+                // No need to persist intermediate states if system restore is already finished.
+                return false;
+            }
+            // If either of the internal states is non-empty, then we need to persist that
+            return !(mPrunedAppsPerUser.size() == 0 && mUpdatesByProvider.isEmpty()
+                    && mUpdatesByHost.isEmpty());
+        }
+
         public List<String> getWidgetParticipants(int userId) {
             if (DEBUG) {
                 Slog.i(TAG, "Getting widget participants for user: " + userId);
@@ -5436,7 +5517,7 @@
                                 // If there's no live entry for this provider, add an inactive one
                                 // so that widget IDs referring to them can be properly allocated
 
-                                // Backup and resotre only for the parent profile.
+                                // Backup and restore only for the parent profile.
                                 ComponentName componentName = new ComponentName(pkg, cl);
 
                                 Provider p = findProviderLocked(componentName, userId);
@@ -5579,9 +5660,9 @@
 
             final UserHandle userHandle = new UserHandle(userId);
             // Build the providers' broadcasts and send them off
-            Set<Map.Entry<Provider, ArrayList<RestoreUpdateRecord>>> providerEntries
+            Set<Map.Entry<Provider, List<RestoreUpdateRecord>>> providerEntries
                     = mUpdatesByProvider.entrySet();
-            for (Map.Entry<Provider, ArrayList<RestoreUpdateRecord>> e : providerEntries) {
+            for (Map.Entry<Provider, List<RestoreUpdateRecord>> e : providerEntries) {
                 // For each provider there's a list of affected IDs
                 Provider provider = e.getKey();
                 if (provider.zombie) {
@@ -5589,7 +5670,7 @@
                     // We'll be called again when the provider is installed.
                     continue;
                 }
-                ArrayList<RestoreUpdateRecord> updates = e.getValue();
+                List<RestoreUpdateRecord> updates = e.getValue();
                 final int pending = countPendingUpdates(updates);
                 if (DEBUG) {
                     Slog.i(TAG, "Provider " + provider + " pending: " + pending);
@@ -5618,12 +5699,12 @@
             }
 
             // same thing per host
-            Set<Map.Entry<Host, ArrayList<RestoreUpdateRecord>>> hostEntries
+            Set<Map.Entry<Host, List<RestoreUpdateRecord>>> hostEntries
                     = mUpdatesByHost.entrySet();
-            for (Map.Entry<Host, ArrayList<RestoreUpdateRecord>> e : hostEntries) {
+            for (Map.Entry<Host, List<RestoreUpdateRecord>> e : hostEntries) {
                 Host host = e.getKey();
                 if (host.id.uid != UNKNOWN_UID) {
-                    ArrayList<RestoreUpdateRecord> updates = e.getValue();
+                    List<RestoreUpdateRecord> updates = e.getValue();
                     final int pending = countPendingUpdates(updates);
                     if (DEBUG) {
                         Slog.i(TAG, "Host " + host + " pending: " + pending);
@@ -5714,8 +5795,9 @@
             return false;
         }
 
+        @GuardedBy("mLock")
         private void stashProviderRestoreUpdateLocked(Provider provider, int oldId, int newId) {
-            ArrayList<RestoreUpdateRecord> r = mUpdatesByProvider.get(provider);
+            List<RestoreUpdateRecord> r = mUpdatesByProvider.get(provider);
             if (r == null) {
                 r = new ArrayList<>();
                 mUpdatesByProvider.put(provider, r);
@@ -5732,7 +5814,7 @@
             r.add(new RestoreUpdateRecord(oldId, newId));
         }
 
-        private boolean alreadyStashed(ArrayList<RestoreUpdateRecord> stash,
+        private boolean alreadyStashed(List<RestoreUpdateRecord> stash,
                 final int oldId, final int newId) {
             final int N = stash.size();
             for (int i = 0; i < N; i++) {
@@ -5744,8 +5826,9 @@
             return false;
         }
 
+        @GuardedBy("mLock")
         private void stashHostRestoreUpdateLocked(Host host, int oldId, int newId) {
-            ArrayList<RestoreUpdateRecord> r = mUpdatesByHost.get(host);
+            List<RestoreUpdateRecord> r = mUpdatesByHost.get(host);
             if (r == null) {
                 r = new ArrayList<>();
                 mUpdatesByHost.put(host, r);
@@ -5835,7 +5918,7 @@
                     || widget.provider.getUserId() == userId);
         }
 
-        private int countPendingUpdates(ArrayList<RestoreUpdateRecord> updates) {
+        private int countPendingUpdates(List<RestoreUpdateRecord> updates) {
             int pending = 0;
             final int N = updates.size();
             for (int i = 0; i < N; i++) {
@@ -5847,9 +5930,28 @@
             return pending;
         }
 
+        @GuardedBy("mLock")
+        @NonNull
+        private State getStateLocked(final int userId) {
+            final Set<String> prunedApps = mPrunedAppsPerUser.get(userId);
+            final SparseArray<List<RestoreUpdateRecord>> updatesByProvider = new SparseArray<>();
+            final SparseArray<List<RestoreUpdateRecord>> updatesByHost = new SparseArray<>();
+            mUpdatesByProvider.forEach((p, updates) -> {
+                if (p.getUserId() == userId) {
+                    updatesByProvider.put(p.tag, new ArrayList<>(updates));
+                }
+            });
+            mUpdatesByHost.forEach((h, updates) -> {
+                if (h.getUserId() == userId) {
+                    updatesByHost.put(h.tag, new ArrayList<>(updates));
+                }
+            });
+            return new State(prunedApps, updatesByProvider, updatesByHost);
+        }
+
         // Accumulate a list of updates that affect the given provider for a final
         // coalesced notification broadcast once restore is over.
-        private class RestoreUpdateRecord {
+        static class RestoreUpdateRecord {
             public int oldId;
             public int newId;
             public boolean notified;
@@ -5860,6 +5962,45 @@
                 notified = false;
             }
         }
+
+        static final class State {
+            // We need to make sure to wipe the pre-restore widget state only once for
+            // a given package.  Keep track of what we've done so far here; the list is
+            // cleared at the start of every system restore pass, but preserved through
+            // any install-time restore operations.
+            @Nullable
+            private final Set<String> mPrunedApps;
+
+            @Nullable
+            private final SparseArray<List<RestoreUpdateRecord>> mUpdatesByProvider;
+
+            @Nullable
+            private final SparseArray<List<RestoreUpdateRecord>> mUpdatesByHost;
+
+            State(
+                    @Nullable final Set<String> prunedApps,
+                    @Nullable final SparseArray<List<RestoreUpdateRecord>> updatesByProvider,
+                    @Nullable final SparseArray<List<RestoreUpdateRecord>> updatesByHost) {
+                mPrunedApps = prunedApps;
+                mUpdatesByProvider = updatesByProvider;
+                mUpdatesByHost = updatesByHost;
+            }
+
+            @Nullable
+            Set<String> getPrunedApps() {
+                return mPrunedApps;
+            }
+
+            @Nullable
+            SparseArray<List<BackupRestoreController.RestoreUpdateRecord>> getUpdatesByProvider() {
+                return mUpdatesByProvider;
+            }
+
+            @Nullable
+            SparseArray<List<BackupRestoreController.RestoreUpdateRecord>> getUpdatesByHost() {
+                return mUpdatesByHost;
+            }
+        }
     }
 
     private class AppWidgetManagerLocal extends AppWidgetManagerInternal {
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java
index d781cd8..ce9130a 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetXmlUtil.java
@@ -22,17 +22,24 @@
 import android.content.ComponentName;
 import android.os.Build;
 import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
 import android.util.SizeF;
 import android.util.Slog;
+import android.util.SparseArray;
 
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -65,6 +72,16 @@
     private static final String ATTR_DESCRIPTION_RES = "description_res";
     private static final String ATTR_PROVIDER_INHERITANCE = "provider_inheritance";
     private static final String ATTR_OS_FINGERPRINT = "os_fingerprint";
+    static final String TAG_BACKUP_RESTORE_CONTROLLER_STATE = "br";
+    private static final String TAG_PRUNED_APPS = "pruned_apps";
+    private static final String ATTR_TAG = "tag";
+    private static final String ATTR_PACKAGE_NAMES = "pkgs";
+    private static final String TAG_PROVIDER_UPDATES = "provider_updates";
+    private static final String TAG_HOST_UPDATES = "host_updates";
+    private static final String TAG_RECORD = "record";
+    private static final String ATTR_OLD_ID = "old_id";
+    private static final String ATTR_NEW_ID = "new_id";
+    private static final String ATTR_NOTIFIED = "notified";
     private static final String SIZE_SEPARATOR = ",";
 
     /**
@@ -165,4 +182,168 @@
             return null;
         }
     }
+
+    /**
+     * Persists {@link AppWidgetServiceImpl.BackupRestoreController.State} to disk as XML.
+     * See {@link #readBackupRestoreControllerState(TypedXmlPullParser)} for example XML.
+     *
+     * @param out XML serializer
+     * @param state {@link AppWidgetServiceImpl.BackupRestoreController.State} of
+     *      intermediate states to be persisted as xml to resume restore after reboot.
+     */
+    static void writeBackupRestoreControllerState(
+            @NonNull final TypedXmlSerializer out,
+            @NonNull final AppWidgetServiceImpl.BackupRestoreController.State state)
+            throws IOException {
+        Objects.requireNonNull(out);
+        Objects.requireNonNull(state);
+        out.startTag(null, TAG_BACKUP_RESTORE_CONTROLLER_STATE);
+        final Set<String> prunedApps = state.getPrunedApps();
+        if (prunedApps != null && !prunedApps.isEmpty()) {
+            out.startTag(null, TAG_PRUNED_APPS);
+            out.attribute(null, ATTR_PACKAGE_NAMES, String.join(",", prunedApps));
+            out.endTag(null, TAG_PRUNED_APPS);
+        }
+        final SparseArray<List<AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord>>
+                updatesByProvider = state.getUpdatesByProvider();
+        if (updatesByProvider != null) {
+            writeUpdateRecords(out, TAG_PROVIDER_UPDATES, updatesByProvider);
+        }
+        final SparseArray<List<AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord>>
+                updatesByHost = state.getUpdatesByHost();
+        if (updatesByHost != null) {
+            writeUpdateRecords(out, TAG_HOST_UPDATES, updatesByHost);
+        }
+        out.endTag(null, TAG_BACKUP_RESTORE_CONTROLLER_STATE);
+    }
+
+    private static void writeUpdateRecords(@NonNull final TypedXmlSerializer out,
+            @NonNull final String outerTag, @NonNull final SparseArray<List<
+                    AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord>> records)
+            throws IOException {
+        for (int i = 0; i < records.size(); i++) {
+            final int tag = records.keyAt(i);
+            out.startTag(null, outerTag);
+            out.attributeInt(null, ATTR_TAG, tag);
+            final List<AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord> entries =
+                    records.get(tag);
+            for (AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord entry : entries) {
+                out.startTag(null, TAG_RECORD);
+                out.attributeInt(null, ATTR_OLD_ID, entry.oldId);
+                out.attributeInt(null, ATTR_NEW_ID, entry.newId);
+                out.attributeBoolean(null, ATTR_NOTIFIED, entry.notified);
+                out.endTag(null, TAG_RECORD);
+            }
+            out.endTag(null, outerTag);
+        }
+    }
+
+    /**
+     * Parses {@link AppWidgetServiceImpl.BackupRestoreController.State} from xml.
+     *
+     * <pre>
+     * {@code
+     *     <?xml version="1.0"?>
+     *     <br>
+     *         <pruned_apps pkgs="com.example.app1,com.example.app2,com.example.app3" />
+     *         <provider_updates tag="0">
+     *             <record old_id="10" new_id="0" notified="false" />
+     *         </provider_updates>
+     *         <provider_updates tag="1">
+     *             <record old_id="9" new_id="1" notified="true" />
+     *         </provider_updates>
+     *         <provider_updates tag="2">
+     *             <record old_id="8" new_id="2" notified="false" />
+     *         </provider_updates>
+     *         <host_updates tag="0">
+     *             <record old_id="10" new_id="0" notified="false" />
+     *         </host_updates>
+     *         <host_updates tag="1">
+     *             <record old_id="9" new_id="1" notified="true" />
+     *         </host_updates>
+     *         <host_updates tag="2">
+     *             <record old_id="8" new_id="2" notified="false" />
+     *         </host_updates>
+     *     </br>
+     * }
+     * </pre>
+     *
+     * @param parser XML parser
+     * @return {@link AppWidgetServiceImpl.BackupRestoreController.State} of intermediate states
+     * in {@link AppWidgetServiceImpl.BackupRestoreController}, so that backup & restore can be
+     * resumed after reboot.
+     */
+    @Nullable
+    static AppWidgetServiceImpl.BackupRestoreController.State
+            readBackupRestoreControllerState(@NonNull final TypedXmlPullParser parser) {
+        Objects.requireNonNull(parser);
+        int type;
+        String tag = null;
+        final Set<String> prunedApps = new ArraySet<>(1);
+        final SparseArray<List<AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord>>
+                updatesByProviders = new SparseArray<>();
+        final SparseArray<List<AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord>>
+                updatesByHosts = new SparseArray<>();
+
+        try {
+            do {
+                type = parser.next();
+                if (type != XmlPullParser.START_TAG) {
+                    continue;
+                }
+                tag = parser.getName();
+                switch (tag) {
+                    case TAG_PRUNED_APPS:
+                        final String packages =
+                                parser.getAttributeValue(null, ATTR_PACKAGE_NAMES);
+                        prunedApps.addAll(Arrays.asList(packages.split(",")));
+                        break;
+                    case TAG_PROVIDER_UPDATES:
+                        updatesByProviders.put(parser.getAttributeInt(null, ATTR_TAG),
+                                parseRestoreUpdateRecords(parser));
+                        break;
+                    case TAG_HOST_UPDATES:
+                        updatesByHosts.put(parser.getAttributeInt(null, ATTR_TAG),
+                                parseRestoreUpdateRecords(parser));
+                        break;
+                    default:
+                        break;
+                }
+            } while (type != XmlPullParser.END_DOCUMENT
+                    && (!TAG_BACKUP_RESTORE_CONTROLLER_STATE.equals(tag)
+                            || type != XmlPullParser.END_TAG));
+        } catch (IOException | XmlPullParserException e) {
+            Log.e(TAG, "error parsing state", e);
+            return null;
+        }
+        return new AppWidgetServiceImpl.BackupRestoreController.State(
+                prunedApps, updatesByProviders, updatesByHosts);
+    }
+
+    @NonNull
+    private static List<
+            AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord
+            > parseRestoreUpdateRecords(@NonNull final TypedXmlPullParser parser)
+            throws XmlPullParserException, IOException {
+        int type;
+        String tag;
+        final List<AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord> ret =
+                new ArrayList<>();
+        do {
+            type = parser.next();
+            tag = parser.getName();
+            if (tag.equals(TAG_RECORD) && type == XmlPullParser.START_TAG) {
+                final int oldId = parser.getAttributeInt(null, ATTR_OLD_ID);
+                final int newId = parser.getAttributeInt(null, ATTR_NEW_ID);
+                final boolean notified = parser.getAttributeBoolean(
+                        null, ATTR_NOTIFIED);
+                final AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord record =
+                        new AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord(
+                                oldId, newId);
+                record.notified = notified;
+                ret.add(record);
+            }
+        } while (tag.equals(TAG_RECORD));
+        return ret;
+    }
 }
diff --git a/services/core/java/com/android/server/timezonedetector/OWNERS b/services/core/java/com/android/server/timezonedetector/OWNERS
index 485a0dd..dfa07d8 100644
--- a/services/core/java/com/android/server/timezonedetector/OWNERS
+++ b/services/core/java/com/android/server/timezonedetector/OWNERS
@@ -2,7 +2,7 @@
 # This is the main list for platform time / time zone detection maintainers, for this dir and
 # ultimately referenced by other OWNERS files for components maintained by the same team.
 nfuller@google.com
+boullanger@google.com
 jmorace@google.com
 kanyinsola@google.com
-mingaleev@google.com
-narayan@google.com
+mingaleev@google.com
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java b/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java
index 8a7815e..c34e28f 100644
--- a/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java
@@ -47,7 +47,9 @@
 import android.os.Handler;
 import android.os.UserHandle;
 import android.test.InstrumentationTestCase;
+import android.util.ArraySet;
 import android.util.AtomicFile;
+import android.util.SparseArray;
 import android.util.Xml;
 import android.widget.RemoteViews;
 
@@ -67,10 +69,12 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
 import java.util.Random;
+import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 
 /**
@@ -388,6 +392,93 @@
         assertThat(target.previewLayout).isEqualTo(original.previewLayout);
     }
 
+    public void testBackupRestoreControllerStatePersistence() throws IOException {
+        // Setup mock data
+        final Set<String> mockPrunedApps = getMockPrunedApps();
+        final SparseArray<
+                List<AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord>
+                > mockUpdatesByProvider = getMockUpdates();
+        final  SparseArray<
+                List<AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord>
+                > mockUpdatesByHost = getMockUpdates();
+        final AppWidgetServiceImpl.BackupRestoreController.State state =
+                new AppWidgetServiceImpl.BackupRestoreController.State(
+                        mockPrunedApps, mockUpdatesByProvider, mockUpdatesByHost);
+
+        final File file = new File(mTestContext.getDataDir(), "state.xml");
+        saveBackupRestoreControllerState(file, state);
+        final AppWidgetServiceImpl.BackupRestoreController.State target =
+                loadStateLocked(file);
+        assertNotNull(target);
+        final SparseArray<List<AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord>>
+                actualUpdatesByProvider = target.getUpdatesByProvider();
+        assertNotNull(actualUpdatesByProvider);
+        final SparseArray<List<AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord>>
+                actualUpdatesByHost = target.getUpdatesByHost();
+        assertNotNull(actualUpdatesByHost);
+
+        assertEquals(mockPrunedApps, target.getPrunedApps());
+        for (int i = 0; i < mockUpdatesByProvider.size(); i++) {
+            final int key = mockUpdatesByProvider.keyAt(i);
+            verifyRestoreUpdateRecord(
+                    actualUpdatesByProvider.get(key), mockUpdatesByProvider.get(key));
+        }
+        for (int i = 0; i < mockUpdatesByHost.size(); i++) {
+            final int key = mockUpdatesByHost.keyAt(i);
+            verifyRestoreUpdateRecord(
+                    actualUpdatesByHost.get(key), mockUpdatesByHost.get(key));
+        }
+    }
+
+    private void verifyRestoreUpdateRecord(
+            @NonNull final List<AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord>
+                    actualUpdates,
+            @NonNull final List<AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord>
+                    expectedUpdates) {
+        assertEquals(expectedUpdates.size(), actualUpdates.size());
+        for (int i = 0; i < expectedUpdates.size(); i++) {
+            final AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord expected =
+                    expectedUpdates.get(i);
+            final AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord actual =
+                    actualUpdates.get(i);
+            assertEquals(expected.oldId, actual.oldId);
+            assertEquals(expected.newId, actual.newId);
+            assertEquals(expected.notified, actual.notified);
+        }
+    }
+
+    @NonNull
+    private static Set<String> getMockPrunedApps() {
+        final Set<String> mockPrunedApps = new ArraySet<>(10);
+        for (int i = 0; i < 10; i++) {
+            mockPrunedApps.add("com.example.app" + i);
+        }
+        return mockPrunedApps;
+    }
+
+    @NonNull
+    private static SparseArray<
+            List<AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord>
+            > getMockUpdates() {
+        final SparseArray<List<
+                AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord>> ret =
+                new SparseArray<>(4);
+        ret.put(0, new ArrayList<>());
+        for (int i = 0; i < 5; i++) {
+            final AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord record =
+                    new AppWidgetServiceImpl.BackupRestoreController.RestoreUpdateRecord(
+                            5 - i, i);
+            record.notified = (i % 2 == 1);
+            final int key = (i < 3) ? 1 : 2;
+            if (!ret.contains(key)) {
+                ret.put(key, new ArrayList<>());
+            }
+            ret.get(key).add(record);
+        }
+        ret.put(3, new ArrayList<>());
+        return ret;
+    }
+
     private int setupHostAndWidget() {
         List<PendingHostUpdate> updates = mService.startListening(
                 mMockHost, mPkgName, HOST_ID, new int[0]).getList();
@@ -418,6 +509,40 @@
         return mTestContext.getResources().getInteger(resId);
     }
 
+    private static void saveBackupRestoreControllerState(
+            @NonNull final File dst,
+            @Nullable final AppWidgetServiceImpl.BackupRestoreController.State state)
+            throws IOException {
+        Objects.requireNonNull(dst);
+        if (state == null) {
+            return;
+        }
+        final AtomicFile file = new AtomicFile(dst);
+        final FileOutputStream stream = file.startWrite();
+        final TypedXmlSerializer out = Xml.resolveSerializer(stream);
+        out.startDocument(null, true);
+        AppWidgetXmlUtil.writeBackupRestoreControllerState(out, state);
+        out.endDocument();
+        file.finishWrite(stream);
+    }
+
+    private static AppWidgetServiceImpl.BackupRestoreController.State loadStateLocked(
+            @NonNull final File dst) {
+        Objects.requireNonNull(dst);
+        final AtomicFile file = new AtomicFile(dst);
+        try (FileInputStream stream = file.openRead()) {
+            final TypedXmlPullParser parser = Xml.resolvePullParser(stream);
+            int type;
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                    && type != XmlPullParser.START_TAG) {
+                // drain whitespace, comments, etc.
+            }
+            return AppWidgetXmlUtil.readBackupRestoreControllerState(parser);
+        } catch (IOException | XmlPullParserException e) {
+            return null;
+        }
+    }
+
     private static void saveWidgetProviderInfoLocked(@NonNull final File dst,
             @Nullable final AppWidgetProviderInfo info)
             throws IOException {