Merge "Add multi-toggle preference UI for device details page" into main
diff --git a/aconfig/settings_bluetooth_declarations.aconfig b/aconfig/settings_bluetooth_declarations.aconfig
index b8b9d9f..f6c271c 100644
--- a/aconfig/settings_bluetooth_declarations.aconfig
+++ b/aconfig/settings_bluetooth_declarations.aconfig
@@ -31,3 +31,13 @@
   description: "Gates whether to enable bluetooth device details polish"
   bug: "343317785"
 }
+
+flag {
+  name: "disable_bonding_cancellation_for_orientation_change"
+  namespace: "cross_device_experiences"
+  description: "Stop cancelling bonding process when there is an orientation change"
+  bug: "349542301"
+  metadata {
+      purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/res/layout/modes_set_schedule_layout.xml b/res/layout/modes_set_schedule_layout.xml
index ebb349e..d53e2e4 100644
--- a/res/layout/modes_set_schedule_layout.xml
+++ b/res/layout/modes_set_schedule_layout.xml
@@ -48,7 +48,8 @@
             app:layout_constrainedWidth="true"
             app:layout_constraintHorizontal_bias="0"
             android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Medium"
-            android:text="@string/zen_mode_start_time" />
+            android:text="@string/zen_mode_start_time"
+            android:importantForAccessibility="no" />
 
         <!-- Start time display + setter -->
         <TextView
@@ -85,7 +86,8 @@
             app:layout_constrainedWidth="true"
             app:layout_constraintHorizontal_bias="0"
             android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Medium"
-            android:text="@string/zen_mode_end_time" />
+            android:text="@string/zen_mode_end_time"
+            android:importantForAccessibility="no" />
 
         <!-- End time setter; right-aligned -->
         <TextView
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 31372ca..311b56f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -12238,11 +12238,11 @@
     <!-- Default title for the settings panel [CHAR LIMIT=NONE] -->
     <string name="settings_panel_title">Settings Panel</string>
 
-    <!-- Title for a toggle that enables freeform windowing experiences. Freeform windowing experiences are features involving apps running in resizable windows. [CHAR LIMIT=50] -->
-    <string name="enable_desktop_mode">Enable freeform windowing experiences</string>
+    <!-- Title for a toggle that enables freeform windows. Freeform windows enables users to freely arrange and resize overlapping apps. [CHAR LIMIT=50] -->
+    <string name="enable_desktop_mode">Enable freeform windows</string>
 
-    <!-- Title for a toggle that enables desktop mode on secondary display. [CHAR LIMIT=50] -->
-    <string name="enable_desktop_mode_on_secondary_display">Enable desktop mode on secondary display</string>
+    <!-- Title for a toggle that enables freeform windows on secondary display. Freeform windows enables users to freely arrange and resize overlapping apps. [CHAR LIMIT=50] -->
+    <string name="enable_desktop_mode_on_secondary_display">Enable freeform windows on secondary display</string>
 
     <!-- UI debug setting: enable non-resizables in multi window [CHAR LIMIT=60] -->
     <string name="enable_non_resizable_multi_window">Enable non-resizable in multi window</string>
@@ -13202,12 +13202,12 @@
     <!-- The content description for accessibility tools of the customize button. It specifies which screensaver the user is customizing [CHAR LIMIT=NONE] -->
     <string name="customize_button_description">Customize <xliff:g id="screensaver_name" example="Art Gallery">%1$s</xliff:g></string>
 
-    <!-- Dialog body text used to explain a reboot is required after enabling freeform window support for it to work. Freeform window is when an app runs in a resizable window. [CHAR LIMIT=none] -->
+    <!-- Dialog body text used to explain a reboot is required after enabling freeform window support for it to work. Freeform windows enables users to freely arrange and resize overlapping apps. [CHAR LIMIT=none] -->
     <string name="reboot_dialog_enable_freeform_support">A reboot is required to enable freeform window support.</string>
-    <!-- Dialog body text used to explain a reboot is required after updating availability of freeform windowing experiences. Freeform windowing experiences are features involving apps running in resizable windows. [CHAR LIMIT=none] -->
-    <string name="reboot_dialog_override_desktop_mode">A reboot is required to update availability of freeform windowing experiences.</string>
-    <!-- Dialog body text used to explain a reboot is required after enabling desktop mode on secondary displays. [CHAR LIMIT=none] -->
-    <string name="reboot_dialog_enable_desktop_mode_on_secondary_display">A reboot is required to enable desktop mode on secondary displays.</string>
+    <!-- Dialog body text used to explain a reboot is required after updating availability of freeform windows. Freeform windows enables users to freely arrange and resize overlapping apps. [CHAR LIMIT=none] -->
+    <string name="reboot_dialog_override_desktop_mode">A reboot is required to update availability of freeform windows.</string>
+    <!-- Dialog body text used to explain a reboot is required after enabling freeform windows on secondary displays. Freeform windows enables users to freely arrange and resize overlapping apps. [CHAR LIMIT=none] -->
+    <string name="reboot_dialog_enable_desktop_mode_on_secondary_display">A reboot is required to enable freeform windows on secondary displays.</string>
     <!-- Text on the dialog button to reboot the device now [CHAR LIMIT=50] -->
     <string name="reboot_dialog_reboot_now">Reboot now</string>
     <!-- Text on the dialog button to reboot the device later [CHAR LIMIT=50] -->
diff --git a/src/com/android/settings/MainClear.java b/src/com/android/settings/MainClear.java
index 9dadcb9..ab7a714 100644
--- a/src/com/android/settings/MainClear.java
+++ b/src/com/android/settings/MainClear.java
@@ -188,7 +188,7 @@
                     false /* biometricsAuthenticationRequested */,
                     userId)) {
                 Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRICS_REQUEST,
-                        userId);
+                        userId, false /* hideBackground */);
                 return;
             }
         }
diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java
index add5604..badcb63 100644
--- a/src/com/android/settings/Utils.java
+++ b/src/com/android/settings/Utils.java
@@ -25,6 +25,7 @@
 import static android.text.format.DateUtils.FORMAT_SHOW_DATE;
 
 import static com.android.settings.password.ConfirmDeviceCredentialActivity.BIOMETRIC_PROMPT_AUTHENTICATORS;
+import static com.android.settings.password.ConfirmDeviceCredentialActivity.BIOMETRIC_PROMPT_HIDE_BACKGROUND;
 import static com.android.settings.password.ConfirmDeviceCredentialActivity.BIOMETRIC_PROMPT_NEGATIVE_BUTTON_TEXT;
 
 import android.app.ActionBar;
@@ -1519,12 +1520,13 @@
      * to check if all requirements for mandatory biometrics is satisfied
      * before launching biometric prompt.
      *
-     * @param fragment    corresponding fragment of the surface
-     * @param requestCode for starting the new activity
-     * @param userId      user id for the authentication request
+     * @param fragment       corresponding fragment of the surface
+     * @param requestCode    for starting the new activity
+     * @param userId         user id for the authentication request
+     * @param hideBackground if the background activity screen needs to be hidden
      */
     public static void launchBiometricPromptForMandatoryBiometrics(@NonNull Fragment fragment,
-            int requestCode, int userId) {
+            int requestCode, int userId, boolean hideBackground) {
         final Intent intent = new Intent();
         intent.putExtra(BIOMETRIC_PROMPT_AUTHENTICATORS,
                 BiometricManager.Authenticators.MANDATORY_BIOMETRICS);
@@ -1534,6 +1536,7 @@
                 fragment.getString(R.string.mandatory_biometrics_prompt_description));
         intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_ALLOW_ANY_USER, true);
         intent.putExtra(EXTRA_USER_ID, userId);
+        intent.putExtra(BIOMETRIC_PROMPT_HIDE_BACKGROUND, hideBackground);
         intent.setClassName(SETTINGS_PACKAGE_NAME,
                 ConfirmDeviceCredentialActivity.InternalActivity.class.getName());
         fragment.startActivityForResult(intent, requestCode);
diff --git a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java
index 835f3a8..11194ce 100644
--- a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java
+++ b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java
@@ -147,7 +147,7 @@
                 mBiometricsAuthenticationRequested, mUserId)) {
             mBiometricsAuthenticationRequested = true;
             Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST,
-                    mUserId);
+                    mUserId, true /* hideBackground */);
         }
 
         updateUnlockPhonePreferenceSummary();
@@ -166,7 +166,7 @@
                 && mGkPwHandle != 0L) {
             mBiometricsAuthenticationRequested = true;
             Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST,
-                    mUserId);
+                    mUserId, true /* hideBackground */);
         }
         if (!mConfirmCredential) {
             mDoNotFinishActivity = false;
diff --git a/src/com/android/settings/biometrics/face/FaceSettings.java b/src/com/android/settings/biometrics/face/FaceSettings.java
index 305d670..bcd5231 100644
--- a/src/com/android/settings/biometrics/face/FaceSettings.java
+++ b/src/com/android/settings/biometrics/face/FaceSettings.java
@@ -293,7 +293,7 @@
                 mUserId)) {
             mBiometricsAuthenticationRequested = true;
             Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST,
-                    mUserId);
+                    mUserId, true /* hideBackground */);
         } else {
             mAttentionController.setToken(mToken);
             mEnrollController.setToken(mToken);
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
index 83bc0e6..9cda327 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
@@ -489,7 +489,7 @@
                         mUserId)) {
                     mBiometricsAuthenticationRequested = true;
                     Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST,
-                            mUserId);
+                            mUserId, true /* hideBackground */);
                 } else if (!mHasFirstEnrolled) {
                     mIsEnrolling = true;
                     addFirstFingerprint(null);
@@ -784,7 +784,7 @@
                     mUserId)) {
                 mBiometricsAuthenticationRequested = true;
                 Utils.launchBiometricPromptForMandatoryBiometrics(this,
-                        BIOMETRIC_AUTH_REQUEST, mUserId);
+                        BIOMETRIC_AUTH_REQUEST, mUserId, true /* hideBackground */);
             }
         }
 
