Merge "Revert "Split settingslib_main_switch"" into main
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index b0332c3..ffb79b3 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -1642,6 +1642,7 @@
      *
      * <p>
      */
+    // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public Policy getNotificationPolicy() {
         INotificationManager service = getService();
         try {
@@ -1660,6 +1661,7 @@
      *
      * @param policy The new desired policy.
      */
+    // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public void setNotificationPolicy(@NonNull Policy policy) {
         checkRequired("policy", policy);
         INotificationManager service = getService();
@@ -2608,6 +2610,7 @@
      * Only available if policy access is granted to this package. See
      * {@link #isNotificationPolicyAccessGranted}.
      */
+    // TODO(b/309457271): Update documentation with VANILLA_ICE_CREAM behavior.
     public final void setInterruptionFilter(@InterruptionFilter int interruptionFilter) {
         final INotificationManager service = getService();
         try {
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index ff4dfc7..305b751 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -1004,6 +1004,8 @@
             priorityCategories |= Policy.PRIORITY_CATEGORY_CONVERSATIONS;
             conversationSenders = getConversationSendersWithDefault(
                     zenPolicy.getPriorityConversationSenders(), conversationSenders);
+        } else {
+            conversationSenders = CONVERSATION_SENDERS_NONE;
         }
 
         if (zenPolicy.isCategoryAllowed(ZenPolicy.PRIORITY_CATEGORY_CALLS,
@@ -1102,7 +1104,7 @@
         return (policy.suppressedVisualEffects & visualEffect) == 0;
     }
 
-    private int getNotificationPolicySenders(@ZenPolicy.PeopleType int senders,
+    private static int getNotificationPolicySenders(@ZenPolicy.PeopleType int senders,
             int defaultPolicySender) {
         switch (senders) {
             case ZenPolicy.PEOPLE_TYPE_ANYONE:
@@ -1116,7 +1118,7 @@
         }
     }
 
-    private int getConversationSendersWithDefault(@ZenPolicy.ConversationSenders int senders,
+    private static int getConversationSendersWithDefault(@ZenPolicy.ConversationSenders int senders,
             int defaultPolicySender) {
         switch (senders) {
             case ZenPolicy.CONVERSATION_SENDERS_ANYONE:
diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
new file mode 100644
index 0000000..b63a969
--- /dev/null
+++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.window.flags"
+
+flag {
+  name: "allows_screen_size_decoupled_from_status_bar_and_cutout"
+  namespace: "large_screen_experiences_app_compat"
+  description: "When necessary, configuration decoupled from status bar and display cutout"
+  bug: "291870756"
+  is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 15c188d..cec83de 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5245,6 +5245,11 @@
     <!-- Zen mode - name of default automatic calendar time-based rule that is triggered every night (when sleeping). [CHAR LIMIT=40] -->
     <string name="zen_mode_default_every_night_name">Sleeping</string>
 
+    <!-- Zen mode - Condition summary when a rule is activated due to a call to setInterruptionFilter(). [CHAR_LIMIT=NONE] -->
+    <string name="zen_mode_implicit_activated">On</string>
+    <!-- Zen mode - Condition summary when a rule is deactivated due to a call to setInterruptionFilter(). [CHAR_LIMIT=NONE] -->
+    <string name="zen_mode_implicit_deactivated">Off</string>
+
     <!-- Indication that the current volume and other effects (vibration) are being suppressed by a third party, such as a notification listener. [CHAR LIMIT=30] -->
     <string name="muted_by"><xliff:g id="third_party">%1$s</xliff:g> is muting some sounds</string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index e19d548..16ad5c9 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2574,6 +2574,8 @@
   <java-symbol type="string" name="zen_mode_default_weekends_name" />
   <java-symbol type="string" name="zen_mode_default_events_name" />
   <java-symbol type="string" name="zen_mode_default_every_night_name" />
+  <java-symbol type="string" name="zen_mode_implicit_activated" />
+  <java-symbol type="string" name="zen_mode_implicit_deactivated" />
   <java-symbol type="string" name="display_rotation_camera_compat_toast_after_rotation" />
   <java-symbol type="string" name="display_rotation_camera_compat_toast_in_multi_window" />
   <java-symbol type="array" name="config_system_condition_providers" />
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index d5b29e3..d5fab44 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -822,14 +822,23 @@
                     + "participant");
         }
 
-        // Make sure other open changes are visible as entering PIP. Some may be hidden in
-        // Transitions#setupStartState because the transition type is OPEN (such as auto-enter).
+        // Make sure other non-pip changes are handled correctly.
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
             if (change == enterPip) continue;
             if (TransitionUtil.isOpeningType(change.getMode())) {
+                // For other open changes that are visible when entering PIP, some may be hidden in
+                // Transitions#setupStartState because the transition type is OPEN (such as
+                // auto-enter).
                 final SurfaceControl leash = change.getLeash();
                 startTransaction.show(leash).setAlpha(leash, 1.f);
+            } else if (TransitionUtil.isClosingType(change.getMode())) {
+                // For other close changes that are invisible as entering PIP, hide them immediately
+                // to avoid showing a freezing surface.
+                // Ideally, we should let other handler to handle them (likely RemoteHandler by
+                // Launcher).
+                final SurfaceControl leash = change.getLeash();
+                startTransaction.hide(leash);
             }
         }
 
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index e706eb0..d55d28d 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -527,52 +527,65 @@
     return Frame(surface->logicalWidth(), surface->logicalHeight(), bufferAge);
 }
 
-struct DestroySemaphoreInfo {
+class SharedSemaphoreInfo : public LightRefBase<SharedSemaphoreInfo> {
     PFN_vkDestroySemaphore mDestroyFunction;
     VkDevice mDevice;
     VkSemaphore mSemaphore;
+    GrBackendSemaphore mGrBackendSemaphore;
 
-    DestroySemaphoreInfo(PFN_vkDestroySemaphore destroyFunction, VkDevice device,
-                         VkSemaphore semaphore)
-            : mDestroyFunction(destroyFunction), mDevice(device), mSemaphore(semaphore) {}
+    SharedSemaphoreInfo(PFN_vkDestroySemaphore destroyFunction, VkDevice device,
+                        VkSemaphore semaphore)
+            : mDestroyFunction(destroyFunction), mDevice(device), mSemaphore(semaphore) {
+        mGrBackendSemaphore.initVulkan(semaphore);
+    }
 
-    ~DestroySemaphoreInfo() { mDestroyFunction(mDevice, mSemaphore, nullptr); }
+    ~SharedSemaphoreInfo() { mDestroyFunction(mDevice, mSemaphore, nullptr); }
+
+    friend class LightRefBase<SharedSemaphoreInfo>;
+    friend class sp<SharedSemaphoreInfo>;
+
+public:
+    VkSemaphore semaphore() const { return mSemaphore; }
+
+    GrBackendSemaphore* grBackendSemaphore() { return &mGrBackendSemaphore; }
 };
 
 static void destroy_semaphore(void* context) {
-    DestroySemaphoreInfo* info = reinterpret_cast<DestroySemaphoreInfo*>(context);
-    delete info;
+    SharedSemaphoreInfo* info = reinterpret_cast<SharedSemaphoreInfo*>(context);
+    info->decStrong(0);
 }
 
 VulkanManager::VkDrawResult VulkanManager::finishFrame(SkSurface* surface) {
     ATRACE_NAME("Vulkan finish frame");
 
-    VkExportSemaphoreCreateInfo exportInfo;
-    exportInfo.sType = VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO;
-    exportInfo.pNext = nullptr;
-    exportInfo.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT;
-
-    VkSemaphoreCreateInfo semaphoreInfo;
-    semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
-    semaphoreInfo.pNext = &exportInfo;
-    semaphoreInfo.flags = 0;
-    VkSemaphore semaphore;
-    VkResult err = mCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &semaphore);
-    ALOGE_IF(VK_SUCCESS != err, "VulkanManager::makeSwapSemaphore(): Failed to create semaphore");
-
-    GrBackendSemaphore backendSemaphore;
-    backendSemaphore.initVulkan(semaphore);
-
+    sp<SharedSemaphoreInfo> sharedSemaphore;
     GrFlushInfo flushInfo;
-    if (err == VK_SUCCESS) {
-        flushInfo.fNumSemaphores = 1;
-        flushInfo.fSignalSemaphores = &backendSemaphore;
-        flushInfo.fFinishedProc = destroy_semaphore;
-        flushInfo.fFinishedContext =
-                new DestroySemaphoreInfo(mDestroySemaphore, mDevice, semaphore);
-    } else {
-        semaphore = VK_NULL_HANDLE;
+
+    {
+        VkExportSemaphoreCreateInfo exportInfo;
+        exportInfo.sType = VK_STRUCTURE_TYPE_EXPORT_SEMAPHORE_CREATE_INFO;
+        exportInfo.pNext = nullptr;
+        exportInfo.handleTypes = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT;
+
+        VkSemaphoreCreateInfo semaphoreInfo;
+        semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
+        semaphoreInfo.pNext = &exportInfo;
+        semaphoreInfo.flags = 0;
+        VkSemaphore semaphore;
+        VkResult err = mCreateSemaphore(mDevice, &semaphoreInfo, nullptr, &semaphore);
+        ALOGE_IF(VK_SUCCESS != err,
+                 "VulkanManager::makeSwapSemaphore(): Failed to create semaphore");
+
+        if (err == VK_SUCCESS) {
+            sharedSemaphore = sp<SharedSemaphoreInfo>::make(mDestroySemaphore, mDevice, semaphore);
+            flushInfo.fNumSemaphores = 1;
+            flushInfo.fSignalSemaphores = sharedSemaphore->grBackendSemaphore();
+            flushInfo.fFinishedProc = destroy_semaphore;
+            sharedSemaphore->incStrong(0);
+            flushInfo.fFinishedContext = sharedSemaphore.get();
+        }
     }
+
     GrDirectContext* context = GrAsDirectContext(surface->recordingContext());
     ALOGE_IF(!context, "Surface is not backed by gpu");
     GrSemaphoresSubmitted submitted = context->flush(
@@ -581,37 +594,34 @@
     VkDrawResult drawResult{
             .submissionTime = systemTime(),
     };
-    if (semaphore != VK_NULL_HANDLE) {
-        if (submitted == GrSemaphoresSubmitted::kYes) {
-            if (mFrameBoundaryANDROID) {
-                // retrieve VkImage used as render target
-                VkImage image = VK_NULL_HANDLE;
-                GrBackendRenderTarget backendRenderTarget =
-                        SkSurfaces::GetBackendRenderTarget(
-                            surface, SkSurfaces::BackendHandleAccess::kFlushRead);
-                if (backendRenderTarget.isValid()) {
-                    GrVkImageInfo info;
-                    if (GrBackendRenderTargets::GetVkImageInfo(backendRenderTarget, &info)) {
-                        image = info.fImage;
-                    } else {
-                        ALOGE("Frame boundary: backend is not vulkan");
-                    }
+    if (sharedSemaphore) {
+        if (submitted == GrSemaphoresSubmitted::kYes && mFrameBoundaryANDROID) {
+            // retrieve VkImage used as render target
+            VkImage image = VK_NULL_HANDLE;
+            GrBackendRenderTarget backendRenderTarget = SkSurfaces::GetBackendRenderTarget(
+                    surface, SkSurfaces::BackendHandleAccess::kFlushRead);
+            if (backendRenderTarget.isValid()) {
+                GrVkImageInfo info;
+                if (GrBackendRenderTargets::GetVkImageInfo(backendRenderTarget, &info)) {
+                    image = info.fImage;
                 } else {
-                    ALOGE("Frame boundary: invalid backend render target");
+                    ALOGE("Frame boundary: backend is not vulkan");
                 }
-                // frameBoundaryANDROID needs to know about mSwapSemaphore, but
-                // it won't wait on it.
-                mFrameBoundaryANDROID(mDevice, semaphore, image);
+            } else {
+                ALOGE("Frame boundary: invalid backend render target");
             }
+            // frameBoundaryANDROID needs to know about mSwapSemaphore, but
+            // it won't wait on it.
+            mFrameBoundaryANDROID(mDevice, sharedSemaphore->semaphore(), image);
         }
         VkSemaphoreGetFdInfoKHR getFdInfo;
         getFdInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR;
         getFdInfo.pNext = nullptr;
-        getFdInfo.semaphore = semaphore;
+        getFdInfo.semaphore = sharedSemaphore->semaphore();
         getFdInfo.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_SYNC_FD_BIT;
 
         int fenceFd = -1;
-        err = mGetSemaphoreFdKHR(mDevice, &getFdInfo, &fenceFd);
+        VkResult err = mGetSemaphoreFdKHR(mDevice, &getFdInfo, &fenceFd);
         ALOGE_IF(VK_SUCCESS != err, "VulkanManager::swapBuffers(): Failed to get semaphore Fd");
         drawResult.presentFence.reset(fenceFd);
     } else {
@@ -732,15 +742,15 @@
         return INVALID_OPERATION;
     }
 
-    GrBackendSemaphore backendSemaphore;
-    backendSemaphore.initVulkan(semaphore);
+    auto sharedSemaphore = sp<SharedSemaphoreInfo>::make(mDestroySemaphore, mDevice, semaphore);
 
     // Even if Skia fails to submit the semaphore, it will still call the destroy_semaphore callback
     GrFlushInfo flushInfo;
     flushInfo.fNumSemaphores = 1;
-    flushInfo.fSignalSemaphores = &backendSemaphore;
+    flushInfo.fSignalSemaphores = sharedSemaphore->grBackendSemaphore();
     flushInfo.fFinishedProc = destroy_semaphore;
-    flushInfo.fFinishedContext = new DestroySemaphoreInfo(mDestroySemaphore, mDevice, semaphore);
+    sharedSemaphore->incStrong(0);
+    flushInfo.fFinishedContext = sharedSemaphore.get();
     GrSemaphoresSubmitted submitted = grContext->flush(flushInfo);
     grContext->submit();
 
diff --git a/media/java/android/media/AudioDeviceAttributes.java b/media/java/android/media/AudioDeviceAttributes.java
index 5a27435..2b349d4 100644
--- a/media/java/android/media/AudioDeviceAttributes.java
+++ b/media/java/android/media/AudioDeviceAttributes.java
@@ -325,7 +325,7 @@
                 + " role:" + roleToString(mRole)
                 + " type:" + (mRole == ROLE_OUTPUT ? AudioSystem.getOutputDeviceName(mNativeType)
                         : AudioSystem.getInputDeviceName(mNativeType))
-                + " addr:" + mAddress
+                + " addr:" + Utils.anonymizeBluetoothAddress(mNativeType, mAddress)
                 + " name:" + mName
                 + " profiles:" + mAudioProfiles.toString()
                 + " descriptors:" + mAudioDescriptors.toString());
diff --git a/media/java/android/media/AudioDevicePort.java b/media/java/android/media/AudioDevicePort.java
index 9211c53..73bc6f9 100644
--- a/media/java/android/media/AudioDevicePort.java
+++ b/media/java/android/media/AudioDevicePort.java
@@ -156,7 +156,7 @@
                             AudioSystem.getOutputDeviceName(mType));
         return "{" + super.toString()
                 + ", mType: " + type
-                + ", mAddress: " + mAddress
+                + ", mAddress: " + Utils.anonymizeBluetoothAddress(mType, mAddress)
                 + "}";
     }
 }
diff --git a/media/java/android/media/Utils.java b/media/java/android/media/Utils.java
index ecb6b3d..d07f611 100644
--- a/media/java/android/media/Utils.java
+++ b/media/java/android/media/Utils.java
@@ -657,4 +657,35 @@
         // on the fly.
         private final boolean mForceRemoveConsistency; // default false
     }
+
+    /**
+     * Convert a Bluetooth MAC address to an anonymized one when exposed to a non privileged app
+     * Must match the implementation of BluetoothUtils.toAnonymizedAddress()
+     * @param address MAC address to be anonymized
+     * @return anonymized MAC address
+     */
+    public static @Nullable String anonymizeBluetoothAddress(@Nullable String address) {
+        if (address == null) {
+            return null;
+        }
+        if (address.length() != "AA:BB:CC:DD:EE:FF".length()) {
+            return address;
+        }
+        return "XX:XX:XX:XX" + address.substring("XX:XX:XX:XX".length());
+    }
+
+    /**
+     * Convert a Bluetooth MAC address to an anonymized one if the internal device type corresponds
+     * to a Bluetooth.
+     * @param deviceType the internal type of the audio device
+     * @param address MAC address to be anonymized
+     * @return anonymized MAC address
+     */
+    public static @Nullable String anonymizeBluetoothAddress(
+            int deviceType, @Nullable String address) {
+        if (!AudioSystem.isBluetoothDevice(deviceType)) {
+            return address;
+        }
+        return anonymizeBluetoothAddress(address);
+    }
 }
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
index 1c92696..223e99e 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -57,19 +57,24 @@
 
 private fun UserManager.getUserGroups(): List<UserGroup> {
     val userGroupList = mutableListOf<UserGroup>()
-    val profileToShowInSettings = getProfiles(UserHandle.myUserId())
-        .map { userInfo -> userInfo to getUserProperties(userInfo.userHandle) }
+    val showInSettingsMap = getProfiles(UserHandle.myUserId()).groupBy { showInSettings(it) }
 
-    profileToShowInSettings
-        .filter { it.second.showInSettings == UserProperties.SHOW_IN_SETTINGS_WITH_PARENT }
-        .takeIf { it.isNotEmpty() }
-        ?.map { it.first }
-        ?.let { userInfos -> userGroupList += UserGroup(userInfos) }
+    showInSettingsMap[UserProperties.SHOW_IN_SETTINGS_WITH_PARENT]?.let {
+        userGroupList += UserGroup(it)
+    }
 
-    profileToShowInSettings
-        .filter { it.second.showInSettings == UserProperties.SHOW_IN_SETTINGS_SEPARATE &&
-                    (!it.second.hideInSettingsInQuietMode || !it.first.isQuietModeEnabled) }
-        .forEach { userGroupList += UserGroup(userInfos = listOf(it.first)) }
+    showInSettingsMap[UserProperties.SHOW_IN_SETTINGS_SEPARATE]?.forEach {
+        userGroupList += UserGroup(listOf(it))
+    }
 
     return userGroupList
 }
+
+private fun UserManager.showInSettings(userInfo: UserInfo): Int {
+    val userProperties = getUserProperties(userInfo.userHandle)
+    return if (userInfo.isQuietModeEnabled && userProperties.hideInSettingsInQuietMode) {
+        UserProperties.SHOW_IN_SETTINGS_NO
+    } else {
+        userProperties.showInSettings
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/common/UserProfilePagerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/common/UserProfilePagerTest.kt
new file mode 100644
index 0000000..e450364
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/common/UserProfilePagerTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.template.common
+
+import android.content.Context
+import android.content.pm.UserInfo
+import android.content.pm.UserProperties
+import android.os.UserManager
+import androidx.compose.material3.Text
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spaprivileged.framework.common.userManager
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+
+@RunWith(AndroidJUnit4::class)
+class UserProfilePagerTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private val mockUserManager = mock<UserManager> {
+        on { getProfiles(any()) } doReturn listOf(USER_0)
+        on { getUserProperties(USER_0.userHandle) } doReturn
+            UserProperties.Builder()
+                .setShowInSettings(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT)
+                .build()
+    }
+
+    private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+        on { userManager } doReturn mockUserManager
+    }
+
+    @Test
+    fun userProfilePager() {
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalContext provides context) {
+                UserProfilePager { userGroup ->
+                    Text(text = userGroup.userInfos.joinToString { it.id.toString() })
+                }
+            }
+        }
+
+        composeTestRule.onNodeWithText(USER_0.id.toString()).assertIsDisplayed()
+    }
+
+    private companion object {
+        val USER_0 = UserInfo(0, "", 0)
+    }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index aae61bd..b77a60b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -506,7 +506,10 @@
 
     // There is no ongoing transition.
     if (state !is TransitionState.Transition || state.fromScene == state.toScene) {
-        return idleValue
+        // Even if this element SceneTransitionLayout is not animated, the layout itself might be
+        // animated (e.g. by another parent SceneTransitionLayout), in which case this element still
+        // need to participate in the layout phase.
+        return currentValue()
     }
 
     // A transition was started but it's not ready yet (not all elements have been composed/laid
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
index 216608a..5d8eaf7 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
@@ -2,9 +2,10 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.unit.IntSize
 
 interface DraggableHandler {
-    fun onDragStarted(startedPosition: Offset, pointersDown: Int = 1)
+    fun onDragStarted(layoutSize: IntSize, startedPosition: Offset, pointersDown: Int = 1)
     fun onDelta(pixels: Float)
     fun onDragStopped(velocity: Float)
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index d0a5f5b..d48781a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -38,6 +38,7 @@
 import androidx.compose.ui.input.pointer.util.VelocityTracker
 import androidx.compose.ui.input.pointer.util.addPointerInputChange
 import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.util.fastForEach
 
@@ -60,7 +61,7 @@
     orientation: Orientation,
     enabled: Boolean,
     startDragImmediately: Boolean,
-    onDragStarted: (startedPosition: Offset, pointersDown: Int) -> Unit,
+    onDragStarted: (layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) -> Unit,
     onDragDelta: (Float) -> Unit,
     onDragStopped: (velocity: Float) -> Unit,
 ): Modifier = composed {
@@ -83,7 +84,7 @@
 
         val onDragStart: (Offset, Int) -> Unit = { startedPosition, pointersDown ->
             velocityTracker.resetTracking()
-            onDragStarted(startedPosition, pointersDown)
+            onDragStarted(size, startedPosition, pointersDown)
         }
 
         val onDragCancel: () -> Unit = { onDragStopped(/* velocity= */ 0f) }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index eb5168b..1a79522 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -25,8 +25,9 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.layout.intermediateLayout
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.zIndex
@@ -44,14 +45,24 @@
     var content by mutableStateOf(content)
     var userActions by mutableStateOf(actions)
     var zIndex by mutableFloatStateOf(zIndex)
-    var size by mutableStateOf(IntSize.Zero)
+    var targetSize by mutableStateOf(IntSize.Zero)
 
     /** The shared values in this scene that are not tied to a specific element. */
     val sharedValues = SnapshotStateMap<ValueKey, Element.SharedValue<*>>()
 
     @Composable
+    @OptIn(ExperimentalComposeUiApi::class)
     fun Content(modifier: Modifier = Modifier) {
-        Box(modifier.zIndex(zIndex).onPlaced { size = it.size }.testTag(key.testTag)) {
+        Box(
+            modifier
+                .zIndex(zIndex)
+                .intermediateLayout { measurable, constraints ->
+                    targetSize = lookaheadSize
+                    val placeable = measurable.measure(constraints)
+                    layout(placeable.width, placeable.height) { placeable.place(0, 0) }
+                }
+                .testTag(key.testTag)
+        ) {
             scope.content()
         }
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index 9a3a0ae..9d71801 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -27,6 +27,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.round
@@ -90,7 +91,7 @@
 
     internal var gestureWithPriority: Any? = null
 
-    internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?) {
+    internal fun onDragStarted(pointersDown: Int, layoutSize: IntSize, startedPosition: Offset?) {
         if (isDrivingTransition) {
             // This [transition] was already driving the animation: simply take over it.
             // Stop animating and start from where the current offset.
@@ -126,14 +127,14 @@
         // we will also have to make sure that we correctly handle overscroll.
         swipeTransition.absoluteDistance =
             when (orientation) {
-                Orientation.Horizontal -> layoutImpl.size.width
-                Orientation.Vertical -> layoutImpl.size.height
+                Orientation.Horizontal -> layoutSize.width
+                Orientation.Vertical -> layoutSize.height
             }.toFloat()
 
         val fromEdge =
             startedPosition?.let { position ->
                 layoutImpl.edgeDetector.edge(
-                    layoutImpl.size,
+                    layoutSize,
                     position.round(),
                     layoutImpl.density,
                     orientation,
@@ -513,9 +514,9 @@
 private class SceneDraggableHandler(
     private val gestureHandler: SceneGestureHandler,
 ) : DraggableHandler {
-    override fun onDragStarted(startedPosition: Offset, pointersDown: Int) {
+    override fun onDragStarted(layoutSize: IntSize, startedPosition: Offset, pointersDown: Int) {
         gestureHandler.gestureWithPriority = this
-        gestureHandler.onDragStarted(pointersDown, startedPosition)
+        gestureHandler.onDragStarted(pointersDown, layoutSize, startedPosition)
     }
 
     override fun onDelta(pixels: Float) {
@@ -647,7 +648,11 @@
             canContinueScroll = { true },
             onStart = {
                 gestureHandler.gestureWithPriority = this
-                gestureHandler.onDragStarted(pointersDown = 1, startedPosition = null)
+                gestureHandler.onDragStarted(
+                    pointersDown = 1,
+                    layoutSize = gestureHandler.currentScene.targetSize,
+                    startedPosition = null,
+                )
             },
             onScroll = { offsetAvailable ->
                 if (gestureHandler.gestureWithPriority != this) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 6edd1b6..0b06953 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -30,13 +30,15 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawWithContent
 import androidx.compose.ui.layout.LookaheadScope
-import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.intermediateLayout
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastForEach
+import com.android.compose.ui.util.lerp
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.Channel
 
@@ -64,12 +66,6 @@
     private val horizontalGestureHandler: SceneGestureHandler
     private val verticalGestureHandler: SceneGestureHandler
 
-    /**
-     * The size of this layout. Note that this could be [IntSize.Zero] if this layour does not have
-     * any scene configured or right before the first measure pass of the layout.
-     */
-    @VisibleForTesting var size by mutableStateOf(IntSize.Zero)
-
     init {
         setScenes(builder)
 
@@ -157,15 +153,46 @@
     }
 
     @Composable
+    @OptIn(ExperimentalComposeUiApi::class)
     internal fun Content(modifier: Modifier) {
         Box(
             modifier
                 // Handle horizontal and vertical swipes on this layout.
                 // Note: order here is important and will give a slight priority to the vertical
                 // swipes.
-                .swipeToScene(gestureHandler(Orientation.Horizontal))
-                .swipeToScene(gestureHandler(Orientation.Vertical))
-                .onSizeChanged { size = it }
+                .swipeToScene(horizontalGestureHandler)
+                .swipeToScene(verticalGestureHandler)
+                // Animate the size of this layout.
+                .intermediateLayout { measurable, constraints ->
+                    // Measure content normally.
+                    val placeable = measurable.measure(constraints)
+
+                    val width: Int
+                    val height: Int
+                    val state = state.transitionState
+                    if (state !is TransitionState.Transition || state.fromScene == state.toScene) {
+                        width = placeable.width
+                        height = placeable.height
+                    } else {
+                        // Interpolate the size.
+                        val fromSize = scene(state.fromScene).targetSize
+                        val toSize = scene(state.toScene).targetSize
+
+                        // Optimization: make sure we don't read state.progress if fromSize ==
+                        // toSize to avoid running this code every frame when the layout size does
+                        // not change.
+                        if (fromSize == toSize) {
+                            width = fromSize.width
+                            height = fromSize.height
+                        } else {
+                            val size = lerp(fromSize, toSize, state.progress)
+                            width = size.width.coerceAtLeast(0)
+                            height = size.height.coerceAtLeast(0)
+                        }
+                    }
+
+                    layout(width, height) { placeable.place(0, 0) }
+                }
         ) {
             LookaheadScope {
                 val scenesToCompose =
@@ -230,4 +257,9 @@
     }
 
     internal fun isSceneReady(scene: SceneKey): Boolean = readyScenes.containsKey(scene)
+
+    @VisibleForTesting
+    fun setScenesTargetSizeForTest(size: IntSize) {
+        scenes.values.forEach { it.targetSize = size }
+    }
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
index 840800d..70534dd 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
@@ -38,7 +38,7 @@
         transition: TransitionState.Transition,
         value: Offset
     ): Offset {
-        val sceneSize = scene.size
+        val sceneSize = scene.targetSize
         val elementSize = sceneValues.targetSize
         if (elementSize == Element.SizeUnspecified) {
             return value
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index 1e3d011..7ab2096 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -46,6 +46,7 @@
 import org.junit.runner.RunWith
 
 private const val SCREEN_SIZE = 100f
+private val LAYOUT_SIZE = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt())
 
 @RunWith(AndroidJUnit4::class)
 class SceneGestureHandlerTest {
@@ -80,7 +81,7 @@
                             edgeDetector = DefaultEdgeDetector,
                             coroutineScope = coroutineScope,
                         )
-                        .also { it.size = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) },
+                        .apply { setScenesTargetSizeForTest(LAYOUT_SIZE) },
                 orientation = Orientation.Vertical,
                 coroutineScope = coroutineScope,
             )
@@ -128,18 +129,21 @@
         runMonotonicClockTest { TestGestureScope(coroutineScope = this).block() }
     }
 
+    private fun DraggableHandler.onDragStarted() =
+        onDragStarted(layoutSize = LAYOUT_SIZE, startedPosition = Offset.Zero)
+
     @Test
     fun testPreconditions() = runGestureTest { assertScene(currentScene = SceneA, isIdle = true) }
 
     @Test
     fun onDragStarted_shouldStartATransition() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
     }
 
     @Test
     fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
         val transition = transitionState as Transition
 
@@ -152,7 +156,7 @@
 
     @Test
     fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDelta(pixels = deltaInPixels10)
@@ -170,7 +174,7 @@
 
     @Test
     fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDelta(pixels = deltaInPixels10)
@@ -188,7 +192,7 @@
 
     @Test
     fun onDragStoppedAfterStarted_returnImmediatelyToIdle() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDragStopped(velocity = 0f)
@@ -197,7 +201,7 @@
 
     @Test
     fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest {
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
 
         draggable.onDelta(pixels = deltaInPixels10)
@@ -217,7 +221,7 @@
         assertScene(currentScene = SceneC, isIdle = false)
 
         // Start a new gesture while the offset is animating
-        draggable.onDragStarted(startedPosition = Offset.Zero)
+        draggable.onDragStarted()
         assertThat(sceneGestureHandler.isAnimatingOffset).isFalse()
     }
 
@@ -421,6 +425,7 @@
         draggable.onDelta(deltaInPixels10)
         assertScene(currentScene = SceneA, isIdle = true)
     }
+
     @Test
     fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest {
         draggable.onDragStopped(velocityThreshold)
@@ -437,7 +442,7 @@
     @Test
     fun startNestedScrollWhileDragging() = runGestureTest {
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = Always)
-        draggable.onDragStarted(Offset.Zero)
+        draggable.onDragStarted()
         assertScene(currentScene = SceneA, isIdle = false)
         val transition = transitionState as Transition
 
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index 5afd420..321cf63 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -18,6 +18,8 @@
 
 import androidx.activity.ComponentActivity
 import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
@@ -48,6 +50,7 @@
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.test.assertSizeIsEqualTo
 import com.android.compose.test.subjects.DpOffsetSubject
 import com.android.compose.test.subjects.assertThat
 import com.google.common.truth.Truth.assertThat
@@ -307,6 +310,26 @@
         assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
     }
 
+    @Test
+    fun layoutSizeIsAnimated() {
+        val layoutTag = "layout"
+        rule.testTransition(
+            fromSceneContent = { Box(Modifier.size(200.dp, 100.dp)) },
+            toSceneContent = { Box(Modifier.size(120.dp, 140.dp)) },
+            transition = {
+                // 4 frames of animation.
+                spec = tween(4 * 16, easing = LinearEasing)
+            },
+            layoutModifier = Modifier.testTag(layoutTag),
+        ) {
+            before { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(200.dp, 100.dp) }
+            at(16) { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(180.dp, 110.dp) }
+            at(32) { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(160.dp, 120.dp) }
+            at(48) { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(140.dp, 130.dp) }
+            after { rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(120.dp, 140.dp) }
+        }
+    }
+
     private fun SemanticsNodeInteraction.offsetRelativeTo(
         other: SemanticsNodeInteraction,
     ): DpOffset {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt
index 2a27763..8cffcf6 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt
@@ -48,7 +48,7 @@
         rule.testTransition(
             // The layout under test is 300dp x 300dp.
             layoutModifier = Modifier.size(300.dp),
-            fromSceneContent = {},
+            fromSceneContent = { Box(Modifier.fillMaxSize()) },
             toSceneContent = {
                 // Foo is 100dp x 100dp in the center of the layout, so at offset = (100dp, 100dp)
                 Box(Modifier.fillMaxSize()) {
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index e0ae1be..06de296 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -16,7 +16,6 @@
 
 package com.android.compose.animation.scene
 
-import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -104,7 +103,7 @@
                 currentScene,
                 onChangeScene,
                 transitions { from(fromScene, to = toScene, transition) },
-                layoutModifier.fillMaxSize(),
+                layoutModifier,
             ) {
                 scene(fromScene, content = fromSceneContent)
                 scene(toScene, content = toSceneContent)
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index d64a131..ced96f1 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -441,9 +441,6 @@
     // TODO(b/270437894): Tracking Bug
     val MEDIA_REMOTE_RESUME = unreleasedFlag("media_remote_resume")
 
-    // TODO(b/304506662): Tracking Bug
-    val MEDIA_DEVICE_NAME_FIX = releasedFlag("media_device_name_fix")
-
     // 1000 - dock
     val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag("simulate_dock_through_charging")
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
index 078feff..c2aedca 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationPanelView
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import javax.inject.Inject
@@ -44,6 +45,7 @@
     sharedNotificationContainer: SharedNotificationContainer,
     sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     controller: NotificationStackScrollLayoutController,
+    notificationStackSizeCalculator: NotificationStackSizeCalculator,
     private val smartspaceViewModel: KeyguardSmartspaceViewModel,
 ) :
     NotificationStackScrollLayoutSection(
@@ -53,6 +55,7 @@
         sharedNotificationContainer,
         sharedNotificationContainerViewModel,
         controller,
+        notificationStackSizeCalculator,
     ) {
     override fun applyConstraints(constraintSet: ConstraintSet) {
         if (!featureFlags.isEnabled(Flags.MIGRATE_NSSL)) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
index 00966f2..ea2bdf7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationPanelView
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
@@ -40,6 +41,7 @@
     private val sharedNotificationContainer: SharedNotificationContainer,
     private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     private val controller: NotificationStackScrollLayoutController,
+    private val notificationStackSizeCalculator: NotificationStackSizeCalculator,
 ) : KeyguardSection() {
     private val placeHolderId = R.id.nssl_placeholder
     private var disposableHandle: DisposableHandle? = null
@@ -69,6 +71,7 @@
                 sharedNotificationContainer,
                 sharedNotificationContainerViewModel,
                 controller,
+                notificationStackSizeCalculator,
             )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
index bf95c77..dc2ad8d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.NotificationPanelView
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import javax.inject.Inject
@@ -44,6 +45,7 @@
     sharedNotificationContainer: SharedNotificationContainer,
     sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     controller: NotificationStackScrollLayoutController,
+    notificationStackSizeCalculator: NotificationStackSizeCalculator,
     private val smartspaceViewModel: KeyguardSmartspaceViewModel,
 ) :
     NotificationStackScrollLayoutSection(
@@ -53,6 +55,7 @@
         sharedNotificationContainer,
         sharedNotificationContainerViewModel,
         controller,
+        notificationStackSizeCalculator,
     ) {
     override fun applyConstraints(constraintSet: ConstraintSet) {
         if (!featureFlags.isEnabled(Flags.MIGRATE_NSSL)) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
index 2034d97..dcbf670 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDeviceManager.kt
@@ -37,8 +37,6 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlagsClassic
-import com.android.systemui.flags.Flags
 import com.android.systemui.media.controls.models.player.MediaData
 import com.android.systemui.media.controls.models.player.MediaDeviceData
 import com.android.systemui.media.controls.util.MediaControllerFactory
@@ -70,7 +68,6 @@
     @Main private val fgExecutor: Executor,
     @Background private val bgExecutor: Executor,
     dumpManager: DumpManager,
-    private val featureFlags: FeatureFlagsClassic,
 ) : MediaDataManager.Listener, Dumpable {
 
     private val listeners: MutableSet<Listener> = mutableSetOf()
@@ -392,13 +389,6 @@
                 )
             }
 
-            if (!featureFlags.isEnabled(Flags.MEDIA_DEVICE_NAME_FIX)) {
-                if (controller == null || routingSession != null) {
-                    return routingSession?.name?.toString() ?: device?.name
-                }
-                return null
-            }
-
             if (controller == null) {
                 // In resume state, we don't have a controller - just use the device name
                 return device?.name
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
index 1ec43c5..d277f32 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt
@@ -60,7 +60,7 @@
     }
 
     companion object {
-        @JvmField val GUTS_ANIMATION_DURATION = 500L
+        @JvmField val GUTS_ANIMATION_DURATION = 234L
     }
 
     /** A listener when the current dimensions of the player change */
@@ -234,7 +234,8 @@
             currentStartLocation,
             currentEndLocation,
             currentTransitionProgress,
-            applyImmediately = false
+            applyImmediately = false,
+            isGutsAnimation = true,
         )
     }
 
@@ -254,7 +255,8 @@
             currentStartLocation,
             currentEndLocation,
             currentTransitionProgress,
-            applyImmediately = immediate
+            applyImmediately = immediate,
+            isGutsAnimation = true,
         )
     }
 
@@ -414,7 +416,10 @@
      * it's not available, it will recreate one by measuring, which may be expensive.
      */
     @VisibleForTesting
-    fun obtainViewState(state: MediaHostState?): TransitionViewState? {
+    fun obtainViewState(
+        state: MediaHostState?,
+        isGutsAnimation: Boolean = false
+    ): TransitionViewState? {
         if (state == null || state.measurementInput == null) {
             return null
         }
@@ -423,7 +428,7 @@
         val viewState = viewStates[cacheKey]
         if (viewState != null) {
             // we already have cached this measurement, let's continue
-            if (state.squishFraction <= 1f) {
+            if (state.squishFraction <= 1f && !isGutsAnimation) {
                 return squishViewState(viewState, state.squishFraction)
             }
             return viewState
@@ -455,13 +460,14 @@
 
             // Given that we have a measurement and a view, let's get (guaranteed) viewstates
             // from the start and end state and interpolate them
-            val startViewState = obtainViewState(startState) as TransitionViewState
+            val startViewState = obtainViewState(startState, isGutsAnimation) as TransitionViewState
             val endState = state.copy().also { it.expansion = 1.0f }
-            val endViewState = obtainViewState(endState) as TransitionViewState
+            val endViewState = obtainViewState(endState, isGutsAnimation) as TransitionViewState
             result =
                 layoutController.getInterpolatedState(startViewState, endViewState, state.expansion)
         }
-        if (state.squishFraction <= 1f) {
+        // Skip the adjustments of squish view state if UMO changes due to guts animation.
+        if (state.squishFraction <= 1f && !isGutsAnimation) {
             return squishViewState(result, state.squishFraction)
         }
         return result
@@ -521,7 +527,8 @@
         @MediaLocation startLocation: Int,
         @MediaLocation endLocation: Int,
         transitionProgress: Float,
-        applyImmediately: Boolean
+        applyImmediately: Boolean,
+        isGutsAnimation: Boolean = false,
     ) =
         traceSection("MediaViewController#setCurrentState") {
             currentEndLocation = endLocation
@@ -537,7 +544,7 @@
             // Obtain the view state that we'd want to be at the end
             // The view might not be bound yet or has never been measured and in that case will be
             // reset once the state is fully available
-            var endViewState = obtainViewState(endHostState) ?: return
+            var endViewState = obtainViewState(endHostState, isGutsAnimation) ?: return
             endViewState = updateViewStateSize(endViewState, endLocation, tmpState2)!!
             layoutController.setMeasureState(endViewState)
 
@@ -548,7 +555,7 @@
             }
 
             val result: TransitionViewState
-            var startViewState = obtainViewState(startHostState)
+            var startViewState = obtainViewState(startHostState, isGutsAnimation)
             startViewState = updateViewStateSize(startViewState, startLocation, tmpState3)
 
             if (!endHostState.visible) {
@@ -602,7 +609,8 @@
                 applyImmediately,
                 shouldAnimate,
                 animationDuration,
-                animationDelay
+                animationDelay,
+                isGutsAnimation,
             )
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 285cb5a..e9c930a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -1483,16 +1483,16 @@
     }
 
     private void updateMaxDisplayedNotifications(boolean recompute) {
+        if (mFeatureFlags.isEnabled(Flags.MIGRATE_NSSL)) {
+            return;
+        }
+
         if (recompute) {
             setMaxDisplayedNotifications(Math.max(computeMaxKeyguardNotifications(), 1));
         } else {
             if (SPEW_LOGCAT) Log.d(TAG, "Skipping computeMaxKeyguardNotifications() by request");
         }
 
-        if (mFeatureFlags.isEnabled(Flags.MIGRATE_NSSL)) {
-            return;
-        }
-
         if (isKeyguardShowing() && !mKeyguardBypassController.getBypassEnabled()) {
             mNotificationStackScrollLayoutController.setMaxDisplayedNotifications(
                     mMaxAllowedKeyguardNotifications);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
index e2e4556..8bab669 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -65,6 +65,10 @@
      */
     @Deprecated("Use ShadeInteractor instead") val legacyShadeTracking: StateFlow<Boolean>
 
+    /** Specifically tracks the user expanding the shade on the lockscreen only */
+    @Deprecated("Use ShadeInteractor.isUserInteractingWithShade instead")
+    val legacyLockscreenShadeTracking: MutableStateFlow<Boolean>
+
     /**
      * QuickSettingsController.mTracking as a flow. "Tracking" means that the user is moving quick
      * settings up or down with a pointer. Going forward, this concept will be replaced by checks
@@ -106,6 +110,9 @@
     /** Sets whether the user is moving the shade with a pointer */
     fun setLegacyShadeTracking(tracking: Boolean)
 
+    /** Sets whether the user is moving the shade with a pointer, on lockscreen only */
+    fun setLegacyLockscreenShadeTracking(tracking: Boolean)
+
     /** Amount shade has expanded with regard to the UDFPS location */
     val udfpsTransitionToFullShadeProgress: StateFlow<Float>
 
@@ -177,6 +184,8 @@
     @Deprecated("Use ShadeInteractor instead")
     override val legacyShadeTracking: StateFlow<Boolean> = _legacyShadeTracking.asStateFlow()
 
+    override val legacyLockscreenShadeTracking = MutableStateFlow(false)
+
     private val _legacyQsTracking = MutableStateFlow(false)
     @Deprecated("Use ShadeInteractor instead")
     override val legacyQsTracking: StateFlow<Boolean> = _legacyQsTracking.asStateFlow()
@@ -212,6 +221,11 @@
         _legacyShadeTracking.value = tracking
     }
 
+    @Deprecated("Should only be called by NPVC and tests")
+    override fun setLegacyLockscreenShadeTracking(tracking: Boolean) {
+        legacyLockscreenShadeTracking.value = tracking
+    }
+
     override fun setQsExpansion(qsExpansion: Float) {
         _qsExpansion.value = qsExpansion
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index b2ffeb3..d687ef6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -72,7 +72,7 @@
     userSetupRepository: UserSetupRepository,
     userSwitcherInteractor: UserSwitcherInteractor,
     sharedNotificationContainerInteractor: SharedNotificationContainerInteractor,
-    repository: ShadeRepository,
+    private val repository: ShadeRepository,
 ) {
     /** Emits true if the shade is currently allowed and false otherwise. */
     val isShadeEnabled: StateFlow<Boolean> =
@@ -185,7 +185,15 @@
         if (sceneContainerFlags.isEnabled()) {
             sceneBasedInteracting(sceneInteractorProvider.get(), SceneKey.Shade)
         } else {
-            userInteractingFlow(repository.legacyShadeTracking, repository.legacyShadeExpansion)
+            combine(
+                userInteractingFlow(
+                    repository.legacyShadeTracking,
+                    repository.legacyShadeExpansion
+                ),
+                repository.legacyLockscreenShadeTracking
+            ) { legacyShadeTracking, legacyLockscreenShadeTracking ->
+                legacyShadeTracking || legacyLockscreenShadeTracking
+            }
         }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index bf722af..2e3f3f8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -340,6 +340,7 @@
         )
         nsslController.resetScrollPosition()
         nsslController.resetCheckSnoozeLeavebehind()
+        shadeRepository.setLegacyLockscreenShadeTracking(false)
         setDragDownAmountAnimated(0f)
     }
 
@@ -366,6 +367,7 @@
                 cancel()
             }
         }
+        shadeRepository.setLegacyLockscreenShadeTracking(true)
     }
 
     /** Do we need a falsing check currently? */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index 3f080c2..0e83c78 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -680,7 +680,7 @@
         }
         NotificationEntry entry = mCommonNotifCollectionLazy.get().getEntry(key);
         if (mFeatureFlags.isEnabled(Flags.NOTIF_LS_BACKGROUND_THREAD)) {
-            return entry != null
+            return entry != null && entry.getRanking().getChannel() != null
                     && entry.getRanking().getChannel().getLockscreenVisibility()
                     == Notification.VISIBILITY_PRIVATE;
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index a0ffba3..14ec08f35 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -253,6 +253,7 @@
     private NotificationLogger.OnChildLocationsChangedListener mListener;
     private OnOverscrollTopChangedListener mOverscrollTopChangedListener;
     private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
+    private Runnable mOnHeightChangedRunnable;
     private OnEmptySpaceClickListener mOnEmptySpaceClickListener;
     private boolean mNeedsAnimation;
     private boolean mTopPaddingNeedsAnimation;
@@ -1121,6 +1122,10 @@
         if (mOnHeightChangedListener != null) {
             mOnHeightChangedListener.onHeightChanged(view, needsAnimation);
         }
+
+        if (mOnHeightChangedRunnable != null) {
+            mOnHeightChangedRunnable.run();
+        }
     }
 
     public boolean isPulseExpanding() {
@@ -4252,6 +4257,10 @@
         this.mOnHeightChangedListener = onHeightChangedListener;
     }
 
+    void setOnHeightChangedRunnable(Runnable r) {
+        this.mOnHeightChangedRunnable = r;
+    }
+
     void onChildAnimationFinished() {
         setAnimationRunning(false);
         requestChildrenUpdate();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 99b3a00..2cf0c26 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -20,6 +20,7 @@
 import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
 
 import static com.android.app.animation.Interpolators.STANDARD;
+
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
 import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
@@ -34,7 +35,6 @@
 
 import android.animation.ObjectAnimator;
 import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.graphics.Point;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -65,9 +65,7 @@
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
 import com.android.systemui.classifier.Classifier;
 import com.android.systemui.classifier.FalsingCollector;
-import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FeatureFlagsClassic;
 import com.android.systemui.flags.Flags;
@@ -94,7 +92,6 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.LaunchAnimationParameters;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -112,7 +109,6 @@
 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
 import com.android.systemui.statusbar.notification.dagger.SilentHeader;
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore;
 import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
@@ -122,11 +118,9 @@
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.row.NotificationSnooze;
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
@@ -960,6 +954,13 @@
         mView.setOnHeightChangedListener(listener);
     }
 
+    /**
+     * Invoked in addition to {@see #setOnHeightChangedListener}
+     */
+    public void setOnHeightChangedRunnable(Runnable r) {
+        mView.setOnHeightChangedRunnable(r);
+    }
+
     public void setOverscrollTopChangedListener(
             OnOverscrollTopChangedListener listener) {
         mView.setOverscrollTopChangedListener(listener);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
index 57cea5d..eb1c17a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
@@ -18,13 +18,15 @@
 package com.android.systemui.statusbar.notification.stack.domain.interactor
 
 import android.content.Context
-import com.android.systemui.res.R
 import com.android.systemui.common.ui.data.repository.ConfigurationRepository
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.SplitShadeStateController
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
@@ -43,6 +45,10 @@
     private val _topPosition = MutableStateFlow(0f)
     val topPosition = _topPosition.asStateFlow()
 
+    private val _notificationStackChanged = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
+    /** An internal modification was made to notifications */
+    val notificationStackChanged = _notificationStackChanged.asSharedFlow()
+
     val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
         configurationRepository.onAnyConfigurationChange
             .onStart { emit(Unit) }
@@ -72,6 +78,11 @@
         _topPosition.value = top
     }
 
+    /** An internal modification was made to notifications */
+    fun notificationStackChanged() {
+        _notificationStackChanged.tryEmit(Unit)
+    }
+
     data class ConfigurationBasedDimensions(
         val useSplitShade: Boolean,
         val useLargeScreenHeader: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index a1a0cca..0ff1bec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -20,6 +20,7 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
 import kotlinx.coroutines.DisposableHandle
@@ -33,6 +34,7 @@
         view: SharedNotificationContainer,
         viewModel: SharedNotificationContainerViewModel,
         controller: NotificationStackScrollLayoutController,
+        notificationStackSizeCalculator: NotificationStackSizeCalculator,
     ): DisposableHandle {
         val disposableHandle =
             view.repeatWhenAttached {
@@ -54,9 +56,16 @@
                     }
 
                     launch {
-                        viewModel.maxNotifications.collect {
-                            controller.setMaxDisplayedNotifications(it)
-                        }
+                        viewModel
+                            .getMaxNotifications { space ->
+                                notificationStackSizeCalculator.computeMaxKeyguardNotifications(
+                                    controller.getView(),
+                                    space,
+                                    0f, // Vertical space for shelf is already accounted for
+                                    controller.getShelfHeight().toFloat(),
+                                )
+                            }
+                            .collect { controller.setMaxDisplayedNotifications(it) }
                     }
 
                     launch {
@@ -70,9 +79,12 @@
                 }
             }
 
+        controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() })
+
         return object : DisposableHandle {
             override fun dispose() {
                 disposableHandle.dispose()
+                controller.setOnHeightChangedRunnable(null)
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index b86b5dc..d6b6f75 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -22,28 +22,26 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.util.kotlin.sample
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.combineTransform
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
 
 /** View-model for the shared notification container, used by both the shade and keyguard spaces */
 class SharedNotificationContainerViewModel
 @Inject
 constructor(
-    interactor: SharedNotificationContainerInteractor,
+    private val interactor: SharedNotificationContainerInteractor,
     keyguardInteractor: KeyguardInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
-    notificationStackSizeCalculator: NotificationStackSizeCalculator,
-    controller: NotificationStackScrollLayoutController,
-    shadeInteractor: ShadeInteractor,
+    private val shadeInteractor: ShadeInteractor,
 ) {
     private val statesForConstrainedNotifications =
         setOf(
@@ -151,24 +149,46 @@
      * When on keyguard, there is limited space to display notifications so calculate how many could
      * be shown. Otherwise, there is no limit since the vertical space will be scrollable.
      *
-     * TODO: b/296606746 - Need to rerun logic when notifs change
+     * When expanding or when the user is interacting with the shade, keep the count stable; do not
+     * emit a value.
      */
-    val maxNotifications: Flow<Int> =
-        combine(isOnLockscreen, shadeInteractor.shadeExpansion, position) {
-            onLockscreen,
-            shadeExpansion,
-            positionInfo ->
-            if (onLockscreen && shadeExpansion < 1f) {
-                notificationStackSizeCalculator.computeMaxKeyguardNotifications(
-                    controller.getView(),
-                    positionInfo.bottom - positionInfo.top,
-                    0f, // Vertical space for shelf is already accounted for
-                    controller.getShelfHeight().toFloat(),
-                )
-            } else {
-                -1 // No limit
+    fun getMaxNotifications(calculateSpace: (Float) -> Int): Flow<Int> {
+        // When to limit notifications: on lockscreen with an unexpanded shade. Also, recalculate
+        // when the notification stack has changed internally
+        val limitedNotifications =
+            combineTransform(
+                isOnLockscreen,
+                position,
+                shadeInteractor.shadeExpansion,
+                interactor.notificationStackChanged.onStart { emit(Unit) },
+            ) { isOnLockscreen, position, shadeExpansion, _ ->
+                if (isOnLockscreen && shadeExpansion == 0f) {
+                    emit(calculateSpace(position.bottom - position.top))
+                }
             }
-        }
+
+        // When to show unlimited notifications: When the shade is fully expanded and the user is
+        // not actively dragging the shade
+        val unlimitedNotifications =
+            combineTransform(
+                shadeInteractor.shadeExpansion,
+                shadeInteractor.isUserInteracting,
+            ) { shadeExpansion, isUserInteracting ->
+                if (shadeExpansion == 1f && !isUserInteracting) {
+                    emit(-1)
+                }
+            }
+
+        return merge(
+                limitedNotifications,
+                unlimitedNotifications,
+            )
+            .distinctUntilChanged()
+    }
+
+    fun notificationStackChanged() {
+        interactor.notificationStackChanged()
+    }
 
     data class ConfigurationBasedDimensions(
         val marginStart: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt
index db4ab7e..5b91615 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt
@@ -25,6 +25,16 @@
  * The fraction after which we start fading in when going from a gone widget to a visible one
  */
 private const val GONE_FADE_FRACTION = 0.8f
+/**
+ * The fraction after which we start fading in going from a gone widget to a visible one in guts
+ * animation.
+ */
+private const val GONE_FADE_GUTS_FRACTION = 0.286f
+/**
+ * The fraction before which we fade out when going from a visible widget to a gone one in guts
+ * animation.
+ */
+private const val VISIBLE_FADE_GUTS_FRACTION = 0.355f
 
 /**
  * The amont we're scaling appearing views
@@ -45,6 +55,7 @@
     private var animationStartState: TransitionViewState? = null
     private var state = TransitionViewState()
     private var animator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f)
+    private var isGutsAnimation = false
     private var currentHeight: Int = 0
     private var currentWidth: Int = 0
     var sizeChangedListener: ((Int, Int) -> Unit)? = null
@@ -152,15 +163,6 @@
                 // this looks quite ugly
                 val nowGone: Boolean
                 if (widgetStart.gone) {
-
-                    // Only fade it in at the very end
-                    alphaProgress = MathUtils.map(GONE_FADE_FRACTION, 1.0f, 0.0f, 1.0f, progress)
-                    nowGone = progress < GONE_FADE_FRACTION
-
-                    // Scale it just a little, not all the way
-                    val endScale = widgetEnd.scale
-                    newScale = MathUtils.lerp(GONE_SCALE_AMOUNT * endScale, endScale, progress)
-
                     // don't clip
                     widthProgress = 1.0f
 
@@ -168,25 +170,52 @@
                     resultMeasureWidth = widgetEnd.measureWidth
                     resultMeasureHeight = widgetEnd.measureHeight
 
-                    // Let's make sure we're centering the view in the gone view instead of having
-                    // the left at 0
-                    resultX = MathUtils.lerp(widgetStart.x - resultMeasureWidth / 2.0f,
-                            widgetEnd.x,
-                            progress)
-                    resultY = MathUtils.lerp(widgetStart.y - resultMeasureHeight / 2.0f,
-                            widgetEnd.y,
-                            progress)
+                    if (isGutsAnimation) {
+                        // if animation is open/close guts, fade in starts early.
+                        alphaProgress = MathUtils.map(
+                            GONE_FADE_GUTS_FRACTION,
+                            1.0f,
+                            0.0f,
+                            1.0f,
+                            progress
+                        )
+                        nowGone = progress < GONE_FADE_GUTS_FRACTION
+
+                        // Do not change scale of widget.
+                        newScale = 1.0f
+
+                        // We do not want any horizontal or vertical movement.
+                        resultX = widgetStart.x
+                        resultY = widgetStart.y
+                    } else {
+                        // Only fade it in at the very end
+                        alphaProgress = MathUtils.map(
+                            GONE_FADE_FRACTION,
+                            1.0f,
+                            0.0f,
+                            1.0f,
+                            progress
+                        )
+                        nowGone = progress < GONE_FADE_FRACTION
+
+                        // Scale it just a little, not all the way
+                        val endScale = widgetEnd.scale
+                        newScale = MathUtils.lerp(GONE_SCALE_AMOUNT * endScale, endScale, progress)
+
+                        // Let's make sure we're centering the view in the gone view instead of
+                        // having the left at 0
+                        resultX = MathUtils.lerp(
+                                widgetStart.x - resultMeasureWidth / 2.0f,
+                                widgetEnd.x,
+                                progress
+                        )
+                        resultY = MathUtils.lerp(
+                                widgetStart.y - resultMeasureHeight / 2.0f,
+                                widgetEnd.y,
+                                progress
+                        )
+                    }
                 } else {
-
-                    // Fadeout in the very beginning
-                    alphaProgress = MathUtils.map(0.0f, 1.0f - GONE_FADE_FRACTION, 0.0f, 1.0f,
-                            progress)
-                    nowGone = progress > 1.0f - GONE_FADE_FRACTION
-
-                    // Scale it just a little, not all the way
-                    val startScale = widgetStart.scale
-                    newScale = MathUtils.lerp(startScale, startScale * GONE_SCALE_AMOUNT, progress)
-
                     // Don't clip
                     widthProgress = 0.0f
 
@@ -194,14 +223,54 @@
                     resultMeasureWidth = widgetStart.measureWidth
                     resultMeasureHeight = widgetStart.measureHeight
 
-                    // Let's make sure we're centering the view in the gone view instead of having
-                    // the left at 0
-                    resultX = MathUtils.lerp(widgetStart.x,
-                            widgetEnd.x - resultMeasureWidth / 2.0f,
-                            progress)
-                    resultY = MathUtils.lerp(widgetStart.y,
-                            widgetEnd.y - resultMeasureHeight / 2.0f,
-                            progress)
+                    // Fadeout in the very beginning
+                    if (isGutsAnimation) {
+                        alphaProgress = MathUtils.map(
+                            0.0f,
+                            VISIBLE_FADE_GUTS_FRACTION,
+                            0.0f,
+                            1.0f,
+                            progress
+                        )
+                        nowGone = progress > VISIBLE_FADE_GUTS_FRACTION
+
+                        // Do not change scale of widget during open/close guts animation.
+                        newScale = 1.0f
+
+                        // We do not want any horizontal or vertical movement.
+                        resultX = widgetEnd.x
+                        resultY = widgetEnd.y
+                    } else {
+                        alphaProgress = MathUtils.map(
+                            0.0f,
+                            1.0f - GONE_FADE_FRACTION,
+                            0.0f,
+                            1.0f,
+                            progress
+                        )
+                        nowGone = progress > 1.0f - GONE_FADE_FRACTION
+
+                        // Scale it just a little, not all the way
+                        val startScale = widgetStart.scale
+                        newScale = MathUtils.lerp(
+                                startScale,
+                                startScale * GONE_SCALE_AMOUNT,
+                                progress
+                        )
+
+                        // Let's make sure we're centering the view in the gone view instead of
+                        // having the left at 0
+                        resultX = MathUtils.lerp(
+                                widgetStart.x,
+                                widgetEnd.x - resultMeasureWidth / 2.0f,
+                                progress
+                        )
+                        resultY = MathUtils.lerp(
+                                widgetStart.y,
+                                widgetEnd.y - resultMeasureHeight / 2.0f,
+                                progress
+                        )
+                    }
                 }
                 resultWidgetState.gone = nowGone
             } else {
@@ -279,8 +348,10 @@
         applyImmediately: Boolean,
         animate: Boolean,
         duration: Long = 0,
-        delay: Long = 0
+        delay: Long = 0,
+        isGuts: Boolean,
     ) {
+        isGutsAnimation = isGuts
         val animated = animate && currentState.width != 0
         this.state = state.copy()
         if (applyImmediately || transitionLayout == null) {
@@ -291,6 +362,8 @@
             animationStartState = currentState.copy()
             animator.duration = duration
             animator.startDelay = delay
+            animator.interpolator =
+                if (isGutsAnimation) Interpolators.LINEAR else Interpolators.FAST_OUT_SLOW_IN
             animator.start()
         } else if (!animator.isRunning) {
             applyStateToLayout(this.state)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
index b101acf..437a35f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDeviceManagerTest.kt
@@ -38,8 +38,6 @@
 import com.android.settingslib.media.PhoneMediaDevice
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FakeFeatureFlagsClassic
-import com.android.systemui.flags.Flags
 import com.android.systemui.media.controls.MediaTestUtils
 import com.android.systemui.media.controls.models.player.MediaData
 import com.android.systemui.media.controls.models.player.MediaDeviceData
@@ -112,7 +110,6 @@
     private lateinit var session: MediaSession
     private lateinit var mediaData: MediaData
     @JvmField @Rule val mockito = MockitoJUnit.rule()
-    private val featureFlags = FakeFeatureFlagsClassic()
 
     @Before
     fun setUp() {
@@ -131,7 +128,6 @@
                 fakeFgExecutor,
                 fakeBgExecutor,
                 dumpster,
-                featureFlags,
             )
         manager.addListener(listener)
 
@@ -150,7 +146,6 @@
             MediaTestUtils.emptyMediaData.copy(packageName = PACKAGE, token = session.sessionToken)
         whenever(controllerFactory.create(session.sessionToken)).thenReturn(controller)
         setupLeAudioConfiguration(false)
-        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, false)
     }
 
     @After
@@ -463,7 +458,6 @@
 
     @Test
     fun mr2ReturnsSystemRouteWithNullName_isPhone_usePhoneName() {
-        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
         // When the routing session name is null, and is a system session for a PhoneMediaDevice
         val phoneDevice = mock(PhoneMediaDevice::class.java)
         whenever(phoneDevice.iconWithoutBackground).thenReturn(icon)
@@ -489,7 +483,6 @@
 
     @Test
     fun mr2ReturnsSystemRouteWithNullName_useSelectedRouteName() {
-        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
         // When the routing session does not have a name, and is a system session
         whenever(route.name).thenReturn(null)
         whenever(mr2.getSelectedRoutes(any())).thenReturn(listOf(selectedRoute))
@@ -725,101 +718,6 @@
         assertThat(data.showBroadcastButton).isFalse()
     }
 
-    // Duplicates of above tests with MEDIA_DEVICE_NAME_FIX enabled
-
-    @Test
-    fun loadMediaDataWithNullToken_withNameFix() {
-        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
-        manager.onMediaDataLoaded(KEY, null, mediaData.copy(token = null))
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(DEVICE_NAME)
-    }
-
-    @Test
-    fun onAboutToConnectDeviceAdded_findsDeviceInfoFromAddress_withNameFix() {
-        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        // Run and reset the executors and listeners so we only focus on new events.
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        reset(listener)
-
-        // Ensure we'll get device info when using the address
-        val fullMediaDevice = mock(MediaDevice::class.java)
-        val address = "fakeAddress"
-        val nameFromDevice = "nameFromDevice"
-        val iconFromDevice = mock(Drawable::class.java)
-        whenever(lmm.getMediaDeviceById(eq(address))).thenReturn(fullMediaDevice)
-        whenever(fullMediaDevice.name).thenReturn(nameFromDevice)
-        whenever(fullMediaDevice.iconWithoutBackground).thenReturn(iconFromDevice)
-
-        // WHEN the about-to-connect device changes to non-null
-        val deviceCallback = captureCallback()
-        val nameFromParam = "nameFromParam"
-        val iconFromParam = mock(Drawable::class.java)
-        deviceCallback.onAboutToConnectDeviceAdded(address, nameFromParam, iconFromParam)
-        assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
-
-        // THEN the about-to-connect device based on the address is returned
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(nameFromDevice)
-        assertThat(data.name).isNotEqualTo(nameFromParam)
-        assertThat(data.icon).isEqualTo(iconFromDevice)
-        assertThat(data.icon).isNotEqualTo(iconFromParam)
-    }
-
-    @Test
-    fun deviceNameFromMR2RouteInfo_withNameFix() {
-        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
-        // GIVEN that MR2Manager returns a valid routing session
-        whenever(route.name).thenReturn(REMOTE_DEVICE_NAME)
-        // WHEN a notification is added
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        // THEN it uses the route name (instead of device name)
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isTrue()
-        assertThat(data.name).isEqualTo(REMOTE_DEVICE_NAME)
-    }
-
-    @Test
-    fun deviceDisabledWhenMR2ReturnsNullRouteInfo_withNameFix() {
-        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
-        // GIVEN that MR2Manager returns null for routing session
-        whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
-        // WHEN a notification is added
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        // THEN the device is disabled and name is set to null
-        val data = captureDeviceData(KEY)
-        assertThat(data.enabled).isFalse()
-        assertThat(data.name).isNull()
-    }
-
-    @Test
-    fun mr2ReturnsNonSystemRouteWithNullName_useLocalDeviceName_withNameFix() {
-        featureFlags.set(Flags.MEDIA_DEVICE_NAME_FIX, true)
-        // GIVEN that MR2Manager returns a routing session that does not have a name
-        whenever(route.name).thenReturn(null)
-        whenever(route.isSystemSession).thenReturn(false)
-        // WHEN a notification is added
-        manager.onMediaDataLoaded(KEY, null, mediaData)
-        fakeBgExecutor.runAllReady()
-        fakeFgExecutor.runAllReady()
-        // THEN the device is enabled and uses the current connected device name
-        val data = captureDeviceData(KEY)
-        assertThat(data.name).isEqualTo(DEVICE_NAME)
-        assertThat(data.enabled).isTrue()
-    }
-
-    // End duplicate tests
-
     private fun captureCallback(): LocalMediaManager.DeviceCallback {
         val captor = ArgumentCaptor.forClass(LocalMediaManager.DeviceCallback::class.java)
         verify(lmm).registerCallback(captor.capture())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
index e920687..20b19fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeRepositoryImplTest.kt
@@ -158,6 +158,15 @@
         }
 
     @Test
+    fun updateLegacyLockscreenShadeTracking() =
+        testScope.runTest {
+            assertThat(underTest.legacyLockscreenShadeTracking.value).isEqualTo(false)
+
+            underTest.setLegacyLockscreenShadeTracking(true)
+            assertThat(underTest.legacyLockscreenShadeTracking.value).isEqualTo(true)
+        }
+
+    @Test
     fun updateLegacyQsTracking() =
         testScope.runTest {
             assertThat(underTest.legacyQsTracking.value).isEqualTo(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index a1425f8..ae32142 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -323,6 +323,20 @@
     }
 
     @Test
+    public void testCurrentUserPrivateNotificationsNullChannel() {
+        // GIVEN current user allows private notifications to show
+        mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 1,
+                mCurrentUser.id);
+        changeSetting(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
+
+        mCurrentUserNotif.setRanking(new RankingBuilder(mCurrentUserNotif.getRanking())
+                .setChannel(null)
+                .setVisibilityOverride(VISIBILITY_NO_OVERRIDE).build());
+        // THEN the notification is not redacted
+        assertFalse(mLockscreenUserManager.needsRedaction(mCurrentUserNotif));
+    }
+
+    @Test
     public void testWorkPrivateNotificationsRedacted() {
         // GIVEN work profile doesn't private notifications to show
         mSettings.putIntForUser(LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 6203531..e91d6d7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -83,6 +83,7 @@
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
 import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
@@ -906,6 +907,20 @@
         assertEquals(bottomImeInset, mStackScrollerInternal.mBottomInset);
     }
 
+    @Test
+    public void testSetMaxDisplayedNotifications_notifiesListeners() {
+        ExpandableView.OnHeightChangedListener listener =
+                mock(ExpandableView.OnHeightChangedListener.class);
+        Runnable runnable = mock(Runnable.class);
+        mStackScroller.setOnHeightChangedListener(listener);
+        mStackScroller.setOnHeightChangedRunnable(runnable);
+
+        mStackScroller.setMaxDisplayedNotifications(50);
+
+        verify(listener).onHeightChanged(mNotificationShelf, false);
+        verify(runnable).run();
+    }
+
     private void setBarStateForTest(int state) {
         // Can't inject this through the listener or we end up on the actual implementation
         // rather than the mock because the spy just coppied the anonymous inner /shruggie.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 22553df..db8f217 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -40,13 +40,8 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.res.R
 import com.android.systemui.shade.data.repository.FakeShadeRepository
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.user.domain.UserDomainLayerModule
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import dagger.BindsInstance
 import dagger.Component
@@ -84,25 +79,13 @@
         }
     }
 
-    private val notificationStackSizeCalculator: NotificationStackSizeCalculator = mock()
-    private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController =
-        mock {
-            whenever(view).thenReturn(mock())
-            whenever(shelfHeight).thenReturn(0)
-        }
-
     private val testComponent: TestComponent =
         DaggerSharedNotificationContainerViewModelTest_TestComponent.factory()
             .create(
                 test = this,
                 featureFlags =
                     FakeFeatureFlagsClassicModule { set(Flags.FULL_SCREEN_USER_SWITCHER, true) },
-                mocks =
-                    TestMocksModule(
-                        notificationStackSizeCalculator = notificationStackSizeCalculator,
-                        notificationStackScrollLayoutController =
-                            notificationStackScrollLayoutController,
-                    )
+                mocks = TestMocksModule(),
             )
 
     @Test
@@ -336,17 +319,9 @@
     @Test
     fun maxNotificationsOnLockscreen() =
         testComponent.runTest {
-            whenever(
-                    notificationStackSizeCalculator.computeMaxKeyguardNotifications(
-                        any(),
-                        any(),
-                        any(),
-                        any()
-                    )
-                )
-                .thenReturn(10)
-
-            val maxNotifications by collectLastValue(underTest.maxNotifications)
+            var notificationCount = 10
+            val maxNotifications by
+                collectLastValue(underTest.getMaxNotifications { notificationCount })
 
             showLockscreen()
 
@@ -356,21 +331,52 @@
                 SharedNotificationContainerPosition(top = 1f, bottom = 2f)
 
             assertThat(maxNotifications).isEqualTo(10)
+
+            // Also updates when directly requested (as it would from NotificationStackScrollLayout)
+            notificationCount = 25
+            sharedNotificationContainerInteractor.notificationStackChanged()
+            assertThat(maxNotifications).isEqualTo(25)
+        }
+
+    @Test
+    fun maxNotificationsOnLockscreen_DoesNotUpdateWhenUserInteracting() =
+        testComponent.runTest {
+            var notificationCount = 10
+            val maxNotifications by
+                collectLastValue(underTest.getMaxNotifications { notificationCount })
+
+            showLockscreen()
+
+            overrideResource(R.bool.config_use_split_notification_shade, false)
+            configurationRepository.onAnyConfigurationChange()
+            keyguardInteractor.sharedNotificationContainerPosition.value =
+                SharedNotificationContainerPosition(top = 1f, bottom = 2f)
+
+            assertThat(maxNotifications).isEqualTo(10)
+
+            // Shade expanding... still 10
+            shadeRepository.setLockscreenShadeExpansion(0.5f)
+            assertThat(maxNotifications).isEqualTo(10)
+
+            notificationCount = 25
+
+            // When shade is expanding by user interaction
+            shadeRepository.setLegacyLockscreenShadeTracking(true)
+
+            // Should still be 10, since the user is interacting
+            assertThat(maxNotifications).isEqualTo(10)
+
+            shadeRepository.setLegacyLockscreenShadeTracking(false)
+            shadeRepository.setLockscreenShadeExpansion(0f)
+
+            // Stopped tracking, show 25
+            assertThat(maxNotifications).isEqualTo(25)
         }
 
     @Test
     fun maxNotificationsOnShade() =
         testComponent.runTest {
-            whenever(
-                    notificationStackSizeCalculator.computeMaxKeyguardNotifications(
-                        any(),
-                        any(),
-                        any(),
-                        any()
-                    )
-                )
-                .thenReturn(10)
-            val maxNotifications by collectLastValue(underTest.maxNotifications)
+            val maxNotifications by collectLastValue(underTest.getMaxNotifications { 10 })
 
             // Show lockscreen with shade expanded
             showLockscreenWithShadeExpanded()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
index 800593f..02318ab 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
@@ -59,6 +59,8 @@
     private val _legacyIsQsExpanded = MutableStateFlow(false)
     @Deprecated("Use ShadeInteractor instead") override val legacyIsQsExpanded = _legacyIsQsExpanded
 
+    override val legacyLockscreenShadeTracking = MutableStateFlow(false)
+
     @Deprecated("Use ShadeInteractor instead")
     override fun setLegacyIsQsExpanded(legacyIsQsExpanded: Boolean) {
         _legacyIsQsExpanded.value = legacyIsQsExpanded
@@ -81,6 +83,11 @@
         _legacyShadeTracking.value = tracking
     }
 
+    @Deprecated("Should only be called by NPVC and tests")
+    override fun setLegacyLockscreenShadeTracking(tracking: Boolean) {
+        legacyLockscreenShadeTracking.value = tracking
+    }
+
     fun setShadeModel(model: ShadeModel) {
         _shadeModel.value = model
     }
diff --git a/services/companion/java/com/android/server/companion/virtual/OWNERS b/services/companion/java/com/android/server/companion/virtual/OWNERS
index 83143a4..5295ec8 100644
--- a/services/companion/java/com/android/server/companion/virtual/OWNERS
+++ b/services/companion/java/com/android/server/companion/virtual/OWNERS
@@ -1,3 +1,5 @@
+# Bug component: 1171888
+
 set noparent
 
 ogunwale@google.com
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 15fc2dc..f6835fe 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -3651,7 +3651,26 @@
             Watchdog.getInstance().pauseWatchingMonitorsFor(
                 SLOW_OPERATION_WATCHDOG_TIMEOUT_MS, "#close might be slow");
             if (mMounted) {
-                mVold.unmountAppFuse(uid, mountId);
+                BackgroundThread.getHandler().post(() -> {
+                    try {
+                        // We need to run the unmount on a separate thread to
+                        // prevent a possible deadlock, where:
+                        // 1. AppFuseThread (this thread) tries to call into vold
+                        // 2. the vold lock is held by another thread, which called:
+                        //    mVold.openAppFuseFile()
+                        //    as part of that call, vold calls open() on the
+                        //    underlying file, which is a call that needs to be
+                        //    handled by the AppFuseThread, which is stuck waiting
+                        //    for the vold lock (see 1.)
+                        // It is safe to do the unmount asynchronously, because the mount
+                        // path we use is never reused during the current boot cycle;
+                        // see mNextAppFuseName. Also,we have anyway stopped serving
+                        // requests at this point.
+                        mVold.unmountAppFuse(uid, mountId);
+                    } catch (RemoteException e) {
+                        throw e.rethrowAsRuntimeException();
+                    }
+                });
                 mMounted = false;
             }
         }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b99a98f..f92af67 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -599,6 +599,9 @@
     private static final String INTENT_REMOTE_BUGREPORT_FINISHED =
             "com.android.internal.intent.action.REMOTE_BUGREPORT_FINISHED";
 
+    public static final String DATA_FILE_PATH_HEADER = "Data File: ";
+    public static final String DATA_FILE_PATH_FOOTER = "End Data File\n";
+
     // If set, we will push process association information in to procstats.
     static final boolean TRACK_PROCSTATS_ASSOCIATIONS = true;
 
@@ -9595,17 +9598,33 @@
                         : Settings.Global.getInt(mContext.getContentResolver(), logcatSetting, 0);
                 int dropboxMaxSize = Settings.Global.getInt(
                         mContext.getContentResolver(), maxBytesSetting, DROPBOX_DEFAULT_MAX_SIZE);
-                int maxDataFileSize = dropboxMaxSize - sb.length()
-                        - lines * RESERVED_BYTES_PER_LOGCAT_LINE;
 
-                if (dataFile != null && maxDataFileSize > 0) {
-                    try {
-                        sb.append(FileUtils.readTextFile(dataFile, maxDataFileSize,
-                                    "\n\n[[TRUNCATED]]"));
-                    } catch (IOException e) {
-                        Slog.e(TAG, "Error reading " + dataFile, e);
+                if (dataFile != null) {
+                    // Attach the stack traces file to the report so collectors can load them
+                    // by file if they have access.
+                    sb.append(DATA_FILE_PATH_HEADER)
+                            .append(dataFile.getAbsolutePath()).append('\n');
+
+                    int maxDataFileSize = dropboxMaxSize
+                            - sb.length()
+                            - lines * RESERVED_BYTES_PER_LOGCAT_LINE
+                            - DATA_FILE_PATH_FOOTER.length();
+
+                    if (maxDataFileSize > 0) {
+                        // Inline dataFile contents if there is room.
+                        try {
+                            sb.append(FileUtils.readTextFile(dataFile, maxDataFileSize,
+                                    "\n\n[[TRUNCATED]]\n"));
+                        } catch (IOException e) {
+                            Slog.e(TAG, "Error reading " + dataFile, e);
+                        }
                     }
+
+                    // Always append the footer, even there wasn't enough space to inline the
+                    // dataFile contents.
+                    sb.append(DATA_FILE_PATH_FOOTER);
                 }
+
                 if (crashInfo != null && crashInfo.stackTrace != null) {
                     sb.append(crashInfo.stackTrace);
                 }
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index e41b6ae..3ce92bc 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -265,13 +265,6 @@
     @Nullable
     public BroadcastRecord enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record,
             int recordIndex, @NonNull BroadcastConsumer deferredStatesApplyConsumer) {
-        // When updateDeferredStates() has already applied a deferred state to
-        // all pending items, apply to this new broadcast too
-        if (mLastDeferredStates && record.deferUntilActive
-                && (record.getDeliveryState(recordIndex) == BroadcastRecord.DELIVERY_PENDING)) {
-            deferredStatesApplyConsumer.accept(record, recordIndex);
-        }
-
         // Ignore FLAG_RECEIVER_REPLACE_PENDING if the sender specified the policy using the
         // BroadcastOptions delivery group APIs.
         if (record.isReplacePending()
@@ -294,6 +287,13 @@
         // with implicit responsiveness expectations.
         getQueueForBroadcast(record).addLast(newBroadcastArgs);
         onBroadcastEnqueued(record, recordIndex);
+
+        // When updateDeferredStates() has already applied a deferred state to
+        // all pending items, apply to this new broadcast too
+        if (mLastDeferredStates && shouldBeDeferred()
+                && (record.getDeliveryState(recordIndex) == BroadcastRecord.DELIVERY_PENDING)) {
+            deferredStatesApplyConsumer.accept(record, recordIndex);
+        }
         return null;
     }
 
@@ -1235,32 +1235,45 @@
     }
 
     /**
-     * Update {@link BroadcastRecord.DELIVERY_DEFERRED} states of all our
+     * Update {@link BroadcastRecord#DELIVERY_DEFERRED} states of all our
      * pending broadcasts, when needed.
      */
     void updateDeferredStates(@NonNull BroadcastConsumer applyConsumer,
             @NonNull BroadcastConsumer clearConsumer) {
         // When all we have pending is deferred broadcasts, and we're cached,
         // then we want everything to be marked deferred
-        final boolean wantDeferredStates = (mCountDeferred > 0)
-                && (mCountDeferred == mCountEnqueued) && mProcessFreezable;
+        final boolean wantDeferredStates = shouldBeDeferred();
 
         if (mLastDeferredStates != wantDeferredStates) {
             mLastDeferredStates = wantDeferredStates;
             if (wantDeferredStates) {
                 forEachMatchingBroadcast((r, i) -> {
-                    return r.deferUntilActive
-                            && (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_PENDING);
+                    return (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_PENDING);
                 }, applyConsumer, false);
             } else {
                 forEachMatchingBroadcast((r, i) -> {
-                    return r.deferUntilActive
-                            && (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_DEFERRED);
+                    return (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_DEFERRED);
                 }, clearConsumer, false);
             }
         }
     }
 
+    void clearDeferredStates(@NonNull BroadcastConsumer clearConsumer) {
+        if (mLastDeferredStates) {
+            mLastDeferredStates = false;
+            forEachMatchingBroadcast((r, i) -> {
+                return (r.getDeliveryState(i) == BroadcastRecord.DELIVERY_DEFERRED);
+            }, clearConsumer, false);
+        }
+    }
+
+    @VisibleForTesting
+    boolean shouldBeDeferred() {
+        if (mRunnableAtInvalidated) updateRunnableAt();
+        return mRunnableAtReason == REASON_CACHED
+                || mRunnableAtReason == REASON_CACHED_INFINITE_DEFER;
+    }
+
     /**
      * Check overall health, confirming things are in a reasonable state and
      * that we're not wedged.
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index b481697..5b54561 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -479,6 +479,10 @@
                 break;
             }
 
+            // Clear the deferred state of broadcasts in this queue as we are just about to
+            // deliver broadcasts to this process.
+            queue.clearDeferredStates(mBroadcastConsumerDeferClear);
+
             // We might not have heard about a newly running process yet, so
             // consider refreshing if we think we're cold
             updateWarmProcess(queue);
@@ -1567,12 +1571,14 @@
         r.resultExtras = null;
     };
 
-    private final BroadcastConsumer mBroadcastConsumerDeferApply = (r, i) -> {
+    @VisibleForTesting
+    final BroadcastConsumer mBroadcastConsumerDeferApply = (r, i) -> {
         setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_DEFERRED,
                 "mBroadcastConsumerDeferApply");
     };
 
-    private final BroadcastConsumer mBroadcastConsumerDeferClear = (r, i) -> {
+    @VisibleForTesting
+    final BroadcastConsumer mBroadcastConsumerDeferClear = (r, i) -> {
         setDeliveryState(null, null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_PENDING,
                 "mBroadcastConsumerDeferClear");
     };
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index a0a7b2b..d0ab287 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -1472,10 +1472,13 @@
             }
             return;
         }
+        boolean processFreezableChangeReported = false;
         if (opt.isPendingFreeze()) {
             // Remove pending DO_FREEZE message
             mFreezeHandler.removeMessages(SET_FROZEN_PROCESS_MSG, app);
             opt.setPendingFreeze(false);
+            reportProcessFreezableChangedLocked(app);
+            processFreezableChangeReported = true;
             if (DEBUG_FREEZER) {
                 Slog.d(TAG_AM, "Cancel freezing " + pid + " " + app.processName);
             }
@@ -1524,7 +1527,9 @@
         if (processKilled) {
             return;
         }
-        reportProcessFreezableChangedLocked(app);
+        if (!processFreezableChangeReported) {
+            reportProcessFreezableChangedLocked(app);
+        }
 
         long freezeTime = opt.getFreezeUnfreezeTime();
 
diff --git a/services/core/java/com/android/server/audio/AdiDeviceState.java b/services/core/java/com/android/server/audio/AdiDeviceState.java
index 51cb950..5c8dd0d 100644
--- a/services/core/java/com/android/server/audio/AdiDeviceState.java
+++ b/services/core/java/com/android/server/audio/AdiDeviceState.java
@@ -25,6 +25,7 @@
 import android.media.AudioDeviceAttributes;
 import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
+import android.media.Utils;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Pair;
@@ -167,8 +168,9 @@
     public String toString() {
         return "type: " + mDeviceType
                 + " internal type: 0x" + Integer.toHexString(mInternalDeviceType)
-                + " addr: " + mDeviceAddress + " bt audio type: "
-                + AudioManager.audioDeviceCategoryToString(mAudioDeviceCategory)
+                + " addr: " + Utils.anonymizeBluetoothAddress(mInternalDeviceType, mDeviceAddress)
+                + " bt audio type: "
+                        + AudioManager.audioDeviceCategoryToString(mAudioDeviceCategory)
                 + " enabled: " + mSAEnabled + " HT: " + mHasHeadTracker
                 + " HTenabled: " + mHeadTrackerEnabled;
     }
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 7ba0827..e9b102b 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -48,6 +48,7 @@
 import android.media.IStrategyPreferredDevicesDispatcher;
 import android.media.MediaMetrics;
 import android.media.MediaRecorder.AudioSource;
+import android.media.Utils;
 import android.media.audiopolicy.AudioProductStrategy;
 import android.media.permission.ClearCallingIdentityContext;
 import android.media.permission.SafeCloseable;
@@ -477,7 +478,7 @@
             return "[DeviceInfo: type:0x" + Integer.toHexString(mDeviceType)
                     + " (" + AudioSystem.getDeviceName(mDeviceType)
                     + ") name:" + mDeviceName
-                    + " addr:" + mDeviceAddress
+                    + " addr:" + Utils.anonymizeBluetoothAddress(mDeviceType, mDeviceAddress)
                     + " codec: " + Integer.toHexString(mDeviceCodecFormat)
                     + " peer addr:" + mPeerDeviceAddress
                     + " group:" + mGroupId
@@ -532,7 +533,7 @@
         mApmConnectedDevices.forEach((keyType, valueAddress) -> {
             pw.println("  " + prefix + " type:0x" + Integer.toHexString(keyType)
                     + " (" + AudioSystem.getDeviceName(keyType)
-                    + ") addr:" + valueAddress); });
+                    + ") addr:" + Utils.anonymizeBluetoothAddress(keyType, valueAddress)); });
         pw.println("\n" + prefix + "Preferred devices for capture preset:");
         mPreferredDevicesForCapturePreset.forEach((capturePreset, devices) -> {
             pw.println("  " + prefix + "capturePreset:" + capturePreset
@@ -1789,7 +1790,8 @@
             // TODO: return;
         } else {
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                    "A2DP device addr=" + address + " now available").printLog(TAG));
+                    "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address)
+                            + " now available").printLog(TAG));
         }
 
         // Reset A2DP suspend state each time a new sink is connected
@@ -2027,7 +2029,8 @@
                 .equals(mApmConnectedDevices.get(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP))) {
             // removing A2DP device not currently used by AudioPolicy, log but don't act on it
             AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
-                    "A2DP device " + address + " made unavailable, was not used")).printLog(TAG));
+                    "A2DP device " + Utils.anonymizeBluetoothAddress(address)
+                            + " made unavailable, was not used")).printLog(TAG));
             mmi.set(MediaMetrics.Property.EARLY_RETURN,
                     "A2DP device made unavailable, was not used")
                     .record();
@@ -2043,13 +2046,15 @@
 
         if (res != AudioSystem.AUDIO_STATUS_OK) {
             AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                    "APM failed to make unavailable A2DP device addr=" + address
+                    "APM failed to make unavailable A2DP device addr="
+                            + Utils.anonymizeBluetoothAddress(address)
                             + " error=" + res).printLog(TAG));
             // TODO:  failed to disconnect, stop here
             // TODO: return;
         } else {
             AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
-                    "A2DP device addr=" + address + " made unavailable")).printLog(TAG));
+                    "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address)
+                            + " made unavailable")).printLog(TAG));
         }
         mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
 
@@ -2238,7 +2243,8 @@
                 // TODO: return;
             } else {
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                        "LE Audio device addr=" + address + " now available").printLog(TAG));
+                        "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address)
+                                + " now available").printLog(TAG));
             }
             // Reset LEA suspend state each time a new sink is connected
             mDeviceBroker.clearLeAudioSuspended(true /* internalOnly */);
@@ -2282,7 +2288,8 @@
                 // TODO: return;
             } else {
                 AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
-                        "LE Audio device addr=" + address + " made unavailable").printLog(TAG));
+                        "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address)
+                                + " made unavailable").printLog(TAG));
             }
             mConnectedDevices.remove(DeviceInfo.makeDeviceListKey(device, address));
         }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index b2ee610..1e38c0f 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -136,6 +136,7 @@
 import android.media.MediaRecorder.AudioSource;
 import android.media.PlayerBase;
 import android.media.Spatializer;
+import android.media.Utils;
 import android.media.VolumeInfo;
 import android.media.VolumePolicy;
 import android.media.audiofx.AudioEffect;
@@ -7470,7 +7471,7 @@
 
         sVolumeLogger.enqueue(new EventLogger.StringEvent("setDeviceVolumeBehavior: dev:"
                 + AudioSystem.getOutputDeviceName(device.getInternalType()) + " addr:"
-                + device.getAddress() + " behavior:"
+                + Utils.anonymizeBluetoothAddress(device.getAddress()) + " behavior:"
                 + AudioDeviceVolumeManager.volumeBehaviorName(deviceVolumeBehavior)
                 + " pack:" + pkgName).printLog(TAG));
         if (pkgName == null) {
@@ -9641,7 +9642,7 @@
     private void avrcpSupportsAbsoluteVolume(String address, boolean support) {
         // address is not used for now, but may be used when multiple a2dp devices are supported
         sVolumeLogger.enqueue(new EventLogger.StringEvent("avrcpSupportsAbsoluteVolume addr="
-                + address + " support=" + support).printLog(TAG));
+                + Utils.anonymizeBluetoothAddress(address) + " support=" + support).printLog(TAG));
         mDeviceBroker.setAvrcpAbsoluteVolumeSupported(support);
         setAvrcpAbsoluteVolumeSupported(support);
     }
@@ -10539,11 +10540,11 @@
     AudioDeviceAttributes retrieveBluetoothAddressUncheked(@NonNull AudioDeviceAttributes ada) {
         Objects.requireNonNull(ada);
         if (AudioSystem.isBluetoothDevice(ada.getInternalType())) {
-            String anonymizedAddress = anonymizeBluetoothAddress(ada.getAddress());
+            String anonymizedAddress = Utils.anonymizeBluetoothAddress(ada.getAddress());
             for (AdiDeviceState ads : mDeviceBroker.getImmutableDeviceInventory()) {
                 if (!(AudioSystem.isBluetoothDevice(ads.getInternalDeviceType())
                         && (ada.getInternalType() == ads.getInternalDeviceType())
-                        && anonymizedAddress.equals(anonymizeBluetoothAddress(
+                        && anonymizedAddress.equals(Utils.anonymizeBluetoothAddress(
                                 ads.getDeviceAddress())))) {
                     continue;
                 }
@@ -10554,19 +10555,6 @@
         return ada;
     }
 
-    /**
-     * Convert a Bluetooth MAC address to an anonymized one when exposed to a non privileged app
-     * Must match the implementation of BluetoothUtils.toAnonymizedAddress()
-     * @param address Mac address to be anonymized
-     * @return anonymized mac address
-     */
-    static String anonymizeBluetoothAddress(String address) {
-        if (address == null || address.length() != "AA:BB:CC:DD:EE:FF".length()) {
-            return null;
-        }
-        return "XX:XX:XX:XX" + address.substring("XX:XX:XX:XX".length());
-    }
-
     private List<AudioDeviceAttributes> anonymizeAudioDeviceAttributesList(
                 List<AudioDeviceAttributes> devices) {
         if (isBluetoothPrividged()) {
@@ -10590,7 +10578,7 @@
             return ada;
         }
         AudioDeviceAttributes res = new AudioDeviceAttributes(ada);
-        res.setAddress(anonymizeBluetoothAddress(ada.getAddress()));
+        res.setAddress(Utils.anonymizeBluetoothAddress(ada.getAddress()));
         return res;
     }
 
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 53fbe8f..a12243b 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -22,7 +22,6 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-import static android.net.RouteInfo.RTN_THROW;
 import static android.net.RouteInfo.RTN_UNREACHABLE;
 import static android.net.VpnManager.NOTIFICATION_CHANNEL_VPN;
 import static android.net.ipsec.ike.IkeSessionParams.ESP_ENCAP_TYPE_AUTO;
@@ -45,12 +44,10 @@
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
@@ -113,7 +110,6 @@
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
 import android.os.CancellationSignal;
-import android.os.FileUtils;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.INetworkManagementService;
@@ -152,7 +148,6 @@
 import com.android.internal.net.LegacyVpnInfo;
 import com.android.internal.net.VpnConfig;
 import com.android.internal.net.VpnProfile;
-import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.BinderUtils;
 import com.android.net.module.util.LinkPropertiesUtils;
 import com.android.net.module.util.NetdUtils;
@@ -202,7 +197,6 @@
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * @hide
@@ -1063,8 +1057,6 @@
         // Store mPackage since it might be reset or might be replaced with the other VPN app.
         final String oldPackage = mPackage;
         final boolean isPackageChanged = !Objects.equals(packageName, oldPackage);
-        // TODO: Remove "SdkLevel.isAtLeastT()" check once VpnManagerService is decoupled from
-        //  ConnectivityServiceTest.
         // Only notify VPN apps that were already always-on, and only if the always-on provider
         // changed, or the lockdown mode changed.
         final boolean shouldNotifyOldPkg = isVpnApp(oldPackage) && mAlwaysOn
@@ -1078,12 +1070,6 @@
 
         saveAlwaysOnPackage();
 
-        // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
-        //  ConnectivityServiceTest.
-        if (!SdkLevel.isAtLeastT()) {
-            return true;
-        }
-
         if (shouldNotifyOldPkg) {
             // If both of shouldNotifyOldPkg & isPackageChanged are true, that means the
             // always-on of old package is disabled or the old package is replaced with the new
@@ -1984,9 +1970,7 @@
         for (String app : packageNames) {
             int uid = getAppUid(mContext, app, userId);
             if (uid != -1) uids.add(uid);
-            // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
-            // ConnectivityServiceTest.
-            if (Process.isApplicationUid(uid) && SdkLevel.isAtLeastT()) {
+            if (Process.isApplicationUid(uid)) {
                 uids.add(Process.toSdkSandboxUid(uid));
             }
         }
@@ -2297,15 +2281,6 @@
 
     private INetworkManagementEventObserver mObserver = new BaseNetworkObserver() {
         @Override
-        public void interfaceStatusChanged(String interfaze, boolean up) {
-            synchronized (Vpn.this) {
-                if (!up && mVpnRunner != null && mVpnRunner instanceof LegacyVpnRunner) {
-                    ((LegacyVpnRunner) mVpnRunner).exitIfOuterInterfaceIs(interfaze);
-                }
-            }
-        }
-
-        @Override
         public void interfaceRemoved(String interfaze) {
             synchronized (Vpn.this) {
                 if (interfaze.equals(mInterface) && jniCheck(interfaze) == 0) {
@@ -2556,17 +2531,6 @@
     private native boolean jniAddAddress(String interfaze, String address, int prefixLen);
     private native boolean jniDelAddress(String interfaze, String address, int prefixLen);
 
-    private static RouteInfo findIPv4DefaultRoute(LinkProperties prop) {
-        for (RouteInfo route : prop.getAllRoutes()) {
-            // Currently legacy VPN only works on IPv4.
-            if (route.isDefaultRoute() && route.getGateway() instanceof Inet4Address) {
-                return route;
-            }
-        }
-
-        throw new IllegalStateException("Unable to find IPv4 default gateway");
-    }
-
     private void enforceNotRestrictedUser() {
         final long token = Binder.clearCallingIdentity();
         try {
@@ -2665,10 +2629,6 @@
             throw new SecurityException("Restricted users cannot establish VPNs");
         }
 
-        final RouteInfo ipv4DefaultRoute = findIPv4DefaultRoute(egress);
-        final String gateway = ipv4DefaultRoute.getGateway().getHostAddress();
-        final String iface = ipv4DefaultRoute.getInterface();
-
         // Load certificates.
         String privateKey = "";
         String userCert = "";
@@ -2700,8 +2660,6 @@
             throw new IllegalStateException("Cannot load credentials");
         }
 
-        // Prepare arguments for racoon.
-        String[] racoon = null;
         switch (profile.type) {
             case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
                 // Secret key is still just the alias (not the actual private key). The private key
@@ -2731,109 +2689,9 @@
                 // profile.
                 startVpnProfilePrivileged(profile, VpnConfig.LEGACY_VPN);
                 return;
-            case VpnProfile.TYPE_L2TP_IPSEC_PSK:
-                racoon = new String[] {
-                    iface, profile.server, "udppsk", profile.ipsecIdentifier,
-                    profile.ipsecSecret, "1701",
-                };
-                break;
-            case VpnProfile.TYPE_L2TP_IPSEC_RSA:
-                racoon = new String[] {
-                    iface, profile.server, "udprsa", makeKeystoreEngineGrantString(privateKey),
-                    userCert, caCert, serverCert, "1701",
-                };
-                break;
-            case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
-                racoon = new String[] {
-                    iface, profile.server, "xauthpsk", profile.ipsecIdentifier,
-                    profile.ipsecSecret, profile.username, profile.password, "", gateway,
-                };
-                break;
-            case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
-                racoon = new String[] {
-                    iface, profile.server, "xauthrsa", makeKeystoreEngineGrantString(privateKey),
-                    userCert, caCert, serverCert, profile.username, profile.password, "", gateway,
-                };
-                break;
-            case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
-                racoon = new String[] {
-                    iface, profile.server, "hybridrsa",
-                    caCert, serverCert, profile.username, profile.password, "", gateway,
-                };
-                break;
         }
 
-        // Prepare arguments for mtpd. MTU/MRU calculated conservatively. Only IPv4 supported
-        // because LegacyVpn.
-        // 1500 - 60 (Carrier-internal IPv6 + UDP + GTP) - 10 (PPP) - 16 (L2TP) - 8 (UDP)
-        //   - 77 (IPsec w/ SHA-2 512, 256b trunc-len, AES-CBC) - 8 (UDP encap) - 20 (IPv4)
-        //   - 28 (464xlat)
-        String[] mtpd = null;
-        switch (profile.type) {
-            case VpnProfile.TYPE_PPTP:
-                mtpd = new String[] {
-                    iface, "pptp", profile.server, "1723",
-                    "name", profile.username, "password", profile.password,
-                    "linkname", "vpn", "refuse-eap", "nodefaultroute",
-                    "usepeerdns", "idle", "1800", "mtu", "1270", "mru", "1270",
-                    (profile.mppe ? "+mppe" : "nomppe"),
-                };
-                if (profile.mppe) {
-                    // Disallow PAP authentication when MPPE is requested, as MPPE cannot work
-                    // with PAP anyway, and users may not expect PAP (plain text) to be used when
-                    // MPPE was requested.
-                    mtpd = Arrays.copyOf(mtpd, mtpd.length + 1);
-                    mtpd[mtpd.length - 1] = "-pap";
-                }
-                break;
-            case VpnProfile.TYPE_L2TP_IPSEC_PSK:
-            case VpnProfile.TYPE_L2TP_IPSEC_RSA:
-                mtpd = new String[] {
-                    iface, "l2tp", profile.server, "1701", profile.l2tpSecret,
-                    "name", profile.username, "password", profile.password,
-                    "linkname", "vpn", "refuse-eap", "nodefaultroute",
-                    "usepeerdns", "idle", "1800", "mtu", "1270", "mru", "1270",
-                };
-                break;
-        }
-
-        VpnConfig config = new VpnConfig();
-        config.legacy = true;
-        config.user = profile.key;
-        config.interfaze = iface;
-        config.session = profile.name;
-        config.isMetered = false;
-        config.proxyInfo = profile.proxy;
-        if (underlying != null) {
-            config.underlyingNetworks = new Network[] { underlying };
-        }
-
-        config.addLegacyRoutes(profile.routes);
-        if (!profile.dnsServers.isEmpty()) {
-            config.dnsServers = Arrays.asList(profile.dnsServers.split(" +"));
-        }
-        if (!profile.searchDomains.isEmpty()) {
-            config.searchDomains = Arrays.asList(profile.searchDomains.split(" +"));
-        }
-        startLegacyVpn(config, racoon, mtpd, profile);
-    }
-
-    private synchronized void startLegacyVpn(VpnConfig config, String[] racoon, String[] mtpd,
-            VpnProfile profile) {
-        stopVpnRunnerPrivileged();
-
-        // Prepare for the new request.
-        prepareInternal(VpnConfig.LEGACY_VPN);
-        updateState(DetailedState.CONNECTING, "startLegacyVpn");
-
-        // Start a new LegacyVpnRunner and we are done!
-        mVpnRunner = new LegacyVpnRunner(config, racoon, mtpd, profile);
-        startLegacyVpnRunner();
-    }
-
-    @VisibleForTesting
-    protected void startLegacyVpnRunner() {
-        mVpnRunner.start();
+        throw new UnsupportedOperationException("Legacy VPN is deprecated");
     }
 
     /**
@@ -2851,17 +2709,7 @@
             return;
         }
 
-        final boolean isLegacyVpn = mVpnRunner instanceof LegacyVpnRunner;
         mVpnRunner.exit();
-
-        // LegacyVpn uses daemons that must be shut down before new ones are brought up.
-        // The same limitation does not apply to Platform VPNs.
-        if (isLegacyVpn) {
-            synchronized (LegacyVpnRunner.TAG) {
-                // wait for old thread to completely finish before spinning up
-                // new instance, otherwise state updates can be out of order.
-            }
-        }
     }
 
     /**
@@ -4143,9 +3991,7 @@
                 // Ignore stale runner.
                 if (mVpnRunner != this) return;
 
-                // TODO(b/230548427): Remove SDK check once VPN related stuff are
-                //  decoupled from ConnectivityServiceTest.
-                if (SdkLevel.isAtLeastT() && category != null && isVpnApp(mPackage)) {
+                if (category != null && isVpnApp(mPackage)) {
                     sendEventToVpnManagerApp(category, errorClass, errorCode,
                             getPackage(), mSessionKey, makeVpnProfileStateLocked(),
                             mActiveNetwork,
@@ -4256,343 +4102,6 @@
         }
     }
 
-    /**
-     * Bringing up a VPN connection takes time, and that is all this thread
-     * does. Here we have plenty of time. The only thing we need to take
-     * care of is responding to interruptions as soon as possible. Otherwise
-     * requests will pile up. This could be done in a Handler as a state
-     * machine, but it is much easier to read in the current form.
-     */
-    private class LegacyVpnRunner extends VpnRunner {
-        private static final String TAG = "LegacyVpnRunner";
-
-        private final String[] mDaemons;
-        private final String[][] mArguments;
-        private final LocalSocket[] mSockets;
-        private final String mOuterInterface;
-        private final AtomicInteger mOuterConnection =
-                new AtomicInteger(ConnectivityManager.TYPE_NONE);
-        private final VpnProfile mProfile;
-
-        private long mBringupStartTime = -1;
-
-        /**
-         * Watch for the outer connection (passing in the constructor) going away.
-         */
-        private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                if (!mEnableTeardown) return;
-
-                if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
-                    if (intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE,
-                            ConnectivityManager.TYPE_NONE) == mOuterConnection.get()) {
-                        NetworkInfo info = (NetworkInfo)intent.getExtra(
-                                ConnectivityManager.EXTRA_NETWORK_INFO);
-                        if (info != null && !info.isConnectedOrConnecting()) {
-                            try {
-                                mObserver.interfaceStatusChanged(mOuterInterface, false);
-                            } catch (RemoteException e) {}
-                        }
-                    }
-                }
-            }
-        };
-
-        // GuardedBy("Vpn.this") (annotation can't be applied to constructor)
-        LegacyVpnRunner(VpnConfig config, String[] racoon, String[] mtpd, VpnProfile profile) {
-            super(TAG);
-            if (racoon == null && mtpd == null) {
-                throw new IllegalArgumentException(
-                        "Arguments to racoon and mtpd must not both be null");
-            }
-            mConfig = config;
-            mDaemons = new String[] {"racoon", "mtpd"};
-            // TODO: clear arguments from memory once launched
-            mArguments = new String[][] {racoon, mtpd};
-            mSockets = new LocalSocket[mDaemons.length];
-
-            // This is the interface which VPN is running on,
-            // mConfig.interfaze will change to point to OUR
-            // internal interface soon. TODO - add inner/outer to mconfig
-            // TODO - we have a race - if the outer iface goes away/disconnects before we hit this
-            // we will leave the VPN up.  We should check that it's still there/connected after
-            // registering
-            mOuterInterface = mConfig.interfaze;
-
-            mProfile = profile;
-
-            if (!TextUtils.isEmpty(mOuterInterface)) {
-                for (Network network : mConnectivityManager.getAllNetworks()) {
-                    final LinkProperties lp = mConnectivityManager.getLinkProperties(network);
-                    if (lp != null && lp.getAllInterfaceNames().contains(mOuterInterface)) {
-                        final NetworkInfo netInfo = mConnectivityManager.getNetworkInfo(network);
-                        if (netInfo != null) {
-                            mOuterConnection.set(netInfo.getType());
-                            break;
-                        }
-                    }
-                }
-            }
-
-            IntentFilter filter = new IntentFilter();
-            filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
-            mContext.registerReceiver(mBroadcastReceiver, filter);
-        }
-
-        /**
-         * Checks if the parameter matches the underlying interface
-         *
-         * <p>If the underlying interface is torn down, the LegacyVpnRunner also should be. It has
-         * no ability to migrate between interfaces (or Networks).
-         */
-        public void exitIfOuterInterfaceIs(String interfaze) {
-            if (interfaze.equals(mOuterInterface)) {
-                Log.i(TAG, "Legacy VPN is going down with " + interfaze);
-                exitVpnRunner();
-            }
-        }
-
-        /** Tears down this LegacyVpn connection */
-        @Override
-        public void exitVpnRunner() {
-            // We assume that everything is reset after stopping the daemons.
-            interrupt();
-
-            // Always disconnect. This may be called again in cleanupVpnStateLocked() if
-            // exitVpnRunner() was called from exit(), but it will be a no-op.
-            agentDisconnect();
-            try {
-                mContext.unregisterReceiver(mBroadcastReceiver);
-            } catch (IllegalArgumentException e) {}
-        }
-
-        @Override
-        public void run() {
-            // Wait for the previous thread since it has been interrupted.
-            Log.v(TAG, "Waiting");
-            synchronized (TAG) {
-                Log.v(TAG, "Executing");
-                try {
-                    bringup();
-                    waitForDaemonsToStop();
-                    interrupted(); // Clear interrupt flag if execute called exit.
-                } catch (InterruptedException e) {
-                } finally {
-                    for (LocalSocket socket : mSockets) {
-                        IoUtils.closeQuietly(socket);
-                    }
-                    // This sleep is necessary for racoon to successfully complete sending delete
-                    // message to server.
-                    try {
-                        Thread.sleep(50);
-                    } catch (InterruptedException e) {
-                    }
-                    for (String daemon : mDaemons) {
-                        mDeps.stopService(daemon);
-                    }
-                }
-                agentDisconnect();
-            }
-        }
-
-        private void checkInterruptAndDelay(boolean sleepLonger) throws InterruptedException {
-            long now = SystemClock.elapsedRealtime();
-            if (now - mBringupStartTime <= 60000) {
-                Thread.sleep(sleepLonger ? 200 : 1);
-            } else {
-                updateState(DetailedState.FAILED, "checkpoint");
-                throw new IllegalStateException("VPN bringup took too long");
-            }
-        }
-
-        private void checkAndFixupArguments(@NonNull final InetAddress endpointAddress) {
-            final String endpointAddressString = endpointAddress.getHostAddress();
-            // Perform some safety checks before inserting the address in place.
-            // Position 0 in mDaemons and mArguments must be racoon, and position 1 must be mtpd.
-            if (!"racoon".equals(mDaemons[0]) || !"mtpd".equals(mDaemons[1])) {
-                throw new IllegalStateException("Unexpected daemons order");
-            }
-
-            // Respectively, the positions at which racoon and mtpd take the server address
-            // argument are 1 and 2. Not all types of VPN require both daemons however, and
-            // in that case the corresponding argument array is null.
-            if (mArguments[0] != null) {
-                if (!mProfile.server.equals(mArguments[0][1])) {
-                    throw new IllegalStateException("Invalid server argument for racoon");
-                }
-                mArguments[0][1] = endpointAddressString;
-            }
-
-            if (mArguments[1] != null) {
-                if (!mProfile.server.equals(mArguments[1][2])) {
-                    throw new IllegalStateException("Invalid server argument for mtpd");
-                }
-                mArguments[1][2] = endpointAddressString;
-            }
-        }
-
-        private void bringup() {
-            // Catch all exceptions so we can clean up a few things.
-            try {
-                // resolve never returns null. If it does because of some bug, it will be
-                // caught by the catch() block below and cleanup gracefully.
-                final InetAddress endpointAddress = mDeps.resolve(mProfile.server);
-
-                // Big hack : dynamically replace the address of the server in the arguments
-                // with the resolved address.
-                checkAndFixupArguments(endpointAddress);
-
-                // Initialize the timer.
-                mBringupStartTime = SystemClock.elapsedRealtime();
-
-                // Wait for the daemons to stop.
-                for (String daemon : mDaemons) {
-                    while (!mDeps.isServiceStopped(daemon)) {
-                        checkInterruptAndDelay(true);
-                    }
-                }
-
-                // Clear the previous state.
-                final File state = mDeps.getStateFile();
-                state.delete();
-                if (state.exists()) {
-                    throw new IllegalStateException("Cannot delete the state");
-                }
-                new File("/data/misc/vpn/abort").delete();
-
-                updateState(DetailedState.CONNECTING, "execute");
-
-                // Start the daemon with arguments.
-                for (int i = 0; i < mDaemons.length; ++i) {
-                    String[] arguments = mArguments[i];
-                    if (arguments == null) {
-                        continue;
-                    }
-
-                    // Start the daemon.
-                    String daemon = mDaemons[i];
-                    mDeps.startService(daemon);
-
-                    // Wait for the daemon to start.
-                    while (!mDeps.isServiceRunning(daemon)) {
-                        checkInterruptAndDelay(true);
-                    }
-
-                    // Create the control socket.
-                    mSockets[i] = new LocalSocket();
-
-                    // Wait for the socket to connect and send over the arguments.
-                    mDeps.sendArgumentsToDaemon(daemon, mSockets[i], arguments,
-                            this::checkInterruptAndDelay);
-                }
-
-                // Wait for the daemons to create the new state.
-                while (!state.exists()) {
-                    // Check if a running daemon is dead.
-                    for (int i = 0; i < mDaemons.length; ++i) {
-                        String daemon = mDaemons[i];
-                        if (mArguments[i] != null && !mDeps.isServiceRunning(daemon)) {
-                            throw new IllegalStateException(daemon + " is dead");
-                        }
-                    }
-                    checkInterruptAndDelay(true);
-                }
-
-                // Now we are connected. Read and parse the new state.
-                String[] parameters = FileUtils.readTextFile(state, 0, null).split("\n", -1);
-                if (parameters.length != 7) {
-                    throw new IllegalStateException("Cannot parse the state: '"
-                            + String.join("', '", parameters) + "'");
-                }
-
-                // Set the interface and the addresses in the config.
-                synchronized (Vpn.this) {
-                    mConfig.interfaze = parameters[0].trim();
-
-                    mConfig.addLegacyAddresses(parameters[1]);
-                    // Set the routes if they are not set in the config.
-                    if (mConfig.routes == null || mConfig.routes.isEmpty()) {
-                        mConfig.addLegacyRoutes(parameters[2]);
-                    }
-
-                    // Set the DNS servers if they are not set in the config.
-                    if (mConfig.dnsServers == null || mConfig.dnsServers.size() == 0) {
-                        String dnsServers = parameters[3].trim();
-                        if (!dnsServers.isEmpty()) {
-                            mConfig.dnsServers = Arrays.asList(dnsServers.split(" "));
-                        }
-                    }
-
-                    // Set the search domains if they are not set in the config.
-                    if (mConfig.searchDomains == null || mConfig.searchDomains.size() == 0) {
-                        String searchDomains = parameters[4].trim();
-                        if (!searchDomains.isEmpty()) {
-                            mConfig.searchDomains = Arrays.asList(searchDomains.split(" "));
-                        }
-                    }
-
-                    // Add a throw route for the VPN server endpoint, if one was specified.
-                    if (endpointAddress instanceof Inet4Address) {
-                        mConfig.routes.add(new RouteInfo(
-                                new IpPrefix(endpointAddress, 32), null /*gateway*/,
-                                null /*iface*/, RTN_THROW));
-                    } else if (endpointAddress instanceof Inet6Address) {
-                        mConfig.routes.add(new RouteInfo(
-                                new IpPrefix(endpointAddress, 128), null /*gateway*/,
-                                null /*iface*/, RTN_THROW));
-                    } else {
-                        Log.e(TAG, "Unknown IP address family for VPN endpoint: "
-                                + endpointAddress);
-                    }
-
-                    // Here is the last step and it must be done synchronously.
-                    // Set the start time
-                    mConfig.startTime = SystemClock.elapsedRealtime();
-
-                    // Check if the thread was interrupted while we were waiting on the lock.
-                    checkInterruptAndDelay(false);
-
-                    // Check if the interface is gone while we are waiting.
-                    if (!mDeps.isInterfacePresent(Vpn.this, mConfig.interfaze)) {
-                        throw new IllegalStateException(mConfig.interfaze + " is gone");
-                    }
-
-                    // Now INetworkManagementEventObserver is watching our back.
-                    mInterface = mConfig.interfaze;
-                    prepareStatusIntent();
-
-                    agentConnect();
-
-                    Log.i(TAG, "Connected!");
-                }
-            } catch (Exception e) {
-                Log.i(TAG, "Aborting", e);
-                updateState(DetailedState.FAILED, e.getMessage());
-                exitVpnRunner();
-            }
-        }
-
-        /**
-         * Check all daemons every two seconds. Return when one of them is stopped.
-         * The caller will move to the disconnected state when this function returns,
-         * which can happen if a daemon failed or if the VPN was torn down.
-         */
-        private void waitForDaemonsToStop() throws InterruptedException {
-            if (!mNetworkInfo.isConnected()) {
-                return;
-            }
-            while (true) {
-                Thread.sleep(2000);
-                for (int i = 0; i < mDaemons.length; i++) {
-                    if (mArguments[i] != null && mDeps.isServiceStopped(mDaemons[i])) {
-                        return;
-                    }
-                }
-            }
-        }
-    }
-
     private void verifyCallingUidAndPackage(String packageName) {
         mDeps.verifyCallingUidAndPackage(mContext, packageName, mUserId);
     }
@@ -4839,11 +4348,9 @@
         // Build intent first because the sessionKey will be reset after performing
         // VpnRunner.exit(). Also, cache mOwnerUID even if ownerUID will not be changed in
         // VpnRunner.exit() to prevent design being changed in the future.
-        // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
-        //  ConnectivityServiceTest.
         final int ownerUid = mOwnerUID;
         Intent intent = null;
-        if (SdkLevel.isAtLeastT() && isVpnApp(mPackage)) {
+        if (isVpnApp(mPackage)) {
             intent = buildVpnManagerEventIntent(
                     VpnManager.CATEGORY_EVENT_DEACTIVATED_BY_USER,
                     -1 /* errorClass */, -1 /* errorCode*/, mPackage,
@@ -4884,12 +4391,8 @@
         // The underlying network, NetworkCapabilities and LinkProperties are not
         // necessary to send to VPN app since the purpose of this event is to notify
         // VPN app that VPN is deactivated by the user.
-        // TODO(b/230548427): Remove SDK check once VPN related stuff are decoupled from
-        //  ConnectivityServiceTest.
-        if (SdkLevel.isAtLeastT()) {
-            mEventChanges.log("[VMEvent] " + packageName + " stopped");
-            sendEventToVpnManagerApp(intent, packageName);
-        }
+        mEventChanges.log("[VMEvent] " + packageName + " stopped");
+        sendEventToVpnManagerApp(intent, packageName);
     }
 
     private boolean storeAppExclusionList(@NonNull String packageName,
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 1bdd402..707e990 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -179,6 +179,8 @@
 import android.app.role.RoleManager;
 import android.app.usage.UsageEvents;
 import android.app.usage.UsageStatsManagerInternal;
+import android.companion.AssociationInfo;
+import android.companion.AssociationRequest;
 import android.companion.ICompanionDeviceManager;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledAfter;
@@ -546,6 +548,15 @@
     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     static final long ENFORCE_NO_CLEAR_FLAG_ON_MEDIA_NOTIFICATION = 264179692L;
 
+    /**
+     * App calls to {@link android.app.NotificationManager#setInterruptionFilter} and
+     * {@link android.app.NotificationManager#setNotificationPolicy} manage DND through the
+     * creation and activation of an implicit {@link android.app.AutomaticZenRule}.
+     */
+    @ChangeId
+    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    static final long MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES = 308670109L;
+
     private static final Duration POST_WAKE_LOCK_TIMEOUT = Duration.ofSeconds(30);
 
     private IActivityManager mAm;
@@ -5343,6 +5354,12 @@
             if (zen == -1) throw new IllegalArgumentException("Invalid filter: " + filter);
             final int callingUid = Binder.getCallingUid();
             final boolean isSystemOrSystemUi = isCallerIsSystemOrSystemUi();
+
+            if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) {
+                mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, callingUid, zen);
+                return;
+            }
+
             final long identity = Binder.clearCallingIdentity();
             try {
                 mZenModeHelper.setManualZenMode(zen, null, pkg, "setInterruptionFilter",
@@ -5426,6 +5443,16 @@
             }
         }
 
+        private boolean canManageGlobalZenPolicy(String callingPkg, int callingUid) {
+            boolean isCompatChangeEnabled = Binder.withCleanCallingIdentity(
+                    () -> CompatChanges.isChangeEnabled(MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES,
+                            callingUid));
+            return !isCompatChangeEnabled
+                    || isCallerIsSystemOrSystemUi()
+                    || hasCompanionDevice(callingPkg, UserHandle.getUserId(callingUid),
+                            AssociationRequest.DEVICE_PROFILE_WATCH);
+        }
+
         private void enforcePolicyAccess(String pkg, String method) {
             if (PackageManager.PERMISSION_GRANTED == getContext().checkCallingPermission(
                     android.Manifest.permission.MANAGE_NOTIFICATIONS)) {
@@ -5619,6 +5646,10 @@
 
         @Override
         public Policy getNotificationPolicy(String pkg) {
+            final int callingUid = Binder.getCallingUid();
+            if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) {
+                return mZenModeHelper.getNotificationPolicyFromImplicitZenRule(pkg);
+            }
             final long identity = Binder.clearCallingIdentity();
             try {
                 return mZenModeHelper.getNotificationPolicy();
@@ -5649,6 +5680,10 @@
             enforcePolicyAccess(pkg, "setNotificationPolicy");
             int callingUid = Binder.getCallingUid();
             boolean isSystemOrSystemUi = isCallerIsSystemOrSystemUi();
+
+            boolean shouldApplyAsImplicitRule = android.app.Flags.modesApi()
+                    && !canManageGlobalZenPolicy(pkg, callingUid);
+
             final long identity = Binder.clearCallingIdentity();
             try {
                 final ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(pkg,
@@ -5687,16 +5722,21 @@
                 policy = new Policy(policy.priorityCategories,
                         policy.priorityCallSenders, policy.priorityMessageSenders,
                         newVisualEffects, policy.priorityConversationSenders);
-                ZenLog.traceSetNotificationPolicy(pkg, applicationInfo.targetSdkVersion, policy);
-                mZenModeHelper.setNotificationPolicy(policy, callingUid, isSystemOrSystemUi);
+
+                if (shouldApplyAsImplicitRule) {
+                    mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy);
+                } else {
+                    ZenLog.traceSetNotificationPolicy(pkg, applicationInfo.targetSdkVersion,
+                            policy);
+                    mZenModeHelper.setNotificationPolicy(policy, callingUid, isSystemOrSystemUi);
+                }
             } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to set notification policy", e);
             } finally {
                 Binder.restoreCallingIdentity(identity);
             }
         }
 
-
-
         @Override
         public List<String> getEnabledNotificationListenerPackages() {
             checkCallerIsSystem();
@@ -10556,6 +10596,12 @@
     }
 
     boolean hasCompanionDevice(ManagedServiceInfo info) {
+        return hasCompanionDevice(info.component.getPackageName(),
+                info.userid, /* withDeviceProfile= */ null);
+    }
+
+    private boolean hasCompanionDevice(String pkg, @UserIdInt int userId,
+            @Nullable @AssociationRequest.DeviceProfile String withDeviceProfile) {
         if (mCompanionManager == null) {
             mCompanionManager = getCompanionManager();
         }
@@ -10565,17 +10611,19 @@
         }
         final long identity = Binder.clearCallingIdentity();
         try {
-            List<?> associations = mCompanionManager.getAssociations(
-                    info.component.getPackageName(), info.userid);
-            if (!ArrayUtils.isEmpty(associations)) {
-                return true;
+            List<AssociationInfo> associations = mCompanionManager.getAssociations(pkg, userId);
+            for (AssociationInfo association : associations) {
+                if (withDeviceProfile == null || withDeviceProfile.equals(
+                        association.getDeviceProfile())) {
+                    return true;
+                }
             }
         } catch (SecurityException se) {
             // Not a privileged listener
         } catch (RemoteException re) {
             Slog.e(TAG, "Cannot reach companion device service", re);
         } catch (Exception e) {
-            Slog.e(TAG, "Cannot verify listener " + info, e);
+            Slog.e(TAG, "Cannot verify caller pkg=" + pkg + ", userId=" + userId, e);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
diff --git a/services/core/java/com/android/server/notification/SnoozeHelper.java b/services/core/java/com/android/server/notification/SnoozeHelper.java
index 8f5676b3..9106c33 100644
--- a/services/core/java/com/android/server/notification/SnoozeHelper.java
+++ b/services/core/java/com/android/server/notification/SnoozeHelper.java
@@ -118,7 +118,10 @@
 
     protected boolean canSnooze(int numberToSnooze) {
         synchronized (mLock) {
-            if ((mSnoozedNotifications.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT) {
+            if ((mSnoozedNotifications.size() + numberToSnooze) > CONCURRENT_SNOOZE_LIMIT
+                    || (mPersistedSnoozedNotifications.size()
+                    + mPersistedSnoozedNotificationsWithContext.size() + numberToSnooze)
+                    > CONCURRENT_SNOOZE_LIMIT) {
                 return false;
             }
         }
@@ -357,6 +360,9 @@
 
             if (groupSummaryKey != null) {
                 NotificationRecord record = mSnoozedNotifications.remove(groupSummaryKey);
+                String trimmedKey = getTrimmedString(groupSummaryKey);
+                mPersistedSnoozedNotificationsWithContext.remove(trimmedKey);
+                mPersistedSnoozedNotifications.remove(trimmedKey);
 
                 if (record != null && !record.isCanceled) {
                     Runnable runnable = () -> {
diff --git a/services/core/java/com/android/server/notification/ZenAdapters.java b/services/core/java/com/android/server/notification/ZenAdapters.java
new file mode 100644
index 0000000..2a65aff
--- /dev/null
+++ b/services/core/java/com/android/server/notification/ZenAdapters.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import android.app.NotificationManager.Policy;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenPolicy;
+
+/**
+ * Converters between different Zen representations.
+ */
+class ZenAdapters {
+
+    static ZenPolicy notificationPolicyToZenPolicy(Policy policy) {
+        ZenPolicy.Builder zenPolicyBuilder = new ZenPolicy.Builder()
+                .allowAlarms(policy.allowAlarms())
+                .allowCalls(
+                        policy.allowCalls()
+                                ? ZenModeConfig.getZenPolicySenders(policy.allowCallsFrom())
+                                : ZenPolicy.PEOPLE_TYPE_NONE)
+                .allowConversations(
+                        policy.allowConversations()
+                                ? notificationPolicyConversationSendersToZenPolicy(
+                                        policy.allowConversationsFrom())
+                                : ZenPolicy.CONVERSATION_SENDERS_NONE)
+                .allowEvents(policy.allowEvents())
+                .allowMedia(policy.allowMedia())
+                .allowMessages(
+                        policy.allowMessages()
+                                ? ZenModeConfig.getZenPolicySenders(policy.allowMessagesFrom())
+                                : ZenPolicy.PEOPLE_TYPE_NONE)
+                .allowReminders(policy.allowReminders())
+                .allowRepeatCallers(policy.allowRepeatCallers())
+                .allowSystem(policy.allowSystem());
+
+        if (policy.suppressedVisualEffects != Policy.SUPPRESSED_EFFECTS_UNSET) {
+            zenPolicyBuilder.showBadges(policy.showBadges())
+                    .showFullScreenIntent(policy.showFullScreenIntents())
+                    .showInAmbientDisplay(policy.showAmbient())
+                    .showInNotificationList(policy.showInNotificationList())
+                    .showLights(policy.showLights())
+                    .showPeeking(policy.showPeeking())
+                    .showStatusBarIcons(policy.showStatusBarIcons());
+        }
+
+        return zenPolicyBuilder.build();
+    }
+
+    @ZenPolicy.ConversationSenders
+    private static int notificationPolicyConversationSendersToZenPolicy(
+            int npPriorityConversationSenders) {
+        switch (npPriorityConversationSenders) {
+            case Policy.CONVERSATION_SENDERS_ANYONE:
+                return ZenPolicy.CONVERSATION_SENDERS_ANYONE;
+            case Policy.CONVERSATION_SENDERS_IMPORTANT:
+                return ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
+            case Policy.CONVERSATION_SENDERS_NONE:
+                return ZenPolicy.CONVERSATION_SENDERS_NONE;
+            case Policy.CONVERSATION_SENDERS_UNSET:
+            default:
+                return ZenPolicy.CONVERSATION_SENDERS_UNSET;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 762c1a1..c637df2 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -27,6 +27,7 @@
 
 import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
 
+import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
 import android.app.AppOpsManager;
@@ -44,6 +45,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageItemInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -349,11 +351,11 @@
         ZenRule rule;
         synchronized (mConfigLock) {
             if (mConfig == null) return null;
-             rule = mConfig.automaticRules.get(id);
+            rule = mConfig.automaticRules.get(id);
         }
         if (rule == null) return null;
         if (canManageAutomaticZenRule(rule)) {
-             return createAutomaticZenRule(rule);
+            return zenRuleToAutomaticZenRule(rule);
         }
         return null;
     }
@@ -439,6 +441,167 @@
         }
     }
 
+    /**
+     * Create (or activate, or deactivate) an "implicit" {@link ZenRule} when an app that has
+     * Notification Policy Access but is not allowed to manage the global zen state
+     * calls {@link NotificationManager#setInterruptionFilter}.
+     *
+     * <p>When the {@code zenMode} is {@link Global#ZEN_MODE_OFF}, an existing implicit rule will be
+     * deactivated (if there is no implicit rule, the call will be ignored). For other modes, the
+     * rule's interruption filter will match the supplied {@code zenMode}. The policy of the last
+     * call to {@link NotificationManager#setNotificationPolicy} will be used (or, if never called,
+     * the global policy).
+     *
+     * <p>The created rule is owned by the calling package, but it has neither a
+     * {@link ConditionProviderService} nor an associated
+     * {@link AutomaticZenRule#configurationActivity}.
+     *
+     * @param zenMode one of the {@code Global#ZEN_MODE_x} values
+     */
+    void applyGlobalZenModeAsImplicitZenRule(String callingPkg, int callingUid, int zenMode) {
+        if (!android.app.Flags.modesApi()) {
+            Log.wtf(TAG, "applyGlobalZenModeAsImplicitZenRule called with flag off!");
+            return;
+        }
+        synchronized (mConfigLock) {
+            if (mConfig == null) {
+                return;
+            }
+            if (zenMode == Global.ZEN_MODE_OFF) {
+                // Deactivate implicit rule if it exists and is active; otherwise ignore.
+                ZenRule rule = mConfig.automaticRules.get(implicitRuleId(callingPkg));
+                if (rule != null) {
+                    Condition deactivated = new Condition(rule.conditionId,
+                            mContext.getString(R.string.zen_mode_implicit_deactivated),
+                            Condition.STATE_FALSE);
+                    setAutomaticZenRuleState(rule.id, deactivated,
+                            callingUid, /* fromSystemOrSystemUi= */ false);
+                }
+            } else {
+                // Either create a new rule with a default ZenPolicy, or update an existing rule's
+                // filter value. In both cases, also activate (and unsnooze) it.
+                ZenModeConfig newConfig = mConfig.copy();
+                ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg));
+                if (rule == null) {
+                    rule = newImplicitZenRule(callingPkg);
+                    newConfig.automaticRules.put(rule.id, rule);
+                }
+                rule.zenMode = zenMode;
+                rule.snoozing = false;
+                rule.condition = new Condition(rule.conditionId,
+                        mContext.getString(R.string.zen_mode_implicit_activated),
+                        Condition.STATE_TRUE);
+                setConfigLocked(newConfig, /* triggeringComponent= */ null,
+                        "applyGlobalZenModeAsImplicitZenRule",
+                        callingUid, /* fromSystemOrSystemUi= */ false);
+            }
+        }
+    }
+
+    /**
+     * Create (or update) an "implicit" {@link ZenRule} when an app that has Notification Policy
+     * Access but is not allowed to manage the global zen state calls
+     * {@link NotificationManager#setNotificationPolicy}.
+     *
+     * <p>The created rule is owned by the calling package and has the {@link ZenPolicy}
+     * corresponding to the supplied {@code policy}, but it has neither a
+     * {@link ConditionProviderService} nor an associated
+     * {@link AutomaticZenRule#configurationActivity}. Its zen mode will be set to
+     * {@link Global#ZEN_MODE_IMPORTANT_INTERRUPTIONS}.
+     */
+    void applyGlobalPolicyAsImplicitZenRule(String callingPkg, int callingUid,
+            NotificationManager.Policy policy) {
+        if (!android.app.Flags.modesApi()) {
+            Log.wtf(TAG, "applyGlobalPolicyAsImplicitZenRule called with flag off!");
+            return;
+        }
+        synchronized (mConfigLock) {
+            if (mConfig == null) {
+                return;
+            }
+            ZenModeConfig newConfig = mConfig.copy();
+            ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg));
+            if (rule == null) {
+                rule = newImplicitZenRule(callingPkg);
+                rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+                newConfig.automaticRules.put(rule.id, rule);
+            }
+            // TODO: b/308673679 - Keep user customization of this rule!
+            rule.zenPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy);
+            setConfigLocked(newConfig, /* triggeringComponent= */ null,
+                    "applyGlobalPolicyAsImplicitZenRule",
+                    callingUid, /* fromSystemOrSystemUi= */ false);
+        }
+    }
+
+    /**
+     * Returns the {@link Policy} associated to the "implicit" {@link ZenRule} of a package that has
+     * Notification Policy Access but is not allowed to manage the global zen state.
+     *
+     * <p>If the implicit rule doesn't exist, or it doesn't specify a {@link ZenPolicy} (because the
+     * app never called {@link NotificationManager#setNotificationPolicy}) then the default policy
+     * is returned (i.e. same as {@link #getNotificationPolicy}.
+     *
+     * <p>Any unset values in the {@link ZenPolicy} will be mapped to their current defaults.
+     */
+    @Nullable
+    Policy getNotificationPolicyFromImplicitZenRule(String callingPkg) {
+        if (!android.app.Flags.modesApi()) {
+            Log.wtf(TAG, "getNotificationPolicyFromImplicitZenRule called with flag off!");
+            return getNotificationPolicy();
+        }
+        synchronized (mConfigLock) {
+            if (mConfig == null) {
+                return null;
+            }
+            ZenRule implicitRule = mConfig.automaticRules.get(implicitRuleId(callingPkg));
+            if (implicitRule != null && implicitRule.zenPolicy != null) {
+                return mConfig.toNotificationPolicy(implicitRule.zenPolicy);
+            } else {
+                return getNotificationPolicy();
+            }
+        }
+    }
+
+    /**
+     * Creates an empty {@link ZenRule} to be used as the implicit rule for {@code pkg}.
+     * Both {@link ZenRule#zenMode} and {@link ZenRule#zenPolicy} are unset.
+     */
+    private ZenRule newImplicitZenRule(String pkg) {
+        ZenRule rule = new ZenRule();
+        rule.id = implicitRuleId(pkg);
+        rule.pkg = pkg;
+        rule.creationTime = System.currentTimeMillis();
+
+        Binder.withCleanCallingIdentity(() -> {
+            try {
+                ApplicationInfo applicationInfo = mPm.getApplicationInfo(pkg, 0);
+                rule.name = applicationInfo.loadLabel(mPm).toString();
+            } catch (PackageManager.NameNotFoundException e) {
+                // Should not happen, since it's the app calling us (?)
+                Log.w(TAG, "Package not found for creating implicit zen rule");
+                rule.name = "Unknown";
+            }
+        });
+
+        rule.condition = null;
+        rule.conditionId = new Uri.Builder()
+                .scheme(Condition.SCHEME)
+                .authority("android")
+                .appendPath("implicit")
+                .appendPath(pkg)
+                .build();
+        rule.enabled = true;
+        rule.modified = false;
+        rule.component = null;
+        rule.configurationActivity = null;
+        return rule;
+    }
+
+    private static String implicitRuleId(String forPackage) {
+        return "implicit_" + forPackage;
+    }
+
     public boolean removeAutomaticZenRule(String id, String reason, int callingUid,
             boolean fromSystemOrSystemUi) {
         ZenModeConfig newConfig;
@@ -626,7 +789,7 @@
                         }
                         // update default rule (if locale changed, name of rule will change)
                         currRule.name = defaultRule.name;
-                        updateAutomaticZenRule(defaultRule.id, createAutomaticZenRule(currRule),
+                        updateAutomaticZenRule(defaultRule.id, zenRuleToAutomaticZenRule(currRule),
                                 "locale changed", callingUid, fromSystemOrSystemUi);
                     }
                 }
@@ -669,7 +832,7 @@
         return null;
     }
 
-    private void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule,
+    private static void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule,
             boolean isNew) {
         if (rule.enabled != automaticZenRule.isEnabled()) {
             rule.snoozing = false;
@@ -699,7 +862,7 @@
         }
     }
 
-    protected AutomaticZenRule createAutomaticZenRule(ZenRule rule) {
+    private static AutomaticZenRule zenRuleToAutomaticZenRule(ZenRule rule) {
         AutomaticZenRule azr;
         if (Flags.modesApi()) {
             azr = new AutomaticZenRule.Builder(rule.name, rule.conditionId)
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 25b7ca1..dcac8c9 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -7,8 +7,6 @@
   bug: "290381858"
 }
 
-
-
 flag {
   name: "polite_notifications"
   namespace: "systemui"
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index a778415..c9703db 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -4648,7 +4648,7 @@
                                 // Expanding pip into new rotation, so create a rotation leash
                                 // until the display is rotated.
                                 topActivity.getOrCreateFixedRotationLeash(
-                                        topActivity.getSyncTransaction());
+                                        topActivity.getPendingTransaction());
                             }
                             lastParentBeforePip.moveToFront("movePinnedActivityToOriginalTask");
                         }
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index c0bf2ce..3e23fab 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1052,12 +1052,12 @@
      * @return true if we are *guaranteed* to enter-pip. This means we return false if there's
      *         a chance we won't thus legacy-entry (via pause+userLeaving) will return false.
      */
-    private boolean checkEnterPipOnFinish(@NonNull ActivityRecord ar,
-            @Nullable ActivityRecord resuming) {
+    private boolean checkEnterPipOnFinish(@NonNull ActivityRecord ar) {
         if (!mCanPipOnFinish || !ar.isVisible() || ar.getTask() == null || !ar.isState(RESUMED)) {
             return false;
         }
 
+        final ActivityRecord resuming = getVisibleTransientLaunch(ar.getTaskDisplayArea());
         if (ar.pictureInPictureArgs != null && ar.pictureInPictureArgs.isAutoEnterEnabled()) {
             if (!ar.getTask().isVisibleRequested() || didCommitTransientLaunch()) {
                 // force enable pip-on-task-switch now that we've committed to actually launching
@@ -1196,9 +1196,7 @@
                 final boolean isScreenOff = ar.mDisplayContent == null
                         || ar.mDisplayContent.getDisplayInfo().state == Display.STATE_OFF;
                 if ((!visibleAtTransitionEnd || isScreenOff) && !ar.isVisibleRequested()) {
-                    final ActivityRecord resuming = getVisibleTransientLaunch(
-                            ar.getTaskDisplayArea());
-                    final boolean commitVisibility = !checkEnterPipOnFinish(ar, resuming);
+                    final boolean commitVisibility = !checkEnterPipOnFinish(ar);
                     // Avoid commit visibility if entering pip or else we will get a sudden
                     // "flash" / surface going invisible for a split second.
                     if (commitVisibility) {
@@ -1431,7 +1429,7 @@
             if (candidateActivity.getTaskDisplayArea() != taskDisplayArea) {
                 continue;
             }
-            if (!candidateActivity.isVisible()) {
+            if (!candidateActivity.isVisibleRequested()) {
                 continue;
             }
             return candidateActivity;
diff --git a/services/core/java/com/android/server/wm/WindowManagerFlags.java b/services/core/java/com/android/server/wm/WindowManagerFlags.java
index 4667710..b3a3650 100644
--- a/services/core/java/com/android/server/wm/WindowManagerFlags.java
+++ b/services/core/java/com/android/server/wm/WindowManagerFlags.java
@@ -49,5 +49,8 @@
 
     final boolean mWallpaperOffsetAsync = Flags.wallpaperOffsetAsync();
 
+    final boolean mAllowsScreenSizeDecoupledFromStatusBarAndCutout =
+            Flags.allowsScreenSizeDecoupledFromStatusBarAndCutout();
+
     /* End Available Flags */
 }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 55678c5..0a986c8 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -336,7 +336,6 @@
 import com.android.server.policy.WindowManagerPolicy.ScreenOffListener;
 import com.android.server.power.ShutdownThread;
 import com.android.server.utils.PriorityDump;
-import com.android.window.flags.Flags;
 
 import dalvik.annotation.optimization.NeverCompile;
 
@@ -1194,7 +1193,7 @@
                 .getBoolean(R.bool.config_skipActivityRelaunchWhenDocking);
         final boolean isScreenSizeDecoupledFromStatusBarAndCutout = context.getResources()
                 .getBoolean(R.bool.config_decoupleStatusBarAndDisplayCutoutFromScreenSize)
-                && Flags.closeToSquareConfigIncludesStatusBar();
+                && mFlags.mAllowsScreenSizeDecoupledFromStatusBarAndCutout;
         if (!isScreenSizeDecoupledFromStatusBarAndCutout) {
             mDecorTypes = WindowInsets.Type.displayCutout() | WindowInsets.Type.navigationBars();
             mConfigTypes = WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars();
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
new file mode 100644
index 0000000..72dc725
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+
+import android.annotation.NonNull;
+import android.app.usage.UsageStatsManagerInternal;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.ResolveInfo;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.TestLooperManager;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.SparseArray;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.AlarmManagerInternal;
+import com.android.server.DropBoxManagerInternal;
+import com.android.server.LocalServices;
+import com.android.server.appop.AppOpsService;
+import com.android.server.wm.ActivityTaskManagerService;
+
+import org.junit.Rule;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public abstract class BaseBroadcastQueueTest {
+
+    static final int USER_GUEST = 11;
+
+    static final String PACKAGE_ANDROID = "android";
+    static final String PACKAGE_PHONE = "com.android.phone";
+    static final String PACKAGE_RED = "com.example.red";
+    static final String PACKAGE_GREEN = "com.example.green";
+    static final String PACKAGE_BLUE = "com.example.blue";
+    static final String PACKAGE_YELLOW = "com.example.yellow";
+    static final String PACKAGE_ORANGE = "com.example.orange";
+
+    static final String PROCESS_SYSTEM = "system";
+
+    static final String CLASS_RED = "com.example.red.Red";
+    static final String CLASS_GREEN = "com.example.green.Green";
+    static final String CLASS_BLUE = "com.example.blue.Blue";
+    static final String CLASS_YELLOW = "com.example.yellow.Yellow";
+    static final String CLASS_ORANGE = "com.example.orange.Orange";
+
+    static final BroadcastProcessQueue.BroadcastPredicate BROADCAST_PREDICATE_ANY =
+            (r, i) -> true;
+
+    @Rule
+    public final ApplicationExitInfoTest.ServiceThreadRule
+            mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule();
+
+    @Mock
+    AppOpsService mAppOpsService;
+    @Mock
+    PackageManagerInternal mPackageManagerInt;
+    @Mock
+    UsageStatsManagerInternal mUsageStatsManagerInt;
+    @Mock
+    DropBoxManagerInternal mDropBoxManagerInt;
+    @Mock
+    AlarmManagerInternal mAlarmManagerInt;
+    @Mock
+    ProcessList mProcessList;
+
+    Context mContext;
+    ActivityManagerService mAms;
+    BroadcastConstants mConstants;
+    BroadcastSkipPolicy mSkipPolicy;
+    HandlerThread mHandlerThread;
+    TestLooperManager mLooper;
+    AtomicInteger mNextPid;
+
+    /**
+     * Map from PID to registered registered runtime receivers.
+     */
+    SparseArray<ReceiverList> mRegisteredReceivers = new SparseArray<>();
+
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        mHandlerThread = new HandlerThread(getTag());
+        mHandlerThread.start();
+        // Pause all event processing until a test chooses to resume
+        mLooper = Objects.requireNonNull(InstrumentationRegistry.getInstrumentation()
+                .acquireLooperManager(mHandlerThread.getLooper()));
+        mNextPid = new AtomicInteger(100);
+
+        LocalServices.removeServiceForTest(DropBoxManagerInternal.class);
+        LocalServices.addService(DropBoxManagerInternal.class, mDropBoxManagerInt);
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt);
+        LocalServices.removeServiceForTest(AlarmManagerInternal.class);
+        LocalServices.addService(AlarmManagerInternal.class, mAlarmManagerInt);
+        doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent();
+        doNothing().when(mPackageManagerInt).notifyComponentUsed(any(), anyInt(), any(), any());
+        doAnswer((invocation) -> {
+            return getUidForPackage(invocation.getArgument(0));
+        }).when(mPackageManagerInt).getPackageUid(any(), anyLong(), eq(UserHandle.USER_SYSTEM));
+
+        final ActivityManagerService realAms = new ActivityManagerService(
+                new TestInjector(mContext), mServiceThreadRule.getThread());
+        realAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
+        realAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
+        realAms.mAtmInternal = spy(realAms.mActivityTaskManager.getAtmInternal());
+        realAms.mOomAdjuster = spy(realAms.mOomAdjuster);
+        realAms.mPackageManagerInt = mPackageManagerInt;
+        realAms.mUsageStatsService = mUsageStatsManagerInt;
+        realAms.mProcessesReady = true;
+        mAms = spy(realAms);
+
+        mSkipPolicy = spy(new BroadcastSkipPolicy(mAms));
+        doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any());
+        doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any());
+
+        mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
+    }
+
+    public void tearDown() throws Exception {
+        mHandlerThread.quit();
+    }
+
+    static int getUidForPackage(@NonNull String packageName) {
+        switch (packageName) {
+            case PACKAGE_ANDROID: return android.os.Process.SYSTEM_UID;
+            case PACKAGE_PHONE: return android.os.Process.PHONE_UID;
+            case PACKAGE_RED: return android.os.Process.FIRST_APPLICATION_UID + 1;
+            case PACKAGE_GREEN: return android.os.Process.FIRST_APPLICATION_UID + 2;
+            case PACKAGE_BLUE: return android.os.Process.FIRST_APPLICATION_UID + 3;
+            case PACKAGE_YELLOW: return android.os.Process.FIRST_APPLICATION_UID + 4;
+            case PACKAGE_ORANGE: return android.os.Process.FIRST_APPLICATION_UID + 5;
+            default: throw new IllegalArgumentException();
+        }
+    }
+
+    static int getUidForPackage(@NonNull String packageName, int userId) {
+        return UserHandle.getUid(userId, getUidForPackage(packageName));
+    }
+
+    private class TestInjector extends ActivityManagerService.Injector {
+        TestInjector(Context context) {
+            super(context);
+        }
+
+        @Override
+        public AppOpsService getAppOpsService(File recentAccessesFile, File storageFile,
+                                              Handler handler) {
+            return mAppOpsService;
+        }
+
+        @Override
+        public Handler getUiHandler(ActivityManagerService service) {
+            return mHandlerThread.getThreadHandler();
+        }
+
+        @Override
+        public ProcessList getProcessList(ActivityManagerService service) {
+            return mProcessList;
+        }
+    }
+
+    abstract String getTag();
+
+    static ApplicationInfo makeApplicationInfo(String packageName) {
+        return makeApplicationInfo(packageName, packageName, UserHandle.USER_SYSTEM);
+    }
+
+    static ApplicationInfo makeApplicationInfo(String packageName, String processName, int userId) {
+        final ApplicationInfo ai = new ApplicationInfo();
+        ai.packageName = packageName;
+        ai.processName = processName;
+        ai.uid = getUidForPackage(packageName, userId);
+        return ai;
+    }
+
+    static ResolveInfo withPriority(ResolveInfo info, int priority) {
+        info.priority = priority;
+        return info;
+    }
+
+    static BroadcastFilter withPriority(BroadcastFilter filter, int priority) {
+        filter.setPriority(priority);
+        return filter;
+    }
+
+    static ResolveInfo makeManifestReceiver(String packageName, String name) {
+        return makeManifestReceiver(packageName, name, UserHandle.USER_SYSTEM);
+    }
+
+    static ResolveInfo makeManifestReceiver(String packageName, String name, int userId) {
+        return makeManifestReceiver(packageName, packageName, name, userId);
+    }
+
+    static ResolveInfo makeManifestReceiver(String packageName, String processName,
+            String name, int userId) {
+        final ResolveInfo ri = new ResolveInfo();
+        ri.activityInfo = new ActivityInfo();
+        ri.activityInfo.packageName = packageName;
+        ri.activityInfo.processName = processName;
+        ri.activityInfo.name = name;
+        ri.activityInfo.applicationInfo = makeApplicationInfo(packageName, processName, userId);
+        return ri;
+    }
+
+    BroadcastFilter makeRegisteredReceiver(ProcessRecord app) {
+        return makeRegisteredReceiver(app, 0);
+    }
+
+    BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) {
+        final ReceiverList receiverList = mRegisteredReceivers.get(app.getPid());
+        return makeRegisteredReceiver(receiverList, priority);
+    }
+
+    static BroadcastFilter makeRegisteredReceiver(ReceiverList receiverList, int priority) {
+        final IntentFilter filter = new IntentFilter();
+        filter.setPriority(priority);
+        final BroadcastFilter res = new BroadcastFilter(filter, receiverList,
+                receiverList.app.info.packageName, null, null, null, receiverList.uid,
+                receiverList.userId, false, false, true);
+        receiverList.add(res);
+        return res;
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index 08f5d03..2378416 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -31,17 +31,6 @@
 import static com.android.server.am.BroadcastProcessQueue.REASON_CONTAINS_PRIORITIZED;
 import static com.android.server.am.BroadcastProcessQueue.insertIntoRunnableList;
 import static com.android.server.am.BroadcastProcessQueue.removeFromRunnableList;
-import static com.android.server.am.BroadcastQueueTest.CLASS_BLUE;
-import static com.android.server.am.BroadcastQueueTest.CLASS_GREEN;
-import static com.android.server.am.BroadcastQueueTest.CLASS_RED;
-import static com.android.server.am.BroadcastQueueTest.CLASS_YELLOW;
-import static com.android.server.am.BroadcastQueueTest.PACKAGE_BLUE;
-import static com.android.server.am.BroadcastQueueTest.PACKAGE_GREEN;
-import static com.android.server.am.BroadcastQueueTest.PACKAGE_RED;
-import static com.android.server.am.BroadcastQueueTest.PACKAGE_YELLOW;
-import static com.android.server.am.BroadcastQueueTest.getUidForPackage;
-import static com.android.server.am.BroadcastQueueTest.makeManifestReceiver;
-import static com.android.server.am.BroadcastQueueTest.withPriority;
 import static com.android.server.am.BroadcastRecord.isReceiverEquals;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -74,17 +63,15 @@
 import android.content.IIntentReceiver;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.ResolveInfo;
 import android.media.AudioManager;
 import android.os.Bundle;
 import android.os.BundleMerger;
 import android.os.DropBoxManager;
-import android.os.HandlerThread;
 import android.os.Process;
 import android.os.SystemClock;
-import android.os.TestLooperManager;
 import android.os.UserHandle;
-import android.provider.Settings;
 import android.util.IndentingPrintWriter;
 import android.util.Pair;
 
@@ -108,11 +95,12 @@
 import java.util.Objects;
 
 @SmallTest
-public final class BroadcastQueueModernImplTest {
+public final class BroadcastQueueModernImplTest extends BaseBroadcastQueueTest {
+    private static final String TAG = "BroadcastQueueModernImplTest";
+
     private static final int TEST_UID = android.os.Process.FIRST_APPLICATION_UID;
     private static final int TEST_UID2 = android.os.Process.FIRST_APPLICATION_UID + 1;
 
-    @Mock ActivityManagerService mAms;
     @Mock ProcessRecord mProcess;
 
     @Mock BroadcastProcessQueue mQueue1;
@@ -120,11 +108,6 @@
     @Mock BroadcastProcessQueue mQueue3;
     @Mock BroadcastProcessQueue mQueue4;
 
-    HandlerThread mHandlerThread;
-    TestLooperManager mLooper;
-
-    BroadcastConstants mConstants;
-    private BroadcastSkipPolicy mSkipPolicy;
     BroadcastQueueModernImpl mImpl;
 
     BroadcastProcessQueue mHead;
@@ -136,22 +119,12 @@
 
     @Before
     public void setUp() throws Exception {
-        mHandlerThread = new HandlerThread(getClass().getSimpleName());
-        mHandlerThread.start();
+        super.setUp();
 
-        // Pause all event processing until a test chooses to resume
-        mLooper = Objects.requireNonNull(InstrumentationRegistry.getInstrumentation()
-                .acquireLooperManager(mHandlerThread.getLooper()));
-
-        mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
         mConstants.DELAY_URGENT_MILLIS = -120_000;
         mConstants.DELAY_NORMAL_MILLIS = 10_000;
         mConstants.DELAY_CACHED_MILLIS = 120_000;
 
-        mSkipPolicy = spy(new BroadcastSkipPolicy(mAms));
-        doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any());
-        doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any());
-
         final BroadcastHistory emptyHistory = new BroadcastHistory(mConstants) {
             public void addBroadcastToHistoryLocked(BroadcastRecord original) {
                 // Ignored
@@ -169,7 +142,12 @@
 
     @After
     public void tearDown() throws Exception {
-        mHandlerThread.quit();
+        super.tearDown();
+    }
+
+    @Override
+    public String getTag() {
+        return TAG;
     }
 
     /**
@@ -225,11 +203,6 @@
                 List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), false);
     }
 
-    private BroadcastRecord makeOrderedBroadcastRecord(Intent intent) {
-        return makeBroadcastRecord(intent, BroadcastOptions.makeBasic(),
-                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), true);
-    }
-
     private BroadcastRecord makeBroadcastRecord(Intent intent, List receivers) {
         return makeBroadcastRecord(intent, BroadcastOptions.makeBasic(), receivers, false);
     }
@@ -246,8 +219,8 @@
 
     private BroadcastRecord makeBroadcastRecord(Intent intent, BroadcastOptions options,
             List receivers, IIntentReceiver resultTo, boolean ordered) {
-        return new BroadcastRecord(mImpl, intent, mProcess, PACKAGE_RED, null, 21, 42, false, null,
-                null, null, null, AppOpsManager.OP_NONE, options, receivers, null, resultTo,
+        return new BroadcastRecord(mImpl, intent, mProcess, PACKAGE_RED, null, 21, TEST_UID, false,
+                null, null, null, null, AppOpsManager.OP_NONE, options, receivers, null, resultTo,
                 Activity.RESULT_OK, null, null, ordered, false, false, UserHandle.USER_SYSTEM,
                 BackgroundStartPrivileges.NONE, false, null, PROCESS_STATE_UNKNOWN);
     }
@@ -259,12 +232,12 @@
 
     private void enqueueOrReplaceBroadcast(BroadcastProcessQueue queue,
             BroadcastRecord record, int recordIndex, long enqueueTime) {
-        queue.enqueueOrReplaceBroadcast(record, recordIndex, (r, i) -> {
-            throw new UnsupportedOperationException();
-        });
         record.enqueueTime = enqueueTime;
         record.enqueueRealTime = enqueueTime;
         record.enqueueClockTime = enqueueTime;
+        queue.enqueueOrReplaceBroadcast(record, recordIndex, (r, i) -> {
+            throw new UnsupportedOperationException();
+        });
     }
 
     @Test
@@ -419,6 +392,7 @@
         assertFalse(queue.isRunnable());
         assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER,
                 queue.getRunnableAtReason());
+        assertTrue(queue.shouldBeDeferred());
         assertEquals(ProcessList.SCHED_GROUP_UNDEFINED, queue.getPreferredSchedulingGroupLocked());
     }
 
@@ -445,6 +419,7 @@
         assertThat(cachedRunnableAt).isGreaterThan(notCachedRunnableAt);
         assertTrue(queue.isRunnable());
         assertEquals(BroadcastProcessQueue.REASON_CACHED, queue.getRunnableAtReason());
+        assertTrue(queue.shouldBeDeferred());
         assertEquals(ProcessList.SCHED_GROUP_UNDEFINED, queue.getPreferredSchedulingGroupLocked());
     }
 
@@ -526,11 +501,13 @@
         queue.invalidateRunnableAt();
         assertThat(queue.getRunnableAt()).isGreaterThan(airplaneRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
 
         mConstants.MAX_PENDING_BROADCASTS = 1;
         queue.invalidateRunnableAt();
         assertThat(queue.getRunnableAt()).isAtMost(airplaneRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_MAX_PENDING, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
     }
 
     @Test
@@ -549,10 +526,12 @@
         queue.setProcessAndUidState(mProcess, true, false);
         assertThat(queue.getRunnableAt()).isLessThan(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_FOREGROUND, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
 
         queue.setProcessAndUidState(mProcess, false, false);
         assertThat(queue.getRunnableAt()).isGreaterThan(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
     }
 
     @Test
@@ -570,6 +549,7 @@
 
         assertThat(queue.getRunnableAt()).isLessThan(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_TOP_PROCESS, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
 
         doReturn(ActivityManager.PROCESS_STATE_SERVICE).when(mProcess).getSetProcState();
         queue.setProcessAndUidState(mProcess, false, false);
@@ -580,6 +560,7 @@
                 List.of(makeMockRegisteredReceiver())), 0);
         assertThat(queue.getRunnableAt()).isGreaterThan(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
     }
 
     @Test
@@ -594,16 +575,19 @@
 
         assertThat(queue.getRunnableAt()).isGreaterThan(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
 
         doReturn(true).when(mProcess).isPersistent();
         queue.setProcessAndUidState(mProcess, false, false);
         assertThat(queue.getRunnableAt()).isLessThan(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_PERSISTENT, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
 
         doReturn(false).when(mProcess).isPersistent();
         queue.setProcessAndUidState(mProcess, false, false);
         assertThat(queue.getRunnableAt()).isGreaterThan(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_NORMAL, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
     }
 
     @Test
@@ -618,6 +602,7 @@
 
         assertThat(queue.getRunnableAt()).isEqualTo(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_CORE_UID, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
     }
 
     @Test
@@ -636,10 +621,12 @@
         assertEquals(Long.MAX_VALUE, queue.getRunnableAt());
         assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER,
                 queue.getRunnableAtReason());
+        assertTrue(queue.shouldBeDeferred());
 
         queue.setProcessAndUidState(mProcess, false, false);
         assertThat(queue.getRunnableAt()).isEqualTo(timeTickRecord.enqueueTime);
         assertEquals(BroadcastProcessQueue.REASON_CORE_UID, queue.getRunnableAtReason());
+        assertFalse(queue.shouldBeDeferred());
     }
 
     /**
@@ -1575,6 +1562,216 @@
         verifyPendingRecords(redQueue, List.of(userPresent, timeTick));
     }
 
+    @Test
+    public void testDeliveryDeferredForCached() throws Exception {
+        final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+        final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED));
+
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+        final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick,
+                List.of(makeRegisteredReceiver(greenProcess, 0)));
+
+        final Intent batteryChanged = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        final BroadcastOptions optionsBatteryChanged =
+                BroadcastOptions.makeWithDeferUntilActive(true);
+        final BroadcastRecord batteryChangedRecord = makeBroadcastRecord(batteryChanged,
+                optionsBatteryChanged,
+                List.of(makeRegisteredReceiver(greenProcess, 10),
+                        makeRegisteredReceiver(redProcess, 0)),
+                false /* ordered */);
+
+        mImpl.enqueueBroadcastLocked(timeTickRecord);
+        mImpl.enqueueBroadcastLocked(batteryChangedRecord);
+
+        final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED,
+                getUidForPackage(PACKAGE_RED));
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason());
+        assertFalse(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_BLOCKED, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // Simulate process state change
+        greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */,
+                true /* processFreezable */);
+        greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply,
+                mImpl.mBroadcastConsumerDeferClear);
+
+        assertEquals(BroadcastProcessQueue.REASON_CACHED, greenQueue.getRunnableAtReason());
+        assertTrue(greenQueue.shouldBeDeferred());
+        // Once the broadcasts to green process are deferred, broadcasts to red process
+        // shouldn't be blocked anymore.
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // All broadcasts to green process should be deferred.
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+
+        final Intent packageChanged = new Intent(Intent.ACTION_PACKAGE_CHANGED);
+        final BroadcastRecord packageChangedRecord = makeBroadcastRecord(packageChanged,
+                List.of(makeRegisteredReceiver(greenProcess, 0)));
+        mImpl.enqueueBroadcastLocked(packageChangedRecord);
+
+        assertEquals(BroadcastProcessQueue.REASON_CACHED, greenQueue.getRunnableAtReason());
+        assertTrue(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // All broadcasts to the green process, including the newly enqueued one, should be
+        // deferred.
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+
+        // Simulate process state change
+        greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */,
+                false /* processFreezable */);
+        greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply,
+                mImpl.mBroadcastConsumerDeferClear);
+
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason());
+        assertFalse(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+    }
+
+    @Test
+    public void testDeliveryDeferredForCached_withInfiniteDeferred() throws Exception {
+        final ProcessRecord greenProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+        final ProcessRecord redProcess = makeProcessRecord(makeApplicationInfo(PACKAGE_RED));
+
+        final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+        final BroadcastOptions optionsTimeTick = BroadcastOptions.makeWithDeferUntilActive(true);
+        final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick, optionsTimeTick,
+                List.of(makeRegisteredReceiver(greenProcess, 0)), false /* ordered */);
+
+        final Intent batteryChanged = new Intent(Intent.ACTION_BATTERY_CHANGED);
+        final BroadcastOptions optionsBatteryChanged =
+                BroadcastOptions.makeWithDeferUntilActive(true);
+        final BroadcastRecord batteryChangedRecord = makeBroadcastRecord(batteryChanged,
+                optionsBatteryChanged,
+                List.of(makeRegisteredReceiver(greenProcess, 10),
+                        makeRegisteredReceiver(redProcess, 0)),
+                false /* ordered */);
+
+        mImpl.enqueueBroadcastLocked(timeTickRecord);
+        mImpl.enqueueBroadcastLocked(batteryChangedRecord);
+
+        final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED,
+                getUidForPackage(PACKAGE_RED));
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason());
+        assertFalse(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_BLOCKED, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // Simulate process state change
+        greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */,
+                true /* processFreezable */);
+        greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply,
+                mImpl.mBroadcastConsumerDeferClear);
+
+        assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER,
+                greenQueue.getRunnableAtReason());
+        assertTrue(greenQueue.shouldBeDeferred());
+        // Once the broadcasts to green process are deferred, broadcasts to red process
+        // shouldn't be blocked anymore.
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // All broadcasts to green process should be deferred.
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+
+        final Intent packageChanged = new Intent(Intent.ACTION_PACKAGE_CHANGED);
+        final BroadcastOptions optionsPackageChanged =
+                BroadcastOptions.makeWithDeferUntilActive(true);
+        final BroadcastRecord packageChangedRecord = makeBroadcastRecord(packageChanged,
+                optionsPackageChanged,
+                List.of(makeRegisteredReceiver(greenProcess, 0)), false /* ordered */);
+        mImpl.enqueueBroadcastLocked(packageChangedRecord);
+
+        assertEquals(BroadcastProcessQueue.REASON_CACHED_INFINITE_DEFER,
+                greenQueue.getRunnableAtReason());
+        assertTrue(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        // All broadcasts to the green process, including the newly enqueued one, should be
+        // deferred.
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_DEFERRED, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+
+        // Simulate process state change
+        greenQueue.setProcessAndUidState(greenProcess, false /* uidForeground */,
+                false /* processFreezable */);
+        greenQueue.updateDeferredStates(mImpl.mBroadcastConsumerDeferApply,
+                mImpl.mBroadcastConsumerDeferClear);
+
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, greenQueue.getRunnableAtReason());
+        assertFalse(greenQueue.shouldBeDeferred());
+        assertEquals(BroadcastProcessQueue.REASON_NORMAL, redQueue.getRunnableAtReason());
+        assertFalse(redQueue.shouldBeDeferred());
+
+        greenQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+        redQueue.forEachMatchingBroadcast(BROADCAST_PREDICATE_ANY, (r, i) -> {
+            assertEquals("Unexpected state for " + r,
+                    BroadcastRecord.DELIVERY_PENDING, r.getDeliveryState(i));
+        }, false /* andRemove */);
+    }
+
+    // TODO: Reuse BroadcastQueueTest.makeActiveProcessRecord()
+    private ProcessRecord makeProcessRecord(ApplicationInfo info) {
+        final ProcessRecord r = spy(new ProcessRecord(mAms, info, info.processName, info.uid));
+        r.setPid(mNextPid.incrementAndGet());
+        return r;
+    }
+
+    BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) {
+        final IIntentReceiver receiver = mock(IIntentReceiver.class);
+        final ReceiverList receiverList = new ReceiverList(mAms, app, app.getPid(), app.info.uid,
+                UserHandle.getUserId(app.info.uid), receiver);
+        return makeRegisteredReceiver(receiverList, priority);
+    }
+
     private Intent createPackageChangedIntent(int uid, List<String> componentNameList) {
         final Intent packageChangedIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED);
         packageChangedIntent.putExtra(Intent.EXTRA_UID, uid);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index 1c8e949..3364545 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -62,58 +62,36 @@
 import android.app.IApplicationThread;
 import android.app.UidObserver;
 import android.app.usage.UsageEvents.Event;
-import android.app.usage.UsageStatsManagerInternal;
 import android.content.ComponentName;
-import android.content.Context;
 import android.content.IIntentReceiver;
 import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
-import android.content.pm.ResolveInfo;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.DeadObjectException;
-import android.os.Handler;
-import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.PowerExemptionManager;
 import android.os.SystemClock;
-import android.os.TestLooperManager;
 import android.os.UserHandle;
-import android.provider.Settings;
 import android.util.Log;
 import android.util.Pair;
-import android.util.SparseArray;
 import android.util.proto.ProtoOutputStream;
 
 import androidx.test.filters.MediumTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
-import com.android.server.AlarmManagerInternal;
-import com.android.server.DropBoxManagerInternal;
-import com.android.server.LocalServices;
-import com.android.server.am.ActivityManagerService.Injector;
-import com.android.server.appop.AppOpsService;
-import com.android.server.wm.ActivityTaskManagerService;
-
 import org.junit.After;
 import org.junit.Assume;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 import org.mockito.ArgumentMatcher;
 import org.mockito.InOrder;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
 import org.mockito.verification.VerificationMode;
 
-import java.io.File;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.io.Writer;
@@ -126,7 +104,6 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.UnaryOperator;
 
@@ -136,13 +113,9 @@
 @MediumTest
 @RunWith(Parameterized.class)
 @SuppressWarnings("GuardedBy")
-public class BroadcastQueueTest {
+public class BroadcastQueueTest extends BaseBroadcastQueueTest {
     private static final String TAG = "BroadcastQueueTest";
 
-    @Rule
-    public final ApplicationExitInfoTest.ServiceThreadRule
-            mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule();
-
     private final Impl mImpl;
 
     private enum Impl {
@@ -150,30 +123,8 @@
         MODERN,
     }
 
-    private Context mContext;
-    private HandlerThread mHandlerThread;
-    private TestLooperManager mLooper;
-    private AtomicInteger mNextPid;
-
-    @Mock
-    private AppOpsService mAppOpsService;
-    @Mock
-    private ProcessList mProcessList;
-    @Mock
-    private DropBoxManagerInternal mDropBoxManagerInt;
-    @Mock
-    private PackageManagerInternal mPackageManagerInt;
-    @Mock
-    private UsageStatsManagerInternal mUsageStatsManagerInt;
-    @Mock
-    private AlarmManagerInternal mAlarmManagerInt;
-
-    private ActivityManagerService mAms;
     private BroadcastQueue mQueue;
-    BroadcastConstants mConstants;
-    private BroadcastSkipPolicy mSkipPolicy;
     private UidObserver mUidObserver;
-    private UidObserver mUidCachedStateObserver;
 
     /**
      * Desired behavior of the next
@@ -183,11 +134,6 @@
             ProcessStartBehavior.SUCCESS);
 
     /**
-     * Map from PID to registered registered runtime receivers.
-     */
-    private SparseArray<ReceiverList> mRegisteredReceivers = new SparseArray<>();
-
-    /**
      * Collection of all active processes during current test run.
      */
     private List<ProcessRecord> mActiveProcesses = new ArrayList<>();
@@ -208,41 +154,8 @@
 
     @Before
     public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
+        super.setUp();
 
-        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
-
-        mHandlerThread = new HandlerThread(TAG);
-        mHandlerThread.start();
-
-        // Pause all event processing until a test chooses to resume
-        mLooper = Objects.requireNonNull(InstrumentationRegistry.getInstrumentation()
-                .acquireLooperManager(mHandlerThread.getLooper()));
-
-        mNextPid = new AtomicInteger(100);
-
-        LocalServices.removeServiceForTest(DropBoxManagerInternal.class);
-        LocalServices.addService(DropBoxManagerInternal.class, mDropBoxManagerInt);
-        LocalServices.removeServiceForTest(PackageManagerInternal.class);
-        LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt);
-        LocalServices.removeServiceForTest(AlarmManagerInternal.class);
-        LocalServices.addService(AlarmManagerInternal.class, mAlarmManagerInt);
-        doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent();
-        doNothing().when(mPackageManagerInt).notifyComponentUsed(any(), anyInt(), any(), any());
-        doAnswer((invocation) -> {
-            return getUidForPackage(invocation.getArgument(0));
-        }).when(mPackageManagerInt).getPackageUid(any(), anyLong(), eq(UserHandle.USER_SYSTEM));
-
-        final ActivityManagerService realAms = new ActivityManagerService(
-                new TestInjector(mContext), mServiceThreadRule.getThread());
-        realAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
-        realAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
-        realAms.mAtmInternal = spy(realAms.mActivityTaskManager.getAtmInternal());
-        realAms.mOomAdjuster = spy(realAms.mOomAdjuster);
-        realAms.mPackageManagerInt = mPackageManagerInt;
-        realAms.mUsageStatsService = mUsageStatsManagerInt;
-        realAms.mProcessesReady = true;
-        mAms = spy(realAms);
         doAnswer((invocation) -> {
             Log.v(TAG, "Intercepting startProcessLocked() for "
                     + Arrays.toString(invocation.getArguments()));
@@ -321,21 +234,11 @@
             return null;
         }).when(mAms).registerUidObserver(any(), anyInt(),
                 eq(ActivityManager.PROCESS_STATE_TOP), any());
-        doAnswer((invocation) -> {
-            mUidCachedStateObserver = invocation.getArgument(0);
-            return null;
-        }).when(mAms).registerUidObserver(any(), anyInt(),
-                eq(ActivityManager.PROCESS_STATE_LAST_ACTIVITY), any());
 
-        mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
         mConstants.TIMEOUT = 200;
         mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
         mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 500;
 
-        mSkipPolicy = spy(new BroadcastSkipPolicy(mAms));
-        doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any());
-        doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any());
-
         final BroadcastHistory emptyHistory = new BroadcastHistory(mConstants) {
             public void addBroadcastToHistoryLocked(BroadcastRecord original) {
                 // Ignored
@@ -358,7 +261,7 @@
 
     @After
     public void tearDown() throws Exception {
-        mHandlerThread.quit();
+        super.tearDown();
 
         // Verify that all processes have finished handling broadcasts
         for (ProcessRecord app : mActiveProcesses) {
@@ -369,26 +272,9 @@
         }
     }
 
-    private class TestInjector extends Injector {
-        TestInjector(Context context) {
-            super(context);
-        }
-
-        @Override
-        public AppOpsService getAppOpsService(File recentAccessesFile, File storageFile,
-                Handler handler) {
-            return mAppOpsService;
-        }
-
-        @Override
-        public Handler getUiHandler(ActivityManagerService service) {
-            return mHandlerThread.getThreadHandler();
-        }
-
-        @Override
-        public ProcessList getProcessList(ActivityManagerService service) {
-            return mProcessList;
-        }
+    @Override
+    public String getTag() {
+        return TAG;
     }
 
     private enum ProcessStartBehavior {
@@ -534,62 +420,6 @@
         return Pair.create(app.getPid(), intent.getAction());
     }
 
-    static ApplicationInfo makeApplicationInfo(String packageName) {
-        return makeApplicationInfo(packageName, packageName, UserHandle.USER_SYSTEM);
-    }
-
-    static ApplicationInfo makeApplicationInfo(String packageName, String processName, int userId) {
-        final ApplicationInfo ai = new ApplicationInfo();
-        ai.packageName = packageName;
-        ai.processName = processName;
-        ai.uid = getUidForPackage(packageName, userId);
-        return ai;
-    }
-
-    static ResolveInfo withPriority(ResolveInfo info, int priority) {
-        info.priority = priority;
-        return info;
-    }
-
-    static BroadcastFilter withPriority(BroadcastFilter filter, int priority) {
-        filter.setPriority(priority);
-        return filter;
-    }
-
-    static ResolveInfo makeManifestReceiver(String packageName, String name) {
-        return makeManifestReceiver(packageName, name, UserHandle.USER_SYSTEM);
-    }
-
-    static ResolveInfo makeManifestReceiver(String packageName, String name, int userId) {
-        return makeManifestReceiver(packageName, packageName, name, userId);
-    }
-
-    static ResolveInfo makeManifestReceiver(String packageName, String processName, String name,
-            int userId) {
-        final ResolveInfo ri = new ResolveInfo();
-        ri.activityInfo = new ActivityInfo();
-        ri.activityInfo.packageName = packageName;
-        ri.activityInfo.processName = processName;
-        ri.activityInfo.name = name;
-        ri.activityInfo.applicationInfo = makeApplicationInfo(packageName, processName, userId);
-        return ri;
-    }
-
-    private BroadcastFilter makeRegisteredReceiver(ProcessRecord app) {
-        return makeRegisteredReceiver(app, 0);
-    }
-
-    private BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) {
-        final ReceiverList receiverList = mRegisteredReceivers.get(app.getPid());
-        final IntentFilter filter = new IntentFilter();
-        filter.setPriority(priority);
-        final BroadcastFilter res = new BroadcastFilter(filter, receiverList,
-                receiverList.app.info.packageName, null, null, null, receiverList.uid,
-                receiverList.userId, false, false, true);
-        receiverList.add(res);
-        return res;
-    }
-
     private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp,
             List<Object> receivers) {
         return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(),
@@ -776,41 +606,6 @@
                 eq(userId), anyInt(), anyInt(), any());
     }
 
-    static final int USER_GUEST = 11;
-
-    static final String PACKAGE_ANDROID = "android";
-    static final String PACKAGE_PHONE = "com.android.phone";
-    static final String PACKAGE_RED = "com.example.red";
-    static final String PACKAGE_GREEN = "com.example.green";
-    static final String PACKAGE_BLUE = "com.example.blue";
-    static final String PACKAGE_YELLOW = "com.example.yellow";
-    static final String PACKAGE_ORANGE = "com.example.orange";
-
-    static final String PROCESS_SYSTEM = "system";
-
-    static final String CLASS_RED = "com.example.red.Red";
-    static final String CLASS_GREEN = "com.example.green.Green";
-    static final String CLASS_BLUE = "com.example.blue.Blue";
-    static final String CLASS_YELLOW = "com.example.yellow.Yellow";
-    static final String CLASS_ORANGE = "com.example.orange.Orange";
-
-    static int getUidForPackage(@NonNull String packageName) {
-        switch (packageName) {
-            case PACKAGE_ANDROID: return android.os.Process.SYSTEM_UID;
-            case PACKAGE_PHONE: return android.os.Process.PHONE_UID;
-            case PACKAGE_RED: return android.os.Process.FIRST_APPLICATION_UID + 1;
-            case PACKAGE_GREEN: return android.os.Process.FIRST_APPLICATION_UID + 2;
-            case PACKAGE_BLUE: return android.os.Process.FIRST_APPLICATION_UID + 3;
-            case PACKAGE_YELLOW: return android.os.Process.FIRST_APPLICATION_UID + 4;
-            case PACKAGE_ORANGE: return android.os.Process.FIRST_APPLICATION_UID + 5;
-            default: throw new IllegalArgumentException();
-        }
-    }
-
-    static int getUidForPackage(@NonNull String packageName, int userId) {
-        return UserHandle.getUid(userId, getUidForPackage(packageName));
-    }
-
     /**
      * Baseline verification of common debugging infrastructure, mostly to make
      * sure it doesn't crash.
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 6792cfe..3803244 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -43,6 +43,7 @@
 import static android.app.NotificationManager.IMPORTANCE_LOW;
 import static android.app.NotificationManager.IMPORTANCE_MAX;
 import static android.app.NotificationManager.IMPORTANCE_NONE;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CALLS;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CONVERSATIONS;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT;
@@ -72,6 +73,7 @@
 import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
 import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
 import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE;
+import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
 import static android.service.notification.Adjustment.KEY_IMPORTANCE;
 import static android.service.notification.Adjustment.KEY_USER_SENTIMENT;
 import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING;
@@ -163,6 +165,7 @@
 import android.app.admin.DevicePolicyManagerInternal;
 import android.app.usage.UsageStatsManagerInternal;
 import android.companion.AssociationInfo;
+import android.companion.AssociationRequest;
 import android.companion.ICompanionDeviceManager;
 import android.compat.testing.PlatformCompatChangeRule;
 import android.content.BroadcastReceiver;
@@ -3820,6 +3823,7 @@
         when(mCompanionMgr.getAssociations(PKG, mUserId))
                 .thenReturn(singletonList(mock(AssociationInfo.class)));
         mListener = mock(ManagedServices.ManagedServiceInfo.class);
+        mListener.component = new ComponentName(PKG, PKG);
         when(mListener.enabledAndUserMatches(anyInt())).thenReturn(false);
         when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
 
@@ -3870,6 +3874,7 @@
         when(mCompanionMgr.getAssociations(PKG, mUserId))
                 .thenReturn(emptyList());
         mListener = mock(ManagedServices.ManagedServiceInfo.class);
+        mListener.component = new ComponentName(PKG, PKG);
         when(mListener.enabledAndUserMatches(anyInt())).thenReturn(false);
         when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener);
         try {
@@ -12777,6 +12782,145 @@
         verify(mSnoozeHelper).clearData(anyInt());
     }
 
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_mappedToImplicitRule() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+
+        NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
+        mBinderService.setNotificationPolicy("package", policy);
+
+        verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy));
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_systemCaller_setsGlobalPolicy() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenModeHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+        mService.isSystemUid = true;
+
+        NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
+        mBinderService.setNotificationPolicy("package", policy);
+
+        verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyBoolean());
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_watchCompanionApp_setsGlobalPolicy() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenModeHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+        when(mCompanionMgr.getAssociations(anyString(), anyInt()))
+                .thenReturn(ImmutableList.of(
+                        new AssociationInfo.Builder(1, mUserId, "package")
+                                .setDisplayName("My watch")
+                                .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WATCH)
+                                .build()));
+
+        NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
+        mBinderService.setNotificationPolicy("package", policy);
+
+        verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyBoolean());
+    }
+
+    @Test
+    @DisableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setNotificationPolicy_withoutCompat_setsGlobalPolicy() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenModeHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+
+        NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0);
+        mBinderService.setNotificationPolicy("package", policy);
+
+        verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyBoolean());
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void getNotificationPolicy_mappedFromImplicitRule() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+
+        mBinderService.getNotificationPolicy("package");
+
+        verify(zenHelper).getNotificationPolicyFromImplicitZenRule(eq("package"));
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setInterruptionFilter_mappedToImplicitRule() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+
+        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY);
+
+        verify(zenHelper).applyGlobalZenModeAsImplicitZenRule(eq("package"), anyInt(),
+                eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS));
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setInterruptionFilter_systemCaller_setsGlobalPolicy() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mService.setCallerIsNormalPackage();
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenModeHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+        mService.isSystemUid = true;
+
+        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY);
+
+        verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null),
+                eq("package"), anyString(), anyInt(), anyBoolean());
+    }
+
+    @Test
+    @EnableCompatChanges(NotificationManagerService.MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES)
+    public void setInterruptionFilter_watchCompanionApp_setsGlobalPolicy() throws RemoteException {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        ZenModeHelper zenModeHelper = mock(ZenModeHelper.class);
+        mService.mZenModeHelper = zenModeHelper;
+        when(mConditionProviders.isPackageOrComponentAllowed(anyString(), anyInt()))
+                .thenReturn(true);
+        when(mCompanionMgr.getAssociations(anyString(), anyInt()))
+                .thenReturn(ImmutableList.of(
+                        new AssociationInfo.Builder(1, mUserId, "package")
+                                .setDisplayName("My watch")
+                                .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WATCH)
+                                .build()));
+
+        mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY);
+
+        verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null),
+                eq("package"), anyString(), anyInt(), anyBoolean());
+    }
+
     private NotificationRecord createAndPostNotification(Notification.Builder nb, String testName)
             throws RemoteException {
         StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, testName, mUid, 0,
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
index 47f15b8..1e3b728 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java
@@ -19,6 +19,7 @@
 import static com.android.server.notification.SnoozeHelper.EXTRA_KEY;
 
 import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
@@ -73,6 +74,14 @@
 public class SnoozeHelperTest extends UiServiceTestCase {
     private static final String TEST_CHANNEL_ID = "test_channel_id";
 
+    private static final String XML_TAG_NAME = "snoozed-notifications";
+    private static final String XML_SNOOZED_NOTIFICATION = "notification";
+    private static final String XML_SNOOZED_NOTIFICATION_CONTEXT = "context";
+    private static final String XML_SNOOZED_NOTIFICATION_KEY = "key";
+    private static final String XML_SNOOZED_NOTIFICATION_TIME = "time";
+    private static final String XML_SNOOZED_NOTIFICATION_CONTEXT_ID = "id";
+    private static final String XML_SNOOZED_NOTIFICATION_VERSION_LABEL = "version";
+
     @Mock SnoozeHelper.Callback mCallback;
     @Mock AlarmManager mAm;
     @Mock ManagedServices.UserProfiles mUserProfiles;
@@ -316,6 +325,53 @@
     }
 
     @Test
+    public void testSnoozeLimit_maximumPersisted() throws XmlPullParserException, IOException {
+        final long snoozeTimeout = 1234;
+        final String snoozeContext = "ctx";
+        // Serialize & deserialize notifications so that only persisted lists are used
+        TypedXmlSerializer serializer = Xml.newFastSerializer();
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        serializer.setOutput(new BufferedOutputStream(baos), "utf-8");
+        serializer.startDocument(null, true);
+        serializer.startTag(null, XML_TAG_NAME);
+        // Serialize maximum number of timed + context snoozed notifications, half of each
+        for (int i = 0; i < CONCURRENT_SNOOZE_LIMIT; i++) {
+            final boolean timedNotification = i % 2 == 0;
+            if (timedNotification) {
+                serializer.startTag(null, XML_SNOOZED_NOTIFICATION);
+            } else {
+                serializer.startTag(null, XML_SNOOZED_NOTIFICATION_CONTEXT);
+            }
+            serializer.attributeInt(null, XML_SNOOZED_NOTIFICATION_VERSION_LABEL, 1);
+            serializer.attribute(null, XML_SNOOZED_NOTIFICATION_KEY, "key" + i);
+            if (timedNotification) {
+                serializer.attributeLong(null, XML_SNOOZED_NOTIFICATION_TIME, snoozeTimeout);
+                serializer.endTag(null, XML_SNOOZED_NOTIFICATION);
+            } else {
+                serializer.attribute(null, XML_SNOOZED_NOTIFICATION_CONTEXT_ID, snoozeContext);
+                serializer.endTag(null, XML_SNOOZED_NOTIFICATION_CONTEXT);
+            }
+        }
+        serializer.endTag(null, XML_TAG_NAME);
+        serializer.endDocument();
+        serializer.flush();
+
+        TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setInput(new BufferedInputStream(
+                new ByteArrayInputStream(baos.toByteArray())), "utf-8");
+        mSnoozeHelper.readXml(parser, 1);
+        // Verify that we can't snooze any more notifications
+        //  and that the limit is caused by persisted notifications
+        assertThat(mSnoozeHelper.canSnooze(1)).isFalse();
+        assertThat(mSnoozeHelper.isSnoozed(UserHandle.USER_SYSTEM, "pkg", "key0")).isFalse();
+        assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM,
+                "pkg", "key0")).isEqualTo(snoozeTimeout);
+        assertThat(
+            mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg",
+                "key1")).isEqualTo(snoozeContext);
+    }
+
+    @Test
     public void testCancelByApp() throws Exception {
         NotificationRecord r = getNotificationRecord("pkg", 1, "one", UserHandle.SYSTEM);
         NotificationRecord r2 = getNotificationRecord("pkg", 2, "two", UserHandle.SYSTEM);
@@ -611,6 +667,7 @@
 
     @Test
     public void repostGroupSummary_repostsSummary() throws Exception {
+        final int snoozeDuration = 1000;
         IntArray profileIds = new IntArray();
         profileIds.add(UserHandle.USER_SYSTEM);
         when(mUserProfiles.getCurrentProfileIds()).thenReturn(profileIds);
@@ -618,10 +675,14 @@
                 "pkg", 1, "one", UserHandle.SYSTEM, "group1", true);
         NotificationRecord r2 = getNotificationRecord(
                 "pkg", 2, "two", UserHandle.SYSTEM, "group1", false);
-        mSnoozeHelper.snooze(r, 1000);
-        mSnoozeHelper.snooze(r2, 1000);
+        final long snoozeTime = System.currentTimeMillis() + snoozeDuration;
+        mSnoozeHelper.snooze(r, snoozeDuration);
+        mSnoozeHelper.snooze(r2, snoozeDuration);
         assertEquals(2, mSnoozeHelper.getSnoozed().size());
         assertEquals(2, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size());
+        // Verify that summary notification was added to the persisted list
+        assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg",
+                r.getKey())).isAtLeast(snoozeTime);
 
         mSnoozeHelper.repostGroupSummary("pkg", UserHandle.USER_SYSTEM, r.getGroupKey());
 
@@ -630,6 +691,39 @@
 
         assertEquals(1, mSnoozeHelper.getSnoozed().size());
         assertEquals(1, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size());
+        // Verify that summary notification was removed from the persisted list
+        assertThat(mSnoozeHelper.getSnoozeTimeForUnpostedNotification(UserHandle.USER_SYSTEM, "pkg",
+                r.getKey())).isEqualTo(0);
+    }
+
+    @Test
+    public void snoozeWithContext_repostGroupSummary_removesPersisted() throws Exception {
+        final String snoozeContext = "zzzzz";
+        IntArray profileIds = new IntArray();
+        profileIds.add(UserHandle.USER_SYSTEM);
+        when(mUserProfiles.getCurrentProfileIds()).thenReturn(profileIds);
+        NotificationRecord r = getNotificationRecord(
+                "pkg", 1, "one", UserHandle.SYSTEM, "group1", true);
+        NotificationRecord r2 = getNotificationRecord(
+                "pkg", 2, "two", UserHandle.SYSTEM, "group1", false);
+        mSnoozeHelper.snooze(r, snoozeContext);
+        mSnoozeHelper.snooze(r2, snoozeContext);
+        assertEquals(2, mSnoozeHelper.getSnoozed().size());
+        assertEquals(2, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size());
+        // Verify that summary notification was added to the persisted list
+        assertThat(mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM,
+            "pkg", r.getKey())).isEqualTo(snoozeContext);
+
+        mSnoozeHelper.repostGroupSummary("pkg", UserHandle.USER_SYSTEM, r.getGroupKey());
+
+        verify(mCallback, times(1)).repost(UserHandle.USER_SYSTEM, r, false);
+        verify(mCallback, never()).repost(UserHandle.USER_SYSTEM, r2, false);
+
+        assertEquals(1, mSnoozeHelper.getSnoozed().size());
+        assertEquals(1, mSnoozeHelper.getSnoozed(UserHandle.USER_SYSTEM, "pkg").size());
+        // Verify that summary notification was removed from the persisted list
+        assertThat(mSnoozeHelper.getSnoozeContextForUnpostedNotification(UserHandle.USER_SYSTEM,
+                "pkg", r.getKey())).isNull();
     }
 
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
index 27e8f36..8f30f41 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
@@ -54,6 +54,15 @@
         return mRankingHelper;
     }
 
+    /**
+     * Sets {@link #isSystemUid} and {@link #isSystemAppId} to {@code false}, so that calls to NMS
+     * methods don't succeed {@link #isCallingUidSystem()} and similar checks.
+     */
+    void setCallerIsNormalPackage() {
+        isSystemUid = false;
+        isSystemAppId = false;
+    }
+
     @Override
     protected boolean isCallingUidSystem() {
         countSystemChecks++;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
new file mode 100644
index 0000000..6cc1c43
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.notification;
+
+import static com.android.server.notification.ZenAdapters.notificationPolicyToZenPolicy;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.NotificationManager.Policy;
+import android.service.notification.ZenPolicy;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ZenAdaptersTest extends UiServiceTestCase {
+
+    @Test
+    public void notificationPolicyToZenPolicy_allCallers() {
+        Policy policy = new Policy(Policy.PRIORITY_CATEGORY_CALLS, Policy.PRIORITY_SENDERS_ANY, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getPriorityCallSenders()).isEqualTo(ZenPolicy.PEOPLE_TYPE_ANYONE);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_starredCallers() {
+        Policy policy = new Policy(Policy.PRIORITY_CATEGORY_CALLS, Policy.PRIORITY_SENDERS_STARRED,
+                0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getPriorityCallSenders()).isEqualTo(ZenPolicy.PEOPLE_TYPE_STARRED);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_repeatCallers() {
+        Policy policy = new Policy(Policy.PRIORITY_CATEGORY_REPEAT_CALLERS, 0, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(zenPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getPriorityCallSenders()).isEqualTo(ZenPolicy.PEOPLE_TYPE_NONE);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_noCallers() {
+        Policy policy = new Policy(0, 0, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryCalls()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(zenPolicy.getPriorityCallSenders()).isEqualTo(ZenPolicy.PEOPLE_TYPE_NONE);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_conversationsAllowedSendersUnset() {
+        Policy policy = new Policy(Policy.PRIORITY_CATEGORY_CONVERSATIONS, 0, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryConversations()).isEqualTo(ZenPolicy.STATE_UNSET);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_conversationsNotAllowedSendersUnset() {
+        Policy policy = new Policy(0, 0, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getPriorityCategoryConversations()).isEqualTo(
+                ZenPolicy.STATE_DISALLOW);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_setEffects() {
+        Policy policy = new Policy(0, 0, 0,
+                Policy.SUPPRESSED_EFFECT_BADGE | Policy.SUPPRESSED_EFFECT_LIGHTS);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+        assertThat(zenPolicy.getVisualEffectLights()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+
+        assertThat(zenPolicy.getVisualEffectAmbient()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getVisualEffectFullScreenIntent()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getVisualEffectNotificationList()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_ALLOW);
+        assertThat(zenPolicy.getVisualEffectStatusBar()).isEqualTo(ZenPolicy.STATE_ALLOW);
+    }
+
+    @Test
+    public void notificationPolicyToZenPolicy_unsetEffects() {
+        Policy policy = new Policy(0, 0, 0);
+
+        ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
+
+        assertThat(zenPolicy.getVisualEffectAmbient()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectFullScreenIntent()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectLights()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectNotificationList()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_UNSET);
+        assertThat(zenPolicy.getVisualEffectStatusBar()).isEqualTo(ZenPolicy.STATE_UNSET);
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index e8201fd..37aeb57 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -22,6 +22,7 @@
 import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_DISABLED;
 import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ENABLED;
 import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_ANYONE;
+import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CALLS;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CONVERSATIONS;
@@ -32,6 +33,7 @@
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_REPEAT_CALLERS;
 import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_SYSTEM;
 import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_ANY;
+import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS;
 import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_STARRED;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE;
@@ -40,8 +42,11 @@
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
 import static android.provider.Settings.Global.ZEN_MODE_ALARMS;
 import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+import static android.provider.Settings.Global.ZEN_MODE_OFF;
 import static android.service.notification.Condition.STATE_FALSE;
 import static android.service.notification.Condition.STATE_TRUE;
+import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS;
+import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED;
 
 import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.LOG_DND_STATE_EVENTS;
 import static com.android.os.dnd.DNDProtoEnums.PEOPLE_STARRED;
@@ -50,6 +55,8 @@
 import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW;
 import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
@@ -72,6 +79,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.AppGlobals;
 import android.app.AppOpsManager;
@@ -82,6 +90,7 @@
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
@@ -92,6 +101,7 @@
 import android.media.AudioSystem;
 import android.media.VolumePolicy;
 import android.net.Uri;
+import android.os.Parcel;
 import android.os.Process;
 import android.os.UserHandle;
 import android.platform.test.flag.junit.SetFlagsRule;
@@ -100,6 +110,7 @@
 import android.service.notification.Condition;
 import android.service.notification.ZenModeConfig;
 import android.service.notification.ZenModeConfig.ScheduleInfo;
+import android.service.notification.ZenModeConfig.ZenRule;
 import android.service.notification.ZenModeDiff;
 import android.service.notification.ZenPolicy;
 import android.test.suitebuilder.annotation.SmallTest;
@@ -124,6 +135,7 @@
 import com.android.server.notification.ManagedServices.UserProfiles;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.truth.Correspondence;
 import com.google.protobuf.InvalidProtocolBufferException;
 
 import org.junit.Before;
@@ -157,27 +169,29 @@
     private static final String EVENTS_DEFAULT_RULE_ID = "EVENTS_DEFAULT_RULE";
     private static final String SCHEDULE_DEFAULT_RULE_ID = "EVERY_NIGHT_DEFAULT_RULE";
     private static final String CUSTOM_PKG_NAME = "not.android";
+    private static final String CUSTOM_APP_LABEL = "This is not Android";
     private static final int CUSTOM_PKG_UID = 1;
     private static final String CUSTOM_RULE_ID = "custom_rule";
 
-    private final String NAME = "name";
-    private final ComponentName OWNER = new ComponentName("pkg", "cls");
-    private final ComponentName CONFIG_ACTIVITY = new ComponentName("pkg", "act");
-    private final ZenPolicy POLICY = new ZenPolicy.Builder().allowAlarms(true).build();
-    private final Uri CONDITION_ID = new Uri.Builder().scheme("scheme")
+    private static final String NAME = "name";
+    private static final ComponentName OWNER = new ComponentName("pkg", "cls");
+    private static final ComponentName CONFIG_ACTIVITY = new ComponentName("pkg", "act");
+    private static final ZenPolicy POLICY = new ZenPolicy.Builder().allowAlarms(true).build();
+    private static final Uri CONDITION_ID = new Uri.Builder().scheme("scheme")
             .authority("authority")
             .appendPath("path")
             .appendPath("test")
             .build();
 
-    private final Condition CONDITION = new Condition(CONDITION_ID, "", Condition.STATE_TRUE);
-    private final String TRIGGER_DESC = "Every Night, 10pm to 6am";
-    private final int TYPE = TYPE_BEDTIME;
-    private final boolean ALLOW_MANUAL = true;
-    private final int ICON_RES_ID = 1234;
-    private final int INTERRUPTION_FILTER = Settings.Global.ZEN_MODE_ALARMS;
-    private final boolean ENABLED = true;
-    private final int CREATION_TIME = 123;
+    private static final Condition CONDITION = new Condition(CONDITION_ID, "",
+            Condition.STATE_TRUE);
+    private static final String TRIGGER_DESC = "Every Night, 10pm to 6am";
+    private static final int TYPE = TYPE_BEDTIME;
+    private static final boolean ALLOW_MANUAL = true;
+    private static final int ICON_RES_ID = 1234;
+    private static final int INTERRUPTION_FILTER = Settings.Global.ZEN_MODE_ALARMS;
+    private static final boolean ENABLED = true;
+    private static final int CREATION_TIME = 123;
 
     @Rule
     public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -227,6 +241,10 @@
                 .thenReturn(CUSTOM_PKG_UID);
         when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(
                 new String[] {pkg});
+        ApplicationInfo mockAppInfo = mock(ApplicationInfo.class);
+        when(mockAppInfo.loadLabel(any())).thenReturn(CUSTOM_APP_LABEL);
+        when(mPackageManager.getApplicationInfo(eq(CUSTOM_PKG_NAME), anyInt()))
+                .thenReturn(mockAppInfo);
         mZenModeHelper.mPm = mPackageManager;
 
         mZenModeEventLogger.reset();
@@ -334,7 +352,7 @@
 
     @Test
     public void testZenOff_NoMuteApplied() {
-        mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
         mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS
                 | PRIORITY_CATEGORY_MEDIA, 0, 0, 0, 0, 0);
@@ -635,7 +653,7 @@
 
         // 3. apply zen off - verify zen is set to previous ringer (normal)
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.applyZenToRingerMode();
         verify(mAudioManager, atLeastOnce()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL,
                 mZenModeHelper.TAG);
@@ -721,7 +739,7 @@
 
         // 3.  apply zen off - verify ringer remains normal
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL);
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.applyZenToRingerMode();
         verify(mAudioManager, atLeastOnce()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL,
                 mZenModeHelper.TAG);
@@ -746,7 +764,7 @@
 
         // 3. apply zen-off - verify ringer is still silent
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.applyZenToRingerMode();
         verify(mAudioManager, atLeastOnce()).setRingerModeInternal(AudioManager.RINGER_MODE_SILENT,
                 mZenModeHelper.TAG);
@@ -781,7 +799,7 @@
 
         // 4.  apply zen off - verify ringer still silenced
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.applyZenToRingerMode();
         verify(mAudioManager, atLeastOnce()).setRingerModeInternal(AudioManager.RINGER_MODE_SILENT,
                 mZenModeHelper.TAG);
@@ -795,7 +813,7 @@
 
         // apply zen off multiple times - verify ringer is not set to normal
         when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.mConfig = null; // will evaluate config to zen mode off
         for (int i = 0; i < 3; i++) {
             // if zen doesn't change, zen should not reapply itself to the ringer
@@ -809,7 +827,7 @@
     public void testSilentRingerSavedOnZenOff_startsZenOn() {
         AudioManagerInternal mAudioManager = mock(AudioManagerInternal.class);
         mZenModeHelper.mAudioManager = mAudioManager;
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.mConfig = new ZenModeConfig();
 
         // previously set silent ringer
@@ -836,7 +854,7 @@
     public void testVibrateRingerSavedOnZenOff_startsZenOn() {
         AudioManagerInternal mAudioManager = mock(AudioManagerInternal.class);
         mZenModeHelper.mAudioManager = mAudioManager;
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.mConfig = new ZenModeConfig();
 
         // previously set silent ringer
@@ -1209,7 +1227,7 @@
                 .allowMedia(false)
                 .allowRepeatCallers(false)
                 .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE)
-                .allowMessages(ZenPolicy.PEOPLE_TYPE_CONTACTS)
+                .allowMessages(PEOPLE_TYPE_CONTACTS)
                 .allowEvents(true)
                 .allowReminders(false)
                 .build();
@@ -2023,10 +2041,10 @@
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeHelper.mZenMode);
 
         // and also that it works to turn it back off again
-        mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, "",
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, null, "",
                 Process.SYSTEM_UID, true);
 
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeHelper.mZenMode);
+        assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode);
     }
 
     @Test
@@ -2041,7 +2059,7 @@
 
         // Now turn zen mode off, but via a different package UID -- this should get registered as
         // "not an action by the user" because some other app is changing zen mode
-        mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, "", CUSTOM_PKG_UID,
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, null, "", CUSTOM_PKG_UID,
                 false);
 
         // In total, this should be 2 loggable changes
@@ -2060,7 +2078,7 @@
         //   - resulting DNDPolicyProto the same as the values in setupZenConfig()
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
                 mZenModeEventLogger.getEventId(0));
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
+        assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0));
         assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(0));
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
@@ -2080,7 +2098,7 @@
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(),
                 mZenModeEventLogger.getEventId(1));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getPrevZenMode(1));
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1));
+        assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1));
         assertEquals(DNDProtoEnums.MANUAL_RULE, mZenModeEventLogger.getChangedRuleType(1));
         assertEquals(0, mZenModeEventLogger.getNumRulesActive(1));
         assertFalse(mZenModeEventLogger.getIsUserAction(1));
@@ -2144,7 +2162,7 @@
         //   - zen policy is the same as the set-up zen config
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
                 mZenModeEventLogger.getEventId(0));
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
+        assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0));
         assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(0));
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
@@ -2157,7 +2175,7 @@
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(),
                 mZenModeEventLogger.getEventId(1));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getPrevZenMode(1));
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1));
+        assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getNewZenMode(1));
         assertEquals(DNDProtoEnums.AUTOMATIC_RULE, mZenModeEventLogger.getChangedRuleType(1));
         assertEquals(0, mZenModeEventLogger.getNumRulesActive(1));
         assertTrue(mZenModeEventLogger.getIsUserAction(1));
@@ -2201,7 +2219,7 @@
 
         // Turn zen mode off; we want to make sure policy changes do not get logged when zen mode
         // is off.
-        mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, null, "",
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, null, "",
                 Process.SYSTEM_UID, true);
 
         // Change the policy again
@@ -2305,7 +2323,7 @@
         // what the event should reflect. At this time, the policy is the same as initial setup.
         assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
                 mZenModeEventLogger.getEventId(0));
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
+        assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
         assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getNewZenMode(0));
         assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
         assertFalse(mZenModeEventLogger.getIsUserAction(0));
@@ -2355,7 +2373,7 @@
         mZenModeHelper.evaluateZenModeLocked("test", true);
 
         // Check that the change actually took: zen mode should be off now
-        assertEquals(Global.ZEN_MODE_OFF, mZenModeHelper.mZenMode);
+        assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode);
 
         // but still, nothing should've been logged
         assertEquals(0, mZenModeEventLogger.numLoggedChanges());
@@ -2483,7 +2501,7 @@
                 true);
 
         // Turn off manual mode, call from a package: don't reset UID even though enabler is set
-        mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null,
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null,
                 CUSTOM_PKG_NAME, "", 12345, false);
 
         // And likewise when turning it back on again
@@ -2660,8 +2678,11 @@
     }
 
     @Test