diff --git a/src/com/android/settings/bluetooth/BluetoothEnabler.java b/src/com/android/settings/bluetooth/BluetoothEnabler.java
index df5cc72..a5d0bc6 100644
--- a/src/com/android/settings/bluetooth/BluetoothEnabler.java
+++ b/src/com/android/settings/bluetooth/BluetoothEnabler.java
@@ -132,7 +132,7 @@
 
         new Thread(() -> {
             try {
-                mIsSatelliteOn.set(mSatelliteRepository.requestIsEnabled(
+                mIsSatelliteOn.set(mSatelliteRepository.requestIsSessionStarted(
                         Executors.newSingleThreadExecutor()).get(3000, TimeUnit.MILLISECONDS));
             } catch (InterruptedException | ExecutionException | TimeoutException e) {
                 Log.e(TAG, "Error to get satellite status : " + e);
diff --git a/src/com/android/settings/bluetooth/BluetoothPairingDialogFragment.java b/src/com/android/settings/bluetooth/BluetoothPairingDialogFragment.java
index 33e6fc3..e6b197c 100644
--- a/src/com/android/settings/bluetooth/BluetoothPairingDialogFragment.java
+++ b/src/com/android/settings/bluetooth/BluetoothPairingDialogFragment.java
@@ -41,6 +41,7 @@
 
 import com.android.settings.R;
 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+import com.android.settings.flags.Flags;
 
 /**
  * A dialogFragment used by {@link BluetoothPairingDialog} to create an appropriately styled dialog
@@ -87,12 +88,15 @@
     @Override
     public void onDestroy() {
         super.onDestroy();
-        if (mPairingController.getDialogType()
-                != BluetoothPairingController.DISPLAY_PASSKEY_DIALOG) {
-            /* Cancel pairing unless explicitly accepted by user */
-            if (!mPositiveClicked) {
-                mPairingController.onCancel();
-            }
+        /* Cancel pairing unless 1) explicitly accepted by user 2) the event is triggered by
+         * orientation change. */
+        boolean shouldCancelPairing =
+                Flags.disableBondingCancellationForOrientationChange()
+                        ? !mPositiveClicked && !getActivity().isChangingConfigurations()
+                        : !mPositiveClicked;
+        if (mPairingController.getDialogType() != BluetoothPairingController.DISPLAY_PASSKEY_DIALOG
+                && shouldCancelPairing) {
+            mPairingController.onCancel();
         }
     }
 
diff --git a/src/com/android/settings/development/DesktopModeSecondaryDisplayPreferenceController.java b/src/com/android/settings/development/DesktopModeSecondaryDisplayPreferenceController.java
index 0d3d835..2bce9ad 100644
--- a/src/com/android/settings/development/DesktopModeSecondaryDisplayPreferenceController.java
+++ b/src/com/android/settings/development/DesktopModeSecondaryDisplayPreferenceController.java
@@ -67,6 +67,12 @@
         Settings.Global.putInt(mContext.getContentResolver(),
                 DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
                 isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF);
+        // Update freeform window support on device.
+        // DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT setting enables freeform support on device
+        // where it's not present by default.
+        Settings.Global.putInt(mContext.getContentResolver(),
+                Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT,
+                isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF);
         if (isEnabled && mFragment != null) {
             RebootConfirmationDialogFragment.show(
                     mFragment, R.string.reboot_dialog_enable_desktop_mode_on_secondary_display,
diff --git a/src/com/android/settings/network/AirplaneModePreferenceController.java b/src/com/android/settings/network/AirplaneModePreferenceController.java
index b1f6e50..d4bd4a3 100644
--- a/src/com/android/settings/network/AirplaneModePreferenceController.java
+++ b/src/com/android/settings/network/AirplaneModePreferenceController.java
@@ -162,7 +162,8 @@
     public void onResume() {
         try {
             mIsSatelliteOn.set(
-                    mSatelliteRepository.requestIsEnabled(Executors.newSingleThreadExecutor())
+                    mSatelliteRepository
+                            .requestIsSessionStarted(Executors.newSingleThreadExecutor())
                             .get(2000, TimeUnit.MILLISECONDS));
         } catch (ExecutionException | TimeoutException | InterruptedException e) {
             Log.e(TAG, "Error to get satellite status : " + e);
diff --git a/src/com/android/settings/network/telephony/MobileNetworkPhoneNumberPreferenceController.kt b/src/com/android/settings/network/telephony/MobileNetworkPhoneNumberPreferenceController.kt
index 10a8b53..db16acd 100644
--- a/src/com/android/settings/network/telephony/MobileNetworkPhoneNumberPreferenceController.kt
+++ b/src/com/android/settings/network/telephony/MobileNetworkPhoneNumberPreferenceController.kt
@@ -17,49 +17,37 @@
 package com.android.settings.network.telephony
 
 import android.content.Context
-import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager
-import android.util.Log
-import androidx.annotation.VisibleForTesting
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.viewModels
 import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.lifecycleScope
 import androidx.preference.Preference
 import androidx.preference.PreferenceScreen
 import com.android.settings.R
 import com.android.settings.flags.Flags
-import com.android.settings.network.SubscriptionInfoListViewModel
 import com.android.settings.network.SubscriptionUtil
 import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
 
-/**
- * Preference controller for "Phone number"
- */
-class MobileNetworkPhoneNumberPreferenceController(context: Context, key: String) :
-    TelephonyBasePreferenceController(context, key) {
+/** Preference controller for "Phone number" */
+class MobileNetworkPhoneNumberPreferenceController
+@JvmOverloads
+constructor(
+    context: Context,
+    key: String,
+    private val subscriptionRepository: SubscriptionRepository = SubscriptionRepository(context),
+) : TelephonyBasePreferenceController(context, key) {
 
-    private lateinit var lazyViewModel: Lazy<SubscriptionInfoListViewModel>
     private lateinit var preference: Preference
 
-    private var phoneNumber = String()
-
-    fun init(fragment: Fragment, subId: Int) {
-        lazyViewModel = fragment.viewModels()
+    fun init(subId: Int) {
         mSubId = subId
     }
 
-    override fun getAvailabilityStatus(subId: Int): Int = when {
-        !Flags.isDualSimOnboardingEnabled() -> CONDITIONALLY_UNAVAILABLE
-        SubscriptionManager.isValidSubscriptionId(subId)
-                && SubscriptionUtil.isSimHardwareVisible(mContext) -> AVAILABLE
-        else -> CONDITIONALLY_UNAVAILABLE
-    }
+    override fun getAvailabilityStatus(subId: Int): Int =
+        when {
+            !Flags.isDualSimOnboardingEnabled() -> CONDITIONALLY_UNAVAILABLE
+            SubscriptionManager.isValidSubscriptionId(subId) &&
+                SubscriptionUtil.isSimHardwareVisible(mContext) -> AVAILABLE
+            else -> CONDITIONALLY_UNAVAILABLE
+        }
 
     override fun displayPreference(screen: PreferenceScreen) {
         super.displayPreference(screen)
@@ -67,51 +55,10 @@
     }
 
     override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
-        if (!this::lazyViewModel.isInitialized) {
-            Log.e(
-                this.javaClass.simpleName,
-                "lateinit property lazyViewModel has not been initialized"
-            )
-            return
-        }
-        val viewModel by lazyViewModel
-        val coroutineScope = viewLifecycleOwner.lifecycleScope
-
-        viewModel.subscriptionInfoListFlow
-            .map { subscriptionInfoList ->
-                subscriptionInfoList
-                    .firstOrNull { subInfo -> subInfo.subscriptionId == mSubId }
+        subscriptionRepository.phoneNumberFlow(mSubId).collectLatestWithLifecycle(
+            viewLifecycleOwner) { phoneNumber ->
+                preference.summary = phoneNumber ?: getStringUnknown()
             }
-            .flowOn(Dispatchers.Default)
-            .collectLatestWithLifecycle(viewLifecycleOwner) {
-                it?.let {
-                    coroutineScope.launch {
-                        refreshData(it)
-                    }
-                }
-            }
-    }
-
-    @VisibleForTesting
-    suspend fun refreshData(subscriptionInfo: SubscriptionInfo){
-        withContext(Dispatchers.Default) {
-            phoneNumber = getFormattedPhoneNumber(subscriptionInfo)
-        }
-        refreshUi()
-    }
-
-    private fun refreshUi(){
-        preference.summary = phoneNumber
-    }
-
-    private fun getFormattedPhoneNumber(subscriptionInfo: SubscriptionInfo?): String {
-        val phoneNumber = SubscriptionUtil.getBidiFormattedPhoneNumber(
-            mContext,
-            subscriptionInfo
-        )
-        return phoneNumber
-            ?.let { return it.ifEmpty { getStringUnknown() } }
-            ?: getStringUnknown()
     }
 
     private fun getStringUnknown(): String {
diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java
index 896eac6..9db5af2 100644
--- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java
+++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java
@@ -257,7 +257,7 @@
         use(NrDisabledInDsdsFooterPreferenceController.class).init(mSubId);
 
         use(MobileNetworkSpnPreferenceController.class).init(this, mSubId);
-        use(MobileNetworkPhoneNumberPreferenceController.class).init(this, mSubId);
+        use(MobileNetworkPhoneNumberPreferenceController.class).init(mSubId);
         use(MobileNetworkImeiPreferenceController.class).init(this, mSubId);
 
         final MobileDataPreferenceController mobileDataPreferenceController =
diff --git a/src/com/android/settings/network/telephony/SatelliteSetting.java b/src/com/android/settings/network/telephony/SatelliteSetting.java
index 7e9e61d..df58048 100644
--- a/src/com/android/settings/network/telephony/SatelliteSetting.java
+++ b/src/com/android/settings/network/telephony/SatelliteSetting.java
@@ -150,7 +150,7 @@
             /* In case satellite is allowed by carrier's entitlement server, the page will show
                the check icon with guidance that satellite is included in user's mobile plan */
             preference.setTitle(R.string.title_have_satellite_plan);
-            icon = getResources().getDrawable(R.drawable.ic_check_circle_24px);
+            icon = getContext().getDrawable(R.drawable.ic_check_circle_24px);
         } else {
             /* Or, it will show the blocked icon with the guidance that satellite is not included
                in user's mobile plan */
diff --git a/src/com/android/settings/network/telephony/SubscriptionRepository.kt b/src/com/android/settings/network/telephony/SubscriptionRepository.kt
index c952310..cc8c8b4 100644
--- a/src/com/android/settings/network/telephony/SubscriptionRepository.kt
+++ b/src/com/android/settings/network/telephony/SubscriptionRepository.kt
@@ -24,13 +24,14 @@
 import com.android.settings.network.SubscriptionUtil
 import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.asExecutor
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.conflate
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filterNot
+import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
@@ -52,7 +53,7 @@
     /** Flow of whether the subscription enabled for the given [subId]. */
     fun isSubscriptionEnabledFlow(subId: Int): Flow<Boolean> {
         if (!SubscriptionManager.isValidSubscriptionId(subId)) return flowOf(false)
-        return context.subscriptionsChangedFlow()
+        return subscriptionsChangedFlow()
             .map { subscriptionManager.isSubscriptionEnabled(subId) }
             .conflate()
             .onEach { Log.d(TAG, "[$subId] isSubscriptionEnabledFlow: $it") }
@@ -87,12 +88,30 @@
     }.conflate().onEach { Log.d(TAG, "subscriptions changed") }.flowOn(Dispatchers.Default)
 
     /** Flow of active subscription ids. */
-    fun activeSubscriptionIdListFlow(): Flow<List<Int>> = context.subscriptionsChangedFlow()
-        .map { subscriptionManager.activeSubscriptionIdList.sorted() }
-        .distinctUntilChanged()
-        .conflate()
-        .onEach { Log.d(TAG, "activeSubscriptionIdList: $it") }
-        .flowOn(Dispatchers.Default)
+    fun activeSubscriptionIdListFlow(): Flow<List<Int>> =
+        subscriptionsChangedFlow()
+            .map { subscriptionManager.activeSubscriptionIdList.sorted() }
+            .distinctUntilChanged()
+            .conflate()
+            .onEach { Log.d(TAG, "activeSubscriptionIdList: $it") }
+            .flowOn(Dispatchers.Default)
+
+    fun activeSubscriptionInfoFlow(subId: Int): Flow<SubscriptionInfo?> =
+        subscriptionsChangedFlow()
+            .map { subscriptionManager.getActiveSubscriptionInfo(subId) }
+            .distinctUntilChanged()
+            .conflate()
+            .flowOn(Dispatchers.Default)
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    fun phoneNumberFlow(subId: Int): Flow<String?> =
+        activeSubscriptionInfoFlow(subId).flatMapLatest { subInfo ->
+            if (subInfo != null) {
+                context.phoneNumberFlow(subInfo)
+            } else {
+                flowOf(null)
+            }
+        }
 }
 
 val Context.subscriptionManager: SubscriptionManager?
@@ -100,9 +119,12 @@
 
 fun Context.requireSubscriptionManager(): SubscriptionManager = subscriptionManager!!
 
-fun Context.phoneNumberFlow(subscriptionInfo: SubscriptionInfo) = subscriptionsChangedFlow().map {
-    SubscriptionUtil.getBidiFormattedPhoneNumber(this, subscriptionInfo)
-}.filterNot { it.isNullOrEmpty() }.flowOn(Dispatchers.Default)
+fun Context.phoneNumberFlow(subscriptionInfo: SubscriptionInfo): Flow<String?> =
+    subscriptionsChangedFlow()
+        .map { SubscriptionUtil.getBidiFormattedPhoneNumber(this, subscriptionInfo) }
+        .distinctUntilChanged()
+        .conflate()
+        .flowOn(Dispatchers.Default)
 
 fun Context.subscriptionsChangedFlow(): Flow<Unit> =
     SubscriptionRepository(this).subscriptionsChangedFlow()
diff --git a/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java b/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java
index c740847..c473456 100644
--- a/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java
+++ b/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java
@@ -25,8 +25,8 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.preference.Preference;
-import androidx.preference.PreferenceScreen;
 
 import com.android.settingslib.core.AbstractPreferenceController;
 import com.android.settingslib.notification.modes.ZenMode;
@@ -92,29 +92,14 @@
         return true;
     }
 
-    // Called by parent Fragment onAttach, for any methods (such as isAvailable()) that need
-    // zen mode info before onStart. Most callers should use updateZenMode instead, which will
-    // do any further necessary propagation.
-    protected final void setZenMode(@NonNull ZenMode zenMode) {
+    /**
+     * Assigns the {@link ZenMode} of this controller, so that it can be used later from
+     * {@link #isAvailable()} and {@link #updateState(Preference)}.
+     */
+    final void setZenMode(@NonNull ZenMode zenMode) {
         mZenMode = zenMode;
     }
 
-    // Called by the parent Fragment onStart, which means it will happen before resume.
-    public void updateZenMode(@NonNull Preference preference, @NonNull ZenMode zenMode) {
-        mZenMode = zenMode;
-        updateState(preference);
-    }
-
-    @Override
-    public void displayPreference(PreferenceScreen screen) {
-        super.displayPreference(screen);
-        if (mZenMode != null) {
-            displayPreference(screen, mZenMode);
-        }
-    }
-
-    public void displayPreference(PreferenceScreen screen, @NonNull ZenMode zenMode) {}
-
     @Override
     public final void updateState(Preference preference) {
         super.updateState(preference);
@@ -167,4 +152,20 @@
             return mode;
         });
     }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    @Nullable
+    ZenMode getZenMode() {
+        return mZenMode;
+    }
+
+    /**
+     * Convenience method for tests. Assigns the {@link ZenMode} of this controller, and calls
+     * {@link #updateState(Preference)} immediately.
+     */
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    final void updateZenMode(@NonNull Preference preference, @NonNull ZenMode zenMode) {
+        mZenMode = zenMode;
+        updateState(preference);
+    }
 }
diff --git a/src/com/android/settings/notification/modes/ManualDurationPreferenceController.java b/src/com/android/settings/notification/modes/ManualDurationPreferenceController.java
index 073f8ab..28aac63 100644
--- a/src/com/android/settings/notification/modes/ManualDurationPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ManualDurationPreferenceController.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 
+import androidx.annotation.NonNull;
 import androidx.fragment.app.Fragment;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceScreen;
@@ -49,12 +50,12 @@
         return zenMode.isManualDnd();
     }
 
-    // Called by parent fragment onAttach().
+    // Called by parent fragment onStart().
     void registerSettingsObserver() {
         mSettingsObserver.register();
     }
 
-    // Called by parent fragment onDetach().
+    // Called by parent fragment onStop().
     void unregisterSettingsObserver() {
         mSettingsObserver.unregister();
     }
@@ -69,7 +70,7 @@
     }
 
     @Override
-    public void updateState(Preference preference, ZenMode unusedZenMode) {
+    public void updateState(Preference preference, @NonNull ZenMode unusedZenMode) {
         // This controller is a link between a Settings value (ZEN_DURATION) and the manual DND
         // mode. The status of the zen mode object itself doesn't affect the preference
         // value, as that comes from settings; that value from settings will determine the
diff --git a/src/com/android/settings/notification/modes/ZenModeEditNameIconFragmentBase.java b/src/com/android/settings/notification/modes/ZenModeEditNameIconFragmentBase.java
index d666254..96cbf91 100644
--- a/src/com/android/settings/notification/modes/ZenModeEditNameIconFragmentBase.java
+++ b/src/com/android/settings/notification/modes/ZenModeEditNameIconFragmentBase.java
@@ -21,14 +21,11 @@
 
 import android.content.Context;
 import android.os.Bundle;
-import android.util.Log;
 
 import androidx.annotation.DrawableRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceScreen;
 
 import com.android.settings.R;
 import com.android.settings.dashboard.DashboardFragment;
@@ -39,7 +36,6 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 
-import java.util.Collection;
 import java.util.List;
 
 /**
@@ -79,7 +75,11 @@
                 ? icicle.getParcelable(MODE_KEY, ZenMode.class)
                 : onCreateInstantiateZenMode();
 
-        if (mZenMode == null) {
+        if (mZenMode != null) {
+            for (var controller : getZenPreferenceControllers()) {
+                controller.setZenMode(mZenMode);
+            }
+        } else {
             finish();
         }
     }
@@ -110,58 +110,32 @@
         );
     }
 
+    private Iterable<AbstractZenModePreferenceController> getZenPreferenceControllers() {
+        return getPreferenceControllers().stream()
+                .flatMap(List::stream)
+                .filter(AbstractZenModePreferenceController.class::isInstance)
+                .map(AbstractZenModePreferenceController.class::cast)
+                .toList();
+    }
+
     @VisibleForTesting(otherwise = VisibleForTesting.NONE)
     @Nullable
     ZenMode getZenMode() {
         return mZenMode;
     }
 
-    @Override
-    public void onStart() {
-        super.onStart();
-        updateControllers();
-    }
-
     @VisibleForTesting
     final void setModeName(String name) {
         checkNotNull(mZenMode).getRule().setName(Strings.nullToEmpty(name));
-        updateControllers(); // Updates confirmation button.
+        forceUpdatePreferences(); // Updates confirmation button.
     }
 
     @VisibleForTesting
     final void setModeIcon(@DrawableRes int iconResId) {
         checkNotNull(mZenMode).getRule().setIconResId(iconResId);
-        updateControllers(); // Updates icon at the top.
+        forceUpdatePreferences();  // Updates icon at the top.
     }
 
-    protected void updateControllers() {
-        PreferenceScreen screen = getPreferenceScreen();
-        Collection<List<AbstractPreferenceController>> controllers = getPreferenceControllers();
-        if (mZenMode == null || screen == null || controllers == null) {
-            return;
-        }
-        for (List<AbstractPreferenceController> list : controllers) {
-            for (AbstractPreferenceController controller : list) {
-                try {
-                    final String key = controller.getPreferenceKey();
-                    final Preference preference = screen.findPreference(key);
-                    if (preference != null) {
-                        AbstractZenModePreferenceController zenController =
-                                (AbstractZenModePreferenceController) controller;
-                        zenController.updateZenMode(preference, mZenMode);
-                    } else {
-                        Log.d(getLogTag(),
-                                String.format("Cannot find preference with key %s in Controller %s",
-                                        key, controller.getClass().getSimpleName()));
-                    }
-                    controller.displayPreference(screen);
-                } catch (ClassCastException e) {
-                    // Skip any controllers that aren't AbstractZenModePreferenceController.
-                    Log.d(getLogTag(), "Could not cast: " + controller.getClass().getSimpleName());
-                }
-            }
-        }
-    }
 
     @VisibleForTesting
     final void saveMode() {
diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java
index 5aeb34d..1b7e344 100644
--- a/src/com/android/settings/notification/modes/ZenModeFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeFragment.java
@@ -80,14 +80,6 @@
     }
 
     @Override
-    public void onAttach(@NonNull Context context) {
-        super.onAttach(context);
-
-        // allow duration preference controller to listen for settings changes
-        use(ManualDurationPreferenceController.class).registerSettingsObserver();
-    }
-
-    @Override
     public void onStart() {
         super.onStart();
 
@@ -99,6 +91,9 @@
             mModeMenuProvider = new ModeMenuProvider(mode);
             activity.addMenuProvider(mModeMenuProvider);
         }
+
+        // allow duration preference controller to listen for settings changes
+        use(ManualDurationPreferenceController.class).registerSettingsObserver();
     }
 
     @Override
@@ -106,13 +101,8 @@
         if (getActivity() != null) {
             getActivity().removeMenuProvider(mModeMenuProvider);
         }
-        super.onStop();
-    }
-
-    @Override
-    public void onDetach() {
         use(ManualDurationPreferenceController.class).unregisterSettingsObserver();
-        super.onDetach();
+        super.onStop();
     }
 
     @Override
@@ -122,13 +112,13 @@
     }
 
     @Override
-    protected void updateZenModeState() {
+    protected void onUpdatedZenModeState() {
         // Because this fragment may be asked to finish by the delete menu but not be done doing
         // so yet, ignore any attempts to update info in that case.
         if (getActivity() != null && getActivity().isFinishing()) {
             return;
         }
-        super.updateZenModeState();
+        super.onUpdatedZenModeState();
     }
 
     private class ModeMenuProvider implements MenuProvider {
diff --git a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java
index f461fc3..c63b3a8 100644
--- a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java
+++ b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java
@@ -18,24 +18,18 @@
 
 import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
-import android.content.Context;
 import android.os.Bundle;
 import android.util.Log;
 import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceScreen;
+import androidx.lifecycle.Lifecycle;
 
 import com.android.settings.R;
-import com.android.settingslib.core.AbstractPreferenceController;
 import com.android.settingslib.notification.modes.ZenMode;
 
-import com.google.common.base.Preconditions;
-
 import java.util.List;
-import java.util.function.Consumer;
 
 /**
  * Base class for Settings pages used to configure individual modes.
@@ -43,13 +37,27 @@
 abstract class ZenModeFragmentBase extends ZenModesFragmentBase {
     static final String TAG = "ZenModeSettings";
 
-    @Nullable  // only until reloadMode() is called
-    private ZenMode mZenMode;
+    @Nullable private ZenMode mZenMode;
+    @Nullable private ZenMode mModeOnLastControllerUpdate;
 
     @Override
-    public void onAttach(@NonNull Context context) {
-        super.onAttach(context);
+    public void onCreate(Bundle icicle) {
+        mZenMode = loadModeFromArguments();
+        if (mZenMode != null) {
+            // Propagate mode info through to controllers. Must be done before super.onCreate(),
+            // because that one calls AbstractPreferenceController.isAvailable().
+            for (var controller : getZenPreferenceControllers()) {
+                controller.setZenMode(mZenMode);
+            }
+        } else {
+            toastAndFinish();
+        }
 
+        super.onCreate(icicle);
+    }
+
+    @Nullable
+    private ZenMode loadModeFromArguments() {
         String id = null;
         if (getActivity() != null && getActivity().getIntent() != null) {
             id = getActivity().getIntent().getStringExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID);
@@ -60,93 +68,65 @@
         }
         if (id == null) {
             Log.d(TAG, "No id provided");
-            toastAndFinish();
-            return;
+            return null;
         }
-        if (!reloadMode(id)) {
-            Log.d(TAG, "Mode id " + id + " not found");
-            toastAndFinish();
-            return;
+
+        ZenMode mode = mBackend.getMode(id);
+        if (mode == null) {
+            Log.d(TAG, "Mode with id " + id + " not found");
+            return null;
         }
-        if (mZenMode != null) {
-            // Propagate mode info through to controllers.
-            for (List<AbstractPreferenceController> list : getPreferenceControllers()) {
-                try {
-                    for (AbstractPreferenceController controller : list) {
-                        // mZenMode guaranteed non-null from reloadMode() above
-                        ((AbstractZenModePreferenceController) controller).setZenMode(mZenMode);
-                    }
-                } catch (ClassCastException e) {
-                    // ignore controllers that aren't AbstractZenModePreferenceController
-                }
-            }
-        }
+        return mode;
     }
 
-    /**
-     * Refresh stored ZenMode data.
-     * @param id the mode ID
-     * @return whether we successfully got mode data from the backend.
-     */
-    private boolean reloadMode(String id) {
-        mZenMode = mBackend.getMode(id);
-        if (mZenMode == null) {
-            return false;
-        }
-        return true;
+    private Iterable<AbstractZenModePreferenceController> getZenPreferenceControllers() {
+        return getPreferenceControllers().stream()
+                .flatMap(List::stream)
+                .filter(AbstractZenModePreferenceController.class::isInstance)
+                .map(AbstractZenModePreferenceController.class::cast)
+                .toList();
     }
 
-    /**
-     * Refresh ZenMode data any time the system's zen mode state changes (either the zen mode value
-     * itself, or the config), and also (once updated) update the info for all controllers.
-     */
     @Override
-    protected void updateZenModeState() {
+    protected void onUpdatedZenModeState() {
         if (mZenMode == null) {
-            // This shouldn't happen, but guard against it in case
+            Log.wtf(TAG, "mZenMode is null in onUpdatedZenModeState");
             toastAndFinish();
             return;
         }
+
         String id = mZenMode.getId();
-        if (!reloadMode(id)) {
+        ZenMode mode = mBackend.getMode(id);
+        if (mode == null) {
             Log.d(TAG, "Mode id=" + id + " not found");
             toastAndFinish();
             return;
         }
-        updateControllers();
+
+        mZenMode = mode;
+        maybeUpdateControllersState(mode);
     }
 
-    private void updateControllers() {
-        if (getPreferenceControllers() == null || mZenMode == null) {
-            return;
+    /**
+     * Updates all {@link AbstractZenModePreferenceController} based on the loaded mode info.
+     * For each controller, {@link AbstractZenModePreferenceController#setZenMode} will be called.
+     * Then, {@link AbstractZenModePreferenceController#updateState} will be called as well, unless
+     * we determine it's not necessary (for example, if we know that {@code DashboardFragment} will
+     * do it soon).
+     */
+    private void maybeUpdateControllersState(@NonNull ZenMode zenMode) {
+        boolean needsFullUpdate =
+                getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)
+                && (mModeOnLastControllerUpdate == null
+                        || !mModeOnLastControllerUpdate.equals(zenMode));
+        mModeOnLastControllerUpdate = zenMode.copy();
+
+        for (var controller : getZenPreferenceControllers()) {
+            controller.setZenMode(zenMode);
         }
 
-        final PreferenceScreen screen = getPreferenceScreen();
-        if (screen == null) {
-            Log.d(TAG, "PreferenceScreen not found");
-            return;
-        }
-        for (List<AbstractPreferenceController> list : getPreferenceControllers()) {
-            for (AbstractPreferenceController controller : list) {
-                try {
-                    // Find preference associated with controller
-                    final String key = controller.getPreferenceKey();
-                    final Preference preference = screen.findPreference(key);
-                    if (preference != null) {
-                        AbstractZenModePreferenceController zenController =
-                                (AbstractZenModePreferenceController) controller;
-                        zenController.updateZenMode(preference, mZenMode);
-                    } else {
-                        Log.d(TAG,
-                                String.format("Cannot find preference with key %s in Controller %s",
-                                        key, controller.getClass().getSimpleName()));
-                    }
-                    controller.displayPreference(screen);
-                } catch (ClassCastException e) {
-                    // Skip any controllers that aren't AbstractZenModePreferenceController.
-                    Log.d(TAG, "Could not cast: " + controller.getClass().getSimpleName());
-                }
-            }
+        if (needsFullUpdate) {
+            forceUpdatePreferences();
         }
     }
 
@@ -163,16 +143,4 @@
     public ZenMode getMode() {
         return mZenMode;
     }
-
-    protected final boolean saveMode(Consumer<ZenMode> updater) {
-        Preconditions.checkState(mBackend != null);
-        ZenMode mode = mZenMode;
-        if (mode == null) {
-            Log.wtf(TAG, "Cannot save mode, it hasn't been loaded (" + getClass() + ")");
-            return false;
-        }
-        updater.accept(mode);
-        mBackend.updateMode(mode);
-        return true;
-    }
 }
diff --git a/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java b/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java
index 878a508..e4c3f32 100644
--- a/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java
@@ -67,12 +67,18 @@
         LayoutPreference layoutPref = (LayoutPreference) preference;
 
         TextView start = layoutPref.findViewById(R.id.start_time);
-        start.setText(timeString(mSchedule.startHour, mSchedule.startMinute));
+        String startTimeString = timeString(mSchedule.startHour, mSchedule.startMinute);
+        start.setText(startTimeString);
+        start.setContentDescription(
+                mContext.getString(R.string.zen_mode_start_time) + "\n" + startTimeString);
         start.setOnClickListener(
                 timePickerLauncher(mSchedule.startHour, mSchedule.startMinute, mStartSetter));
 
         TextView end = layoutPref.findViewById(R.id.end_time);
-        end.setText(timeString(mSchedule.endHour, mSchedule.endMinute));
+        String endTimeString = timeString(mSchedule.endHour, mSchedule.endMinute);
+        end.setText(endTimeString);
+        end.setContentDescription(
+                mContext.getString(R.string.zen_mode_end_time) + "\n" + endTimeString);
         end.setOnClickListener(
                 timePickerLauncher(mSchedule.endHour, mSchedule.endMinute, mEndSetter));
 
@@ -198,7 +204,10 @@
             // day label.
             dayToggle.setTextOn(mShortDayFormat.format(c.getTime()));
             dayToggle.setTextOff(mShortDayFormat.format(c.getTime()));
-            dayToggle.setContentDescription(mLongDayFormat.format(c.getTime()));
+            String state = dayEnabled
+                    ? mContext.getString(com.android.internal.R.string.capital_on)
+                    : mContext.getString(com.android.internal.R.string.capital_off);
+            dayToggle.setStateDescription(mLongDayFormat.format(c.getTime()) + ", " + state);
 
             dayToggle.setChecked(dayEnabled);
             dayToggle.setOnCheckedChangeListener((buttonView, isChecked) -> {
diff --git a/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java b/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java
index 043a38c..3ee6d94 100644
--- a/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeTriggerUpdatePreferenceController.java
@@ -37,7 +37,6 @@
 import androidx.annotation.StringRes;
 import androidx.annotation.VisibleForTesting;
 import androidx.preference.Preference;
-import androidx.preference.PreferenceScreen;
 
 import com.android.settings.R;
 import com.android.settingslib.PrimarySwitchPreference;
@@ -78,13 +77,6 @@
     }
 
     @Override
-    public void displayPreference(PreferenceScreen screen, @NonNull ZenMode zenMode) {
-        // Preload approved components, but only for the package that owns the rule (since it's the
-        // only package that can have a valid configurationActivity).
-        mServiceListing.loadApprovedComponents(zenMode.getRule().getPackageName());
-    }
-
-    @Override
     void updateState(Preference preference, @NonNull ZenMode zenMode) {
         if (!isAvailable(zenMode)) {
             return;
@@ -137,6 +129,7 @@
     @SuppressLint("SwitchIntDef")
     private void setUpForAppTrigger(Preference preference, ZenMode mode) {
         // App-owned mode may have triggerDescription, configurationActivity, or both/neither.
+        mServiceListing.loadApprovedComponents(mode.getRule().getPackageName());
         Intent configurationIntent =
                 mConfigurationActivityHelper.getConfigurationActivityIntentForMode(
                         mode, mServiceListing::findService);
diff --git a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java
index 0bc0617..652415b 100644
--- a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java
+++ b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java
@@ -16,14 +16,11 @@
 
 package com.android.settings.notification.modes;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.content.Context;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
 import android.os.UserManager;
-import android.provider.Settings.Global;
 import android.util.Log;
 
 import androidx.annotation.VisibleForTesting;
@@ -38,17 +35,10 @@
     protected static final String TAG = "ZenModesSettings";
     protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
-    private final Handler mHandler = new Handler();
-    private final SettingsObserver mSettingsObserver = new SettingsObserver();
-
     protected Context mContext;
-
     protected ZenModesBackend mBackend;
     protected ZenHelperBackend mHelperBackend;
-
-    // Individual pages must implement this method based on what they should do when
-    // the device's zen mode state changes.
-    protected abstract void updateZenModeState();
+    private ZenSettingsObserver mSettingsObserver;
 
     ZenModesFragmentBase() {
         super(UserManager.DISALLOW_ADJUST_VOLUME);
@@ -69,8 +59,8 @@
         mContext = context;
         mBackend = ZenModesBackend.getInstance(context);
         mHelperBackend = ZenHelperBackend.getInstance(context);
+        mSettingsObserver = new ZenSettingsObserver(context, this::onUpdatedZenModeState);
         super.onAttach(context);
-        mSettingsObserver.register();
     }
 
     @Override
@@ -83,45 +73,20 @@
                 finish();
             }
         }
+
+        onUpdatedZenModeState(); // Maybe, while we weren't observing.
+        checkNotNull(mSettingsObserver).register();
     }
 
+    /**
+     * Called by this fragment when we know or suspect that Zen Modes data or state has changed.
+     * Individual pages must implement this method to refresh whatever they're displaying.
+     */
+    protected abstract void onUpdatedZenModeState();
+
     @Override
-    public void onResume() {
-        super.onResume();
-        updateZenModeState();
-    }
-
-    @Override
-    public void onDetach() {
-        super.onDetach();
-        mSettingsObserver.unregister();
-    }
-
-    private final class SettingsObserver extends ContentObserver {
-        private static final Uri ZEN_MODE_URI = Global.getUriFor(Global.ZEN_MODE);
-        private static final Uri ZEN_MODE_CONFIG_ETAG_URI = Global.getUriFor(
-                Global.ZEN_MODE_CONFIG_ETAG);
-
-        private SettingsObserver() {
-            super(mHandler);
-        }
-
-        public void register() {
-            getContentResolver().registerContentObserver(ZEN_MODE_URI, false, this);
-            getContentResolver().registerContentObserver(ZEN_MODE_CONFIG_ETAG_URI, false, this);
-        }
-
-        public void unregister() {
-            getContentResolver().unregisterContentObserver(this);
-        }
-
-        @Override
-        public void onChange(boolean selfChange, @Nullable Uri uri) {
-            super.onChange(selfChange, uri);
-            // Shouldn't have any other URIs trigger this method, but check just in case.
-            if (ZEN_MODE_URI.equals(uri) || ZEN_MODE_CONFIG_ETAG_URI.equals(uri)) {
-                updateZenModeState();
-            }
-        }
+    public void onStop() {
+        checkNotNull(mSettingsObserver).unregister();
+        super.onStop();
     }
 }
diff --git a/src/com/android/settings/notification/modes/ZenModesListFragment.java b/src/com/android/settings/notification/modes/ZenModesListFragment.java
index be458b3..a45ca17 100644
--- a/src/com/android/settings/notification/modes/ZenModesListFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModesListFragment.java
@@ -65,7 +65,7 @@
     }
 
     @Override
-    protected void updateZenModeState() {
+    protected void onUpdatedZenModeState() {
         // TODO: b/322373473 -- update any overall description of modes state here if necessary.
         // Note the preferences linking to individual rules do not need to be updated, as
         // updateState() is called on all preference controllers whenever the page is resumed.
diff --git a/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java b/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java
index ba12b9a..12b7278 100644
--- a/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java
@@ -38,7 +38,8 @@
  * containing links to each individual mode. This is a central controller that populates and updates
  * all the preferences that then lead to a mode configuration page.
  */
-class ZenModesListPreferenceController extends BasePreferenceController {
+class ZenModesListPreferenceController extends BasePreferenceController
+        implements BasePreferenceController.UiBlocker {
     protected static final String KEY = "zen_modes_list";
 
     protected ZenModesBackend mBackend;
@@ -49,11 +50,6 @@
     }
 
     @Override
-    public String getPreferenceKey() {
-        return KEY;
-    }
-
-    @Override
     @AvailabilityStatus
     public int getAvailabilityStatus() {
         return Flags.modesUi() ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE;
@@ -97,6 +93,8 @@
         for (String key : originalPreferences.keySet()) {
             category.removePreferenceRecursively(key);
         }
+
+        setUiBlockerFinished(true);
     }
 
     // Provide search data for the modes, which will allow users to reach the modes page if they
diff --git a/src/com/android/settings/notification/modes/ZenSettingsObserver.java b/src/com/android/settings/notification/modes/ZenSettingsObserver.java
new file mode 100644
index 0000000..a853646
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenSettingsObserver.java
@@ -0,0 +1,68 @@
+/*
+ * 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.settings.notification.modes;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.provider.Settings;
+
+import androidx.annotation.Nullable;
+
+class ZenSettingsObserver extends ContentObserver {
+    private static final Uri ZEN_MODE_URI = Settings.Global.getUriFor(Settings.Global.ZEN_MODE);
+    private static final Uri ZEN_MODE_CONFIG_ETAG_URI = Settings.Global.getUriFor(
+            Settings.Global.ZEN_MODE_CONFIG_ETAG);
+
+    private final Context mContext;
+    @Nullable private Runnable mCallback;
+
+    ZenSettingsObserver(Context context) {
+        this(context, null);
+    }
+
+    ZenSettingsObserver(Context context, @Nullable Runnable callback) {
+        super(context.getMainExecutor(), 0);
+        mContext = context;
+        setOnChangeListener(callback);
+    }
+
+    void register() {
+        mContext.getContentResolver().registerContentObserver(ZEN_MODE_URI, false, this);
+        mContext.getContentResolver().registerContentObserver(ZEN_MODE_CONFIG_ETAG_URI, false,
+                this);
+    }
+
+    void unregister() {
+        mContext.getContentResolver().unregisterContentObserver(this);
+    }
+
+    void setOnChangeListener(@Nullable Runnable callback) {
+        mCallback = callback;
+    }
+
+    @Override
+    public void onChange(boolean selfChange, @Nullable Uri uri) {
+        super.onChange(selfChange, uri);
+        // Shouldn't have any other URIs trigger this method, but check just in case.
+        if (ZEN_MODE_URI.equals(uri) || ZEN_MODE_CONFIG_ETAG_URI.equals(uri)) {
+            if (mCallback != null) {
+                mCallback.run();
+            }
+        }
+    }
+}
diff --git a/src/com/android/settings/password/ChooseLockGeneric.java b/src/com/android/settings/password/ChooseLockGeneric.java
index d5d079e..34c0731 100644
--- a/src/com/android/settings/password/ChooseLockGeneric.java
+++ b/src/com/android/settings/password/ChooseLockGeneric.java
@@ -495,7 +495,7 @@
                         mBiometricsAuthSuccessful, mWaitingForConfirmation, mUserId)) {
                     mWaitingForConfirmation = true;
                     Utils.launchBiometricPromptForMandatoryBiometrics(this, BIOMETRIC_AUTH_REQUEST,
-                            mUserId);
+                            mUserId, true /* hideBackground */);
                 }
             } else if (requestCode == BIOMETRIC_AUTH_REQUEST) {
                 if (resultCode == Activity.RESULT_OK) {
diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java
index c0b3093..4f35532 100644
--- a/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java
+++ b/src/com/android/settings/password/ConfirmDeviceCredentialActivity.java
@@ -80,6 +80,8 @@
     public static final String BIOMETRIC_PROMPT_AUTHENTICATORS = "biometric_prompt_authenticators";
     public static final String BIOMETRIC_PROMPT_NEGATIVE_BUTTON_TEXT =
             "biometric_prompt_negative_button_text";
+    public static final String BIOMETRIC_PROMPT_HIDE_BACKGROUND =
+            "biometric_prompt_hide_background";
 
     public static class InternalActivity extends ConfirmDeviceCredentialActivity {
     }
@@ -165,15 +167,20 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
-        getWindow().setStatusBarColor(Color.TRANSPARENT);
+        final Intent intent = getIntent();
+        if (intent.getBooleanExtra(BIOMETRIC_PROMPT_HIDE_BACKGROUND, false)) {
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+            getWindow().setDimAmount(1);
+            intent.removeExtra(BIOMETRIC_PROMPT_HIDE_BACKGROUND);
+        } else {
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+            getWindow().setStatusBarColor(Color.TRANSPARENT);
+        }
 
         mDevicePolicyManager = getSystemService(DevicePolicyManager.class);
         mUserManager = UserManager.get(this);
         mTrustManager = getSystemService(TrustManager.class);
         mLockPatternUtils = new LockPatternUtils(this);
-
-        Intent intent = getIntent();
         mContext = this;
         mCheckDevicePolicyManager = intent
                 .getBooleanExtra(KeyguardManager.EXTRA_DISALLOW_BIOMETRICS_IF_POLICY_EXISTS, false);
diff --git a/src/com/android/settings/wifi/repository/WifiStatusRepository.kt b/src/com/android/settings/wifi/repository/WifiStatusRepository.kt
index f97ed49..fe4ba6c 100644
--- a/src/com/android/settings/wifi/repository/WifiStatusRepository.kt
+++ b/src/com/android/settings/wifi/repository/WifiStatusRepository.kt
@@ -50,14 +50,20 @@
                 var wifiStatusTracker: WifiStatusTracker? = null
                 wifiStatusTracker = wifiStatusTrackerFactory { wifiStatusTracker?.let(::trySend) }
 
+                // Fetches initial state first, before set listening to true, otherwise could cause
+                // race condition.
+                wifiStatusTracker.fetchInitialState()
+                trySend(wifiStatusTracker)
+
                 context
                     .broadcastReceiverFlow(INTENT_FILTER)
-                    .onEach { intent -> wifiStatusTracker.handleBroadcast(intent) }
+                    .onEach { intent ->
+                        wifiStatusTracker.handleBroadcast(intent)
+                        trySend(wifiStatusTracker)
+                    }
                     .launchIn(this)
 
                 wifiStatusTracker.setListening(true)
-                wifiStatusTracker.fetchInitialState()
-                trySend(wifiStatusTracker)
 
                 awaitClose { wifiStatusTracker.setListening(false) }
             }
diff --git a/src/com/android/settings/wifi/slice/WifiSlice.java b/src/com/android/settings/wifi/slice/WifiSlice.java
index ff448a8..3bb50d3 100644
--- a/src/com/android/settings/wifi/slice/WifiSlice.java
+++ b/src/com/android/settings/wifi/slice/WifiSlice.java
@@ -431,7 +431,7 @@
         boolean isSatelliteOn = false;
         try {
             isSatelliteOn =
-                    satelliteRepository.requestIsEnabled(Executors.newSingleThreadExecutor())
+                    satelliteRepository.requestIsSessionStarted(Executors.newSingleThreadExecutor())
                             .get(2000, TimeUnit.MILLISECONDS);
         } catch (ExecutionException | TimeoutException | InterruptedException e) {
             Log.e(TAG, "Error to get satellite status : " + e);
diff --git a/tests/robotests/res/xml/modes_fake_settings.xml b/tests/robotests/res/xml/modes_fake_settings.xml
new file mode 100644
index 0000000..a5602dc
--- /dev/null
+++ b/tests/robotests/res/xml/modes_fake_settings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+    <Preference android:key="pref_id" />
+    <Preference android:key="pref_name" />
+    <Preference android:key="pref_enabled" />
+</PreferenceScreen>
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/UtilsTest.java b/tests/robotests/src/com/android/settings/UtilsTest.java
index b36e9d6..2aeb906 100644
--- a/tests/robotests/src/com/android/settings/UtilsTest.java
+++ b/tests/robotests/src/com/android/settings/UtilsTest.java
@@ -22,6 +22,7 @@
 
 import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME;
 import static com.android.settings.password.ConfirmDeviceCredentialActivity.BIOMETRIC_PROMPT_AUTHENTICATORS;
+import static com.android.settings.password.ConfirmDeviceCredentialActivity.BIOMETRIC_PROMPT_HIDE_BACKGROUND;
 import static com.android.settings.password.ConfirmDeviceCredentialActivity.BIOMETRIC_PROMPT_NEGATIVE_BUTTON_TEXT;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -581,7 +582,8 @@
 
         final int requestCode = 1;
         final ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
-        Utils.launchBiometricPromptForMandatoryBiometrics(mFragment, requestCode, USER_ID);
+        Utils.launchBiometricPromptForMandatoryBiometrics(mFragment, requestCode, USER_ID,
+                false /* hideBackground */);
 
         verify(mFragment).startActivityForResult(intentArgumentCaptor.capture(), eq(requestCode));
 
@@ -593,6 +595,8 @@
         assertThat(intent.getExtra(KeyguardManager.EXTRA_DESCRIPTION)).isNotNull();
         assertThat(intent.getBooleanExtra(ChooseLockSettingsHelper.EXTRA_KEY_ALLOW_ANY_USER, false))
                 .isTrue();
+        assertThat(intent.getBooleanExtra(BIOMETRIC_PROMPT_HIDE_BACKGROUND, true))
+                .isFalse();
         assertThat(intent.getIntExtra(Intent.EXTRA_USER_ID, 0)).isEqualTo(USER_ID);
         assertThat(intent.getComponent().getPackageName()).isEqualTo(SETTINGS_PACKAGE_NAME);
         assertThat(intent.getComponent().getClassName()).isEqualTo(
diff --git a/tests/robotests/src/com/android/settings/development/DesktopModeSecondaryDisplayPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/DesktopModeSecondaryDisplayPreferenceControllerTest.java
index 5931004..3691d12 100644
--- a/tests/robotests/src/com/android/settings/development/DesktopModeSecondaryDisplayPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/development/DesktopModeSecondaryDisplayPreferenceControllerTest.java
@@ -16,6 +16,7 @@
 
 package com.android.settings.development;
 
+import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT;
 import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
 
 import static com.android.settings.development.DesktopModeSecondaryDisplayPreferenceController.SETTING_VALUE_OFF;
@@ -55,6 +56,7 @@
 
     private static final String ENG_BUILD_TYPE = "eng";
     private static final String USER_BUILD_TYPE = "user";
+    private static final int SETTING_VALUE_INVALID = -1;
 
     @Mock
     private SwitchPreference mPreference;
@@ -102,21 +104,41 @@
 
     @Test
     public void onPreferenceChange_switchEnabled_enablesDesktopModeOnSecondaryDisplay() {
-        mController.onPreferenceChange(mPreference, true /* new value */);
+        mController.onPreferenceChange(mPreference, /* newValue= */ true);
 
         final int mode = Settings.Global.getInt(mContext.getContentResolver(),
-                DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, -1 /* default */);
+                DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
+                /* def= */ SETTING_VALUE_INVALID);
         assertThat(mode).isEqualTo(SETTING_VALUE_ON);
 
         verify(mTransaction).add(any(RebootConfirmationDialogFragment.class), any());
     }
 
     @Test
-    public void onPreferenceChange_switchDisabled_disablesDesktopModeOnSecondaryDisplay() {
-        mController.onPreferenceChange(mPreference, false /* new value */);
+    public void onPreferenceChange_switchEnabled_enablesFreeformSupport() {
+        mController.onPreferenceChange(mPreference, /* newValue= */ true);
 
         final int mode = Settings.Global.getInt(mContext.getContentResolver(),
-                DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, -1 /* default */);
+                DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, /* def= */ SETTING_VALUE_INVALID);
+        assertThat(mode).isEqualTo(SETTING_VALUE_ON);
+    }
+
+    @Test
+    public void onPreferenceChange_switchDisabled_disablesDesktopModeOnSecondaryDisplay() {
+        mController.onPreferenceChange(mPreference, /* newValue= */ false);
+
+        final int mode = Settings.Global.getInt(mContext.getContentResolver(),
+                DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
+                /* def= */ SETTING_VALUE_INVALID);
+        assertThat(mode).isEqualTo(SETTING_VALUE_OFF);
+    }
+
+    @Test
+    public void onPreferenceChange_switchDisabled_disablesFreeformSupport() {
+        mController.onPreferenceChange(mPreference, /* newValue= */ false);
+
+        final int mode = Settings.Global.getInt(mContext.getContentResolver(),
+                DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, /* def= */ SETTING_VALUE_INVALID);
         assertThat(mode).isEqualTo(SETTING_VALUE_OFF);
     }
 
@@ -145,7 +167,8 @@
         mController.onDeveloperOptionsSwitchDisabled();
 
         final int mode = Settings.Global.getInt(mContext.getContentResolver(),
-                DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, -1 /* default */);
+                DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
+                /* def= */ SETTING_VALUE_INVALID);
         assertThat(mode).isEqualTo(SETTING_VALUE_OFF);
         verify(mPreference).setEnabled(false);
     }
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java
index 851dc79..8d6cc08 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java
@@ -75,7 +75,8 @@
     private static final String STATUS_CHARGING_TIME = "50% - 0 min left until full";
     private static final String STATUS_NOT_CHARGING = "Not charging";
     private static final String STATUS_CHARGING_FUTURE_BYPASS = "50% - Charging";
-    private static final String STATUS_CHARGING_PAUSED = "50% - Charging optimized";
+    private static final String STATUS_CHARGING_PAUSED =
+            "50% - Charging on hold to protect battery";
     private static final long REMAINING_TIME_NULL = -1;
     private static final long REMAINING_TIME = 2;
     // Strings are defined in frameworks/base/packages/SettingsLib/res/values/strings.xml
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeFragmentBaseTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeFragmentBaseTest.java
new file mode 100644
index 0000000..21f19ff
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeFragmentBaseTest.java
@@ -0,0 +1,364 @@
+/*
+ * 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.settings.notification.modes;
+
+import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
+
+import static com.android.settings.notification.modes.CharSequenceTruth.assertThat;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.Flags;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.testing.FragmentScenario;
+import androidx.lifecycle.Lifecycle.State;
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.notification.modes.TestModeBuilder;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.shadows.ShadowContentResolver;
+import org.robolectric.shadows.ShadowLooper;
+
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@EnableFlags(Flags.FLAG_MODES_UI)
+public class ZenModeFragmentBaseTest {
+
+    private static final Uri SETTINGS_URI = Settings.Global.getUriFor(
+            Settings.Global.ZEN_MODE_CONFIG_ETAG);
+
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Mock ZenModesBackend mBackend;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void fragment_noArguments_finishes() {
+        when(mBackend.getMode(any())).thenReturn(TestModeBuilder.EXAMPLE);
+
+        FragmentScenario<TestableFragment> scenario = createScenario(null);
+
+        scenario.moveToState(State.RESUMED).onFragment(fragment -> {
+            assertThat(fragment.requireActivity().isFinishing()).isTrue();
+        });
+
+        scenario.close();
+    }
+
+    @Test
+    public void fragment_modeDoesNotExist_finishes() {
+        when(mBackend.getMode(any())).thenReturn(null);
+
+        FragmentScenario<TestableFragment> scenario = createScenario("mode_id");
+
+        scenario.moveToState(State.RESUMED).onFragment(fragment -> {
+            assertThat(fragment.requireActivity().isFinishing()).isTrue();
+        });
+
+        scenario.close();
+    }
+
+    @Test
+    public void fragment_validMode_updatesControllersOnce() {
+        ZenMode mode = new TestModeBuilder().setId("mode_id").build();
+        when(mBackend.getMode("mode_id")).thenReturn(mode);
+
+        FragmentScenario<TestableFragment> scenario = createScenario("mode_id");
+
+        scenario.moveToState(State.CREATED).onFragment(fragment -> {
+            assertThat(fragment.mShowsId.getZenMode()).isEqualTo(mode);
+            assertThat(fragment.mShowsId.isAvailable()).isTrue();
+            assertThat(fragment.mAvailableIfEnabled.getZenMode()).isEqualTo(mode);
+            assertThat(fragment.mAvailableIfEnabled.isAvailable()).isTrue();
+
+            verify(fragment.mShowsId, never()).updateState(any(), any());
+            verify(fragment.mAvailableIfEnabled, never()).updateState(any(), any());
+        });
+
+        scenario.moveToState(State.RESUMED).onFragment(fragment -> {
+            Preference preferenceOne = fragment.requirePreference("pref_id");
+            assertThat(preferenceOne.getSummary()).isEqualTo("Id is mode_id");
+
+            verify(fragment.mShowsId).updateState(any(), eq(mode));
+            verify(fragment.mAvailableIfEnabled).updateState(any(), eq(mode));
+        });
+
+        scenario.close();
+    }
+
+    @Test
+    public void fragment_onStartToOnStop_hasRegisteredContentObserver() {
+        when(mBackend.getMode(any())).thenReturn(TestModeBuilder.EXAMPLE);
+        FragmentScenario<TestableFragment> scenario = createScenario("id");
+
+        scenario.moveToState(State.CREATED).onFragment(fragment ->
+                assertThat(getSettingsContentObservers(fragment)).isEmpty());
+
+        scenario.moveToState(State.STARTED).onFragment(fragment ->
+                assertThat(getSettingsContentObservers(fragment)).hasSize(1));
+
+        scenario.moveToState(State.RESUMED).onFragment(fragment ->
+                assertThat(getSettingsContentObservers(fragment)).hasSize(1));
+
+        scenario.moveToState(State.STARTED).onFragment(fragment ->
+                assertThat(getSettingsContentObservers(fragment)).hasSize(1));
+
+        scenario.moveToState(State.CREATED).onFragment(fragment ->
+                assertThat(getSettingsContentObservers(fragment)).isEmpty());
+
+        scenario.close();
+    }
+
+    @Test
+    public void fragment_onModeUpdatedWithDifferences_updatesControllers() {
+        ZenMode originalMode = new TestModeBuilder().setId("id").setName("Original").build();
+        when(mBackend.getMode("id")).thenReturn(originalMode);
+
+        FragmentScenario<TestableFragment> scenario = createScenario("id");
+        scenario.moveToState(State.RESUMED).onFragment(fragment -> {
+            Preference preference = fragment.requirePreference("pref_name");
+            assertThat(preference.getSummary()).isEqualTo("Original");
+            verify(fragment.mShowsName, times(1)).updateState(any(), eq(originalMode));
+
+            // Now, we get a message saying something changed.
+            ZenMode updatedMode = new TestModeBuilder().setId("id").setName("Updated").build();
+            when(mBackend.getMode("id")).thenReturn(updatedMode);
+            getSettingsContentObservers(fragment).stream().findFirst().get()
+                    .dispatchChange(false, SETTINGS_URI);
+            ShadowLooper.idleMainLooper();
+
+            // The screen was updated, and only updated once.
+            assertThat(preference.getSummary()).isEqualTo("Updated");
+            verify(fragment.mShowsName, times(1)).updateState(any(), eq(updatedMode));
+        });
+
+        scenario.close();
+    }
+
+    @Test
+    public void fragment_onModeUpdatedWithoutDifferences_setsModeInControllersButNothingElse() {
+        ZenMode originalMode = new TestModeBuilder().setId("id").setName("Original").build();
+        when(mBackend.getMode("id")).thenReturn(originalMode);
+
+        FragmentScenario<TestableFragment> scenario = createScenario("id");
+        scenario.moveToState(State.RESUMED).onFragment(fragment -> {
+            Preference preference = fragment.requirePreference("pref_name");
+            assertThat(preference.getSummary()).isEqualTo("Original");
+            verify(fragment.mShowsName, times(1)).updateState(any(), eq(originalMode));
+
+            // Now, we get a message saying something changed, but it was for a different mode.
+            ZenMode notUpdatedMode = new TestModeBuilder(originalMode).build();
+            when(mBackend.getMode("id")).thenReturn(notUpdatedMode);
+            getSettingsContentObservers(fragment).stream().findFirst().get()
+                    .dispatchChange(false, SETTINGS_URI);
+            ShadowLooper.idleMainLooper();
+
+            // The mode instance was updated, but updateState() was not called.
+            assertThat(preference.getSummary()).isEqualTo("Original");
+            assertThat(fragment.mShowsName.getZenMode()).isSameInstanceAs(notUpdatedMode);
+            verify(fragment.mShowsName, never()).updateState(any(), same(notUpdatedMode));
+        });
+
+        scenario.close();
+    }
+
+    @Test
+    public void fragment_onFragmentRestart_reloadsMode() {
+        ZenMode originalMode = new TestModeBuilder().setId("id").setName("Original").build();
+        when(mBackend.getMode("id")).thenReturn(originalMode);
+
+        FragmentScenario<TestableFragment> scenario = createScenario("id");
+        scenario.moveToState(State.RESUMED).onFragment(fragment -> {
+            Preference preference = fragment.requirePreference("pref_name");
+            assertThat(preference.getSummary()).isEqualTo("Original");
+            verify(fragment.mShowsName, times(1)).updateState(any(), eq(originalMode));
+        });
+
+        ZenMode updatedMode = new TestModeBuilder().setId("id").setName("Updated").build();
+        when(mBackend.getMode("id")).thenReturn(updatedMode);
+
+        scenario.moveToState(State.CREATED).moveToState(State.RESUMED).onFragment(fragment -> {
+            Preference preference = fragment.requirePreference("pref_name");
+            assertThat(preference.getSummary()).isEqualTo("Updated");
+            assertThat(fragment.mShowsName.getZenMode()).isSameInstanceAs(updatedMode);
+        });
+
+        scenario.close();
+    }
+
+    @Test
+    public void fragment_onModeDeleted_finishes() {
+        ZenMode originalMode = new TestModeBuilder().setId("mode_id").build();
+        when(mBackend.getMode("mode_id")).thenReturn(originalMode);
+
+        FragmentScenario<TestableFragment> scenario = createScenario("mode_id");
+        scenario.moveToState(State.RESUMED).onFragment(fragment -> {
+            assertThat(fragment.requireActivity().isFinishing()).isFalse();
+
+            // Now it's no longer there...
+            when(mBackend.getMode(any())).thenReturn(null);
+            getSettingsContentObservers(fragment).stream().findFirst().get()
+                    .dispatchChange(false, SETTINGS_URI);
+            ShadowLooper.idleMainLooper();
+
+            assertThat(fragment.requireActivity().isFinishing()).isTrue();
+        });
+
+        scenario.close();
+    }
+
+    private FragmentScenario<TestableFragment> createScenario(@Nullable String modeId) {
+        Bundle fragmentArgs = null;
+        if (modeId != null) {
+            fragmentArgs = new Bundle();
+            fragmentArgs.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, modeId);
+        }
+
+        FragmentScenario<TestableFragment> scenario = FragmentScenario.launch(
+                TestableFragment.class, fragmentArgs, 0, State.INITIALIZED);
+
+        scenario.onFragment(fragment -> {
+            fragment.setBackend(mBackend); // Before onCreate().
+        });
+
+        return scenario;
+    }
+
+    public static class TestableFragment extends ZenModeFragmentBase {
+
+        private ShowsIdPreferenceController mShowsId;
+        private ShowsNamePreferenceController mShowsName;
+        private AvailableIfEnabledPreferenceController mAvailableIfEnabled;
+
+        @Override
+        protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
+            mShowsId = spy(new ShowsIdPreferenceController(context, "pref_id"));
+            mShowsName = spy(new ShowsNamePreferenceController(context, "pref_name"));
+            mAvailableIfEnabled = spy(
+                    new AvailableIfEnabledPreferenceController(context, "pref_enabled"));
+            return ImmutableList.of(mShowsId, mShowsName, mAvailableIfEnabled);
+        }
+
+        @NonNull
+        Preference requirePreference(String key) {
+            Preference preference = getPreferenceScreen().findPreference(key);
+            checkNotNull(preference, "Didn't find preference with key " + key);
+            return preference;
+        }
+
+        ShadowContentResolver getShadowContentResolver() {
+            return shadowOf(requireActivity().getContentResolver());
+        }
+
+        @Override
+        protected int getPreferenceScreenResId() {
+            return R.xml.modes_fake_settings;
+        }
+
+        @Override
+        public int getMetricsCategory() {
+            return 0;
+        }
+    }
+
+    private static class ShowsIdPreferenceController extends AbstractZenModePreferenceController {
+
+        ShowsIdPreferenceController(@NonNull Context context, @NonNull String key) {
+            super(context, key);
+        }
+
+        @Override
+        void updateState(Preference preference, @NonNull ZenMode zenMode) {
+            preference.setSummary("Id is " + zenMode.getId());
+        }
+    }
+
+    private static class ShowsNamePreferenceController extends AbstractZenModePreferenceController {
+
+        ShowsNamePreferenceController(@NonNull Context context, @NonNull String key) {
+            super(context, key);
+        }
+
+        @Override
+        void updateState(Preference preference, @NonNull ZenMode zenMode) {
+            preference.setSummary(zenMode.getName());
+        }
+    }
+
+    private static class AvailableIfEnabledPreferenceController extends
+            AbstractZenModePreferenceController {
+
+        AvailableIfEnabledPreferenceController(@NonNull Context context, @NonNull String key) {
+            super(context, key);
+        }
+
+        @Override
+        public boolean isAvailable(@NonNull ZenMode zenMode) {
+            return zenMode.isEnabled();
+        }
+
+        @Override
+        void updateState(Preference preference, @NonNull ZenMode zenMode) {
+            preference.setSummary("Enabled is " + zenMode.isEnabled());
+        }
+    }
+
+    private ImmutableList<ContentObserver> getSettingsContentObservers(Fragment fragment) {
+        return ImmutableList.copyOf(
+                shadowOf(fragment.requireActivity().getContentResolver())
+                        .getContentObservers(SETTINGS_URI));
+    }
+}
diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkPhoneNumberPreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkPhoneNumberPreferenceControllerTest.kt
index 38c47c2..f56c0c4 100644
--- a/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkPhoneNumberPreferenceControllerTest.kt
+++ b/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkPhoneNumberPreferenceControllerTest.kt
@@ -17,8 +17,7 @@
 package com.android.settings.network.telephony
 
 import android.content.Context
-import android.telephony.SubscriptionInfo
-import androidx.fragment.app.Fragment
+import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.preference.Preference
 import androidx.preference.PreferenceManager
 import androidx.test.core.app.ApplicationProvider
@@ -26,17 +25,19 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito
 import com.android.settings.R
 import com.android.settings.core.BasePreferenceController
-import com.android.settings.network.SubscriptionInfoListViewModel
 import com.android.settings.network.SubscriptionUtil
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.runBlocking
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.MockitoSession
-import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
 import org.mockito.kotlin.whenever
 import org.mockito.quality.Strictness
 
@@ -44,29 +45,25 @@
 class MobileNetworkPhoneNumberPreferenceControllerTest {
     private lateinit var mockSession: MockitoSession
 
-    private val mockViewModels =  mock<Lazy<SubscriptionInfoListViewModel>>()
-    private val mockFragment = mock<Fragment>{
-        val viewmodel = mockViewModels
-    }
-
-    private var mockPhoneNumber = String()
     private val context: Context = ApplicationProvider.getApplicationContext()
-    private val controller = MobileNetworkPhoneNumberPreferenceController(context, TEST_KEY)
+    private val mockSubscriptionRepository = mock<SubscriptionRepository>()
+
+    private val controller =
+        MobileNetworkPhoneNumberPreferenceController(context, TEST_KEY, mockSubscriptionRepository)
     private val preference = Preference(context).apply { key = TEST_KEY }
     private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context)
 
     @Before
     fun setUp() {
-        mockSession = ExtendedMockito.mockitoSession()
-            .initMocks(this)
-            .mockStatic(SubscriptionUtil::class.java)
-            .strictness(Strictness.LENIENT)
-            .startMocking()
+        mockSession =
+            ExtendedMockito.mockitoSession()
+                .mockStatic(SubscriptionUtil::class.java)
+                .strictness(Strictness.LENIENT)
+                .startMocking()
 
         preferenceScreen.addPreference(preference)
+        controller.init(SUB_ID)
         controller.displayPreference(preferenceScreen)
-
-        whenever(SubscriptionUtil.getBidiFormattedPhoneNumber(any(),any())).thenReturn(mockPhoneNumber)
     }
 
     @After
@@ -75,41 +72,29 @@
     }
 
     @Test
-    fun refreshData_getEmptyPhoneNumber_preferenceIsNotVisible() = runBlocking {
+    fun onViewCreated_cannotGetPhoneNumber_displayUnknown() = runBlocking {
         whenever(SubscriptionUtil.isSimHardwareVisible(context)).thenReturn(true)
-        whenever(SubscriptionUtil.getActiveSubscriptions(any())).thenReturn(
-            listOf(
-                SUB_INFO_1,
-                SUB_INFO_2
-            )
-        )
-        var mockSubId = 2
-        controller.init(mockFragment, mockSubId)
-        mockPhoneNumber = String()
+        mockSubscriptionRepository.stub {
+            on { phoneNumberFlow(SUB_ID) } doReturn flowOf(null)
+        }
 
-        controller.refreshData(SUB_INFO_2)
+        controller.onViewCreated(TestLifecycleOwner())
+        delay(100)
 
-        assertThat(preference.summary).isEqualTo(
-            context.getString(R.string.device_info_default))
+        assertThat(preference.summary).isEqualTo(context.getString(R.string.device_info_default))
     }
 
     @Test
-    fun refreshData_getPhoneNumber_preferenceSummaryIsExpected() = runBlocking {
+    fun onViewCreated_canGetPhoneNumber_displayPhoneNumber() = runBlocking {
         whenever(SubscriptionUtil.isSimHardwareVisible(context)).thenReturn(true)
-        whenever(SubscriptionUtil.getActiveSubscriptions(any())).thenReturn(
-            listOf(
-                SUB_INFO_1,
-                SUB_INFO_2
-            )
-        )
-        var mockSubId = 2
-        controller.init(mockFragment, mockSubId)
-        mockPhoneNumber = "test phone number"
-        whenever(SubscriptionUtil.getBidiFormattedPhoneNumber(any(),any())).thenReturn(mockPhoneNumber)
+        mockSubscriptionRepository.stub {
+            on { phoneNumberFlow(SUB_ID) } doReturn flowOf(PHONE_NUMBER)
+        }
 
-        controller.refreshData(SUB_INFO_2)
+        controller.onViewCreated(TestLifecycleOwner())
+        delay(100)
 
-        assertThat(preference.summary).isEqualTo(mockPhoneNumber)
+        assertThat(preference.summary).isEqualTo(PHONE_NUMBER)
     }
 
     @Test
@@ -123,18 +108,7 @@
 
     private companion object {
         const val TEST_KEY = "test_key"
-        const val DISPLAY_NAME_1 = "Sub 1"
-        const val DISPLAY_NAME_2 = "Sub 2"
-
-        val SUB_INFO_1: SubscriptionInfo = SubscriptionInfo.Builder().apply {
-            setId(1)
-            setDisplayName(DISPLAY_NAME_1)
-        }.build()
-
-        val SUB_INFO_2: SubscriptionInfo = SubscriptionInfo.Builder().apply {
-            setId(2)
-            setDisplayName(DISPLAY_NAME_2)
-        }.build()
-
+        const val SUB_ID = 10
+        const val PHONE_NUMBER = "1234567890"
     }
 }
diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt
index 75c9aa1..f75c14a 100644
--- a/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt
+++ b/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt
@@ -204,6 +204,22 @@
         assertThat(phoneNumber).isEqualTo(NUMBER_1)
     }
 
+    @Test
+    fun phoneNumberFlow_withSubId() = runBlocking {
+        val subInfo = SubscriptionInfo.Builder().apply {
+            setId(SUB_ID_IN_SLOT_1)
+            setMcc(MCC)
+        }.build()
+        mockSubscriptionManager.stub {
+            on { getActiveSubscriptionInfo(SUB_ID_IN_SLOT_1) } doReturn subInfo
+            on { getPhoneNumber(SUB_ID_IN_SLOT_1) } doReturn NUMBER_1
+        }
+
+        val phoneNumber = repository.phoneNumberFlow(SUB_ID_IN_SLOT_1).firstWithTimeoutOrNull()
+
+        assertThat(phoneNumber).isEqualTo(NUMBER_1)
+    }
+
     private companion object {
         const val SIM_SLOT_INDEX_0 = 0
         const val SUB_ID_IN_SLOT_0 = 2