-    public void testCreateAutomaticZenRule_allFields() {
+    public void zenRuleToAutomaticZenRule_allFields() {
         mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+        when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(
+                new String[] {OWNER.getPackageName()});
+
         ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
         rule.configurationActivity = CONFIG_ACTIVITY;
         rule.component = OWNER;
@@ -2682,7 +2703,8 @@
         rule.iconResId = ICON_RES_ID;
         rule.triggerDescription = TRIGGER_DESC;
 
-        AutomaticZenRule actual = mZenModeHelper.createAutomaticZenRule(rule);
+        mZenModeHelper.mConfig.automaticRules.put(rule.id, rule);
+        AutomaticZenRule actual = mZenModeHelper.getAutomaticZenRule(rule.id);
 
         assertEquals(NAME, actual.getName());
         assertEquals(OWNER, actual.getOwner());
@@ -2915,8 +2937,253 @@
         assertEquals(false, mZenModeHelper.mConfig.automaticRules.get(createdId).snoozing);
     }
 
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_createsImplicitRuleAndActivatesIt() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.values())
+                .comparingElementsUsing(IGNORE_TIMESTAMPS)
+                .containsExactly(
+                        expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                                null, true));
+    }
+
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_updatesImplicitRuleAndActivatesIt() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, "test", "test", 0, true);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1);
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_ALARMS);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.values())
+                .comparingElementsUsing(IGNORE_TIMESTAMPS)
+                .containsExactly(
+                        expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_ALARMS, null, true));
+    }
+
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_modeOff_deactivatesImplicitRule() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1);
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state)
+                .isEqualTo(STATE_TRUE);
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_OFF);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state)
+                .isEqualTo(STATE_FALSE);
+    }
+
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_modeOffButNoPreviousRule_ignored() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_OFF);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty();
+    }
+
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_update_unsnoozesRule() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_IMPORTANT_INTERRUPTIONS);
+        assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1);
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isFalse();
+
+        mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, "test", "test", 0, true);
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isTrue();
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_ALARMS);
+
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isFalse();
+        assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state)
+                .isEqualTo(STATE_TRUE);
+    }
+
+    @Test
+    public void applyGlobalZenModeAsImplicitZenRule_flagOff_ignored() {
+        mSetFlagsRule.disableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        withoutWtfCrash(
+                () -> mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME,
+                        CUSTOM_PKG_UID,
+                        ZEN_MODE_IMPORTANT_INTERRUPTIONS));
+
+        assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty();
+    }
+
+    @Test
+    public void applyGlobalPolicyAsImplicitZenRule_createsImplicitRule() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        Policy policy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
+                PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
+                Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy);
+
+        ZenPolicy expectedZenPolicy = new ZenPolicy.Builder()
+                .disallowAllSounds()
+                .allowCalls(PEOPLE_TYPE_CONTACTS)
+                .allowConversations(CONVERSATION_SENDERS_IMPORTANT)
+                .hideAllVisualEffects()
+                .build();
+        assertThat(mZenModeHelper.mConfig.automaticRules.values())
+                .comparingElementsUsing(IGNORE_TIMESTAMPS)
+                .containsExactly(
+                        expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                                expectedZenPolicy, /* conditionActive= */ null));
+    }
+
+    @Test
+    public void applyGlobalPolicyAsImplicitZenRule_updatesImplicitRule() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        Policy original = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
+                PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
+                Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                original);
+
+        // Change priorityCallSenders: contacts -> starred.
+        Policy updated = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
+                PRIORITY_SENDERS_STARRED, PRIORITY_SENDERS_STARRED,
+                Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT);
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated);
+
+        ZenPolicy expectedZenPolicy = new ZenPolicy.Builder()
+                .disallowAllSounds()
+                .allowCalls(PEOPLE_TYPE_STARRED)
+                .allowConversations(CONVERSATION_SENDERS_IMPORTANT)
+                .hideAllVisualEffects()
+                .build();
+        assertThat(mZenModeHelper.mConfig.automaticRules.values())
+                .comparingElementsUsing(IGNORE_TIMESTAMPS)
+                .containsExactly(
+                        expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+                                expectedZenPolicy, /* conditionActive= */ null));
+    }
+
+    @Test
+    public void applyGlobalPolicyAsImplicitZenRule_flagOff_ignored() {
+        mSetFlagsRule.disableFlags(android.app.Flags.FLAG_MODES_API);
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        withoutWtfCrash(
+                () -> mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME,
+                        CUSTOM_PKG_UID, new Policy(0, 0, 0)));
+
+        assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty();
+    }
+
+    @Test
+    public void getNotificationPolicyFromImplicitZenRule_returnsSetPolicy() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+        Policy writtenPolicy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS,
+                PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED,
+                Policy.getAllSuppressedVisualEffects(), STATE_FALSE,
+                CONVERSATION_SENDERS_IMPORTANT);
+        mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                writtenPolicy);
+
+        Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule(
+                CUSTOM_PKG_NAME);
+
+        assertThat(readPolicy).isEqualTo(writtenPolicy);
+    }
+
+    @Test
+    public void getNotificationPolicyFromImplicitZenRule_ruleWithoutPolicy_returnsGlobalPolicy() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+
+        mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+                ZEN_MODE_ALARMS);
+        mZenModeHelper.mConfig.allowCalls = true;
+        mZenModeHelper.mConfig.allowConversations = false;
+
+        Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule(
+                CUSTOM_PKG_NAME);
+
+        assertThat(readPolicy).isNotNull();
+        assertThat(readPolicy.allowCalls()).isTrue();
+        assertThat(readPolicy.allowConversations()).isFalse();
+    }
+
+    @Test
+    public void getNotificationPolicyFromImplicitZenRule_noImplicitRule_returnsGlobalPolicy() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+
+        mZenModeHelper.mConfig.allowCalls = true;
+        mZenModeHelper.mConfig.allowConversations = false;
+
+        Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule(
+                CUSTOM_PKG_NAME);
+
+        assertThat(readPolicy).isNotNull();
+        assertThat(readPolicy.allowCalls()).isTrue();
+        assertThat(readPolicy.allowConversations()).isFalse();
+    }
+
+    private static final Correspondence<ZenRule, ZenRule> IGNORE_TIMESTAMPS =
+            Correspondence.transforming(zr -> {
+                Parcel p = Parcel.obtain();
+                try {
+                    zr.writeToParcel(p, 0);
+                    p.setDataPosition(0);
+                    ZenRule copy = new ZenRule(p);
+                    copy.creationTime = 0;
+                    return copy;
+                } finally {
+                    p.recycle();
+                }
+            },
+            "Ignoring timestamps");
+
+    private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy,
+            @Nullable Boolean conditionActive) {
+        ZenRule rule = new ZenModeConfig.ZenRule();
+        rule.id = "implicit_" + ownerPkg;
+        rule.conditionId = Uri.parse("condition://android/implicit/" + ownerPkg);
+        if (conditionActive != null) {
+            rule.condition = conditionActive
+                    ? new Condition(rule.conditionId,
+                            mContext.getString(R.string.zen_mode_implicit_activated), STATE_TRUE)
+                    : new Condition(rule.conditionId,
+                            mContext.getString(R.string.zen_mode_implicit_deactivated),
+                            STATE_FALSE);
+        }
+        rule.zenMode = zenMode;
+        rule.zenPolicy = policy;
+        rule.pkg = ownerPkg;
+        rule.name = CUSTOM_APP_LABEL;
+        rule.enabled = true;
+        return rule;
+    }
+
     private void setupZenConfig() {
-        mZenModeHelper.mZenMode = Global.ZEN_MODE_OFF;
+        mZenModeHelper.mZenMode = ZEN_MODE_OFF;
         mZenModeHelper.mConfig.allowAlarms = false;
         mZenModeHelper.mConfig.allowMedia = false;
         mZenModeHelper.mConfig.allowSystem = false;
@@ -2965,6 +3232,15 @@
         assertEquals(STATE_ALLOW, dndProto.getNotificationList().getNumber());
     }
 
+    private static void withoutWtfCrash(Runnable test) {
+        Log.TerribleFailureHandler oldHandler = Log.setWtfHandler((tag, what, system) -> {});
+        try {
+            test.run();
+        } finally {
+            Log.setWtfHandler(oldHandler);
+        }
+    }
+
     /**
      * Wrapper to use TypedXmlPullParser as XmlResourceParser for Resources.getXml()
      */