Merge "Pass statsToken from IMM to InsetsController when calling show/hide" into main
diff --git a/apct-tests/perftests/protolog/Android.bp b/apct-tests/perftests/protolog/Android.bp
new file mode 100644
index 0000000..08e365b
--- /dev/null
+++ b/apct-tests/perftests/protolog/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+android_test {
+    name: "ProtologPerfTests",
+    team: "trendy_team_windowing_tools",
+    srcs: ["src/**/*.java"],
+    static_libs: [
+        "androidx.test.rules",
+        "androidx.annotation_annotation",
+        "apct-perftests-utils",
+        "collector-device-lib",
+        "platform-test-annotations",
+    ],
+    test_suites: [
+        "device-tests",
+        "automotive-tests",
+    ],
+    data: [":perfetto_artifacts"],
+    platform_apis: true,
+    certificate: "platform",
+}
diff --git a/apct-tests/perftests/protolog/AndroidManifest.xml b/apct-tests/perftests/protolog/AndroidManifest.xml
new file mode 100644
index 0000000..68125df
--- /dev/null
+++ b/apct-tests/perftests/protolog/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.perftests.protolog">
+
+    <!-- For perfetto trace files -->
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.perftests.protolog">
+        <!-- <meta-data android:name="listener" android:value="android.protolog.ProtologPerfRunListener" /> -->
+    </instrumentation>
+</manifest>
diff --git a/apct-tests/perftests/protolog/AndroidTest.xml b/apct-tests/perftests/protolog/AndroidTest.xml
new file mode 100644
index 0000000..871a20c
--- /dev/null
+++ b/apct-tests/perftests/protolog/AndroidTest.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs ProtologPerfTests metric instrumentation.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-metric-instrumentation" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="ProtologPerfTests.apk" />
+    </target_preparer>
+
+    <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+        <option name="force-skip-system-props" value="true" />
+        <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
+        <option name="run-command" value="cmd window dismiss-keyguard" />
+        <option name="run-command" value="cmd package compile -m speed com.android.perftests.wm" />
+    </target_preparer>
+
+    <!-- Needed for pushing the trace config file -->
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="push-file" key="trace_config_detailed.textproto" value="/data/misc/perfetto-traces/trace_config.textproto" />
+    </target_preparer>
+
+    <!-- Needed for storing the perfetto trace files in the sdcard/test_results-->
+    <option name="isolated-storage" value="false" />
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.perftests.protolog" />
+        <option name="hidden-api-checks" value="false"/>
+
+        <!-- Listener related args for collecting the traces and waiting for the device to stabilize. -->
+        <option name="device-listeners" value="android.device.collectors.ProcLoadListener,android.device.collectors.PerfettoListener" />
+
+        <!-- Guarantee that user defined RunListeners will be running before any of the default listeners defined in this runner. -->
+        <option name="instrumentation-arg" key="newRunListenerMode" value="true" />
+
+        <!-- ProcLoadListener related arguments -->
+        <!-- Wait for device last minute threshold to reach 3 with 2 minute timeout before starting the test run -->
+        <option name="instrumentation-arg" key="procload-collector:per_run" value="true" />
+        <option name="instrumentation-arg" key="proc-loadavg-threshold" value="3" />
+        <option name="instrumentation-arg" key="proc-loadavg-timeout" value="120000" />
+        <option name="instrumentation-arg" key="proc-loadavg-interval" value="10000" />
+
+        <!-- PerfettoListener related arguments -->
+        <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true" />
+        <option name="instrumentation-arg" key="perfetto_config_file" value="trace_config.textproto" />
+    </test>
+
+    <!-- <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="directory-keys" value="/data/local/tmp/ProtologPerfTests" /> -->
+        <!-- Needed for pulling the collected trace config on to the host -->
+        <!-- <option name="pull-pattern-keys" value="perfetto_file_path" />
+    </metrics_collector> -->
+</configuration>
diff --git a/apct-tests/perftests/protolog/OWNERS b/apct-tests/perftests/protolog/OWNERS
new file mode 100644
index 0000000..3f3308c
--- /dev/null
+++ b/apct-tests/perftests/protolog/OWNERS
@@ -0,0 +1 @@
+include platform/development:/tools/winscope/OWNERS
diff --git a/apct-tests/perftests/protolog/src/com/android/internal/protolog/ProtologPerfTest.java b/apct-tests/perftests/protolog/src/com/android/internal/protolog/ProtologPerfTest.java
new file mode 100644
index 0000000..e1edb37
--- /dev/null
+++ b/apct-tests/perftests/protolog/src/com/android/internal/protolog/ProtologPerfTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.protolog;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+
+import com.android.internal.protolog.common.IProtoLogGroup;
+import com.android.internal.protolog.common.LogLevel;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+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 java.util.Arrays;
+import java.util.Collection;
+
+@RunWith(Parameterized.class)
+public class ProtologPerfTest {
+    @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    @Parameters(name="logToProto_{0}_logToLogcat_{1}")
+    public static Collection<Object[]> params() {
+        return Arrays.asList(new Object[][] {
+                { true, true },
+                { true, false },
+                { false, true },
+                { false, false }
+        });
+    }
+
+    private final boolean mLogToProto;
+    private final boolean mLogToLogcat;
+
+    public ProtologPerfTest(boolean logToProto, boolean logToLogcat) {
+        mLogToProto = logToProto;
+        mLogToLogcat = logToLogcat;
+    }
+
+    @BeforeClass
+    public static void init() {
+        ProtoLog.init(TestProtoLogGroup.values());
+    }
+
+    @Before
+    public void setUp() {
+        TestProtoLogGroup.TEST_GROUP.setLogToProto(mLogToProto);
+        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(mLogToLogcat);
+    }
+
+    @Test
+    public void logProcessedProtoLogMessageWithoutArgs() {
+        final var protoLog = ProtoLog.getSingleInstance();
+
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            protoLog.log(
+                    LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 123,
+                    0, (Object[]) null);
+        }
+    }
+
+    @Test
+    public void logProcessedProtoLogMessageWithArgs() {
+        final var protoLog = ProtoLog.getSingleInstance();
+
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            protoLog.log(
+                    LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 123,
+                    0b1110101001010100,
+                    new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true});
+        }
+    }
+
+    @Test
+    public void logNonProcessedProtoLogMessageWithNoArgs() {
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            ProtoLog.d(TestProtoLogGroup.TEST_GROUP, "Test message");
+        }
+    }
+
+    @Test
+    public void logNonProcessedProtoLogMessageWithArgs() {
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            ProtoLog.d(TestProtoLogGroup.TEST_GROUP, "Test messag %s, %d, %b", "arg1", 2, true);
+        }
+    }
+
+    private enum TestProtoLogGroup implements IProtoLogGroup {
+        TEST_GROUP(true, true, false, "WindowManagetProtoLogTest");
+
+        private final boolean mEnabled;
+        private volatile boolean mLogToProto;
+        private volatile boolean mLogToLogcat;
+        private final String mTag;
+
+        /**
+         * @param enabled set to false to exclude all log statements for this group from
+         *     compilation, they will not be available in runtime.
+         * @param logToProto enable binary logging for the group
+         * @param logToLogcat enable text logging for the group
+         * @param tag name of the source of the logged message
+         */
+        TestProtoLogGroup(boolean enabled, boolean logToProto, boolean logToLogcat, String tag) {
+            this.mEnabled = enabled;
+            this.mLogToProto = logToProto;
+            this.mLogToLogcat = logToLogcat;
+            this.mTag = tag;
+        }
+
+        @Override
+        public boolean isEnabled() {
+            return mEnabled;
+        }
+
+        @Override
+        public boolean isLogToProto() {
+            return mLogToProto;
+        }
+
+        @Override
+        public boolean isLogToLogcat() {
+            return mLogToLogcat;
+        }
+
+        @Override
+        public boolean isLogToAny() {
+            return mLogToLogcat || mLogToProto;
+        }
+
+        @Override
+        public String getTag() {
+            return mTag;
+        }
+
+        @Override
+        public void setLogToProto(boolean logToProto) {
+            this.mLogToProto = logToProto;
+        }
+
+        @Override
+        public void setLogToLogcat(boolean logToLogcat) {
+            this.mLogToLogcat = logToLogcat;
+        }
+
+        @Override
+        public int getId() {
+            return ordinal();
+        }
+    }
+}
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 57acc71..918e591 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -241,10 +241,11 @@
     // ZenModeConfig XML versions distinguishing key changes.
     public static final int XML_VERSION_ZEN_UPGRADE = 8;
     public static final int XML_VERSION_MODES_API = 11;
+    public static final int XML_VERSION_MODES_UI = 12;
 
-    // TODO: b/310620812 - Update XML_VERSION and update default_zen_config.xml accordingly when
-    //       modes_api is inlined.
-    private static final int XML_VERSION = 10;
+    // TODO: b/310620812, b/344831624 - Update XML_VERSION and update default_zen_config.xml
+    //  accordingly when modes_api / modes_ui are inlined.
+    private static final int XML_VERSION_PRE_MODES = 10;
     public static final String ZEN_TAG = "zen";
     private static final String ZEN_ATT_VERSION = "version";
     private static final String ZEN_ATT_USER = "user";
@@ -952,7 +953,13 @@
     }
 
     public static int getCurrentXmlVersion() {
-        return Flags.modesApi() ? XML_VERSION_MODES_API : XML_VERSION;
+        if (Flags.modesUi()) {
+            return XML_VERSION_MODES_UI;
+        } else if (Flags.modesApi()) {
+            return XML_VERSION_MODES_API;
+        } else {
+            return XML_VERSION_PRE_MODES;
+        }
     }
 
     public static ZenModeConfig readXml(TypedXmlPullParser parser)
@@ -2607,7 +2614,7 @@
         @AutomaticZenRule.Type
         public int type = AutomaticZenRule.TYPE_UNKNOWN;
         public String triggerDescription;
-        public String iconResName;
+        @Nullable public String iconResName;
         public boolean allowManualInvocation;
         @AutomaticZenRule.ModifiableField public int userModifiedFields;
         @ZenPolicy.ModifiableField public int zenPolicyUserModifiedFields;
diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java
index fe26510..7366b9a 100644
--- a/core/java/android/webkit/WebSettings.java
+++ b/core/java/android/webkit/WebSettings.java
@@ -497,25 +497,32 @@
     public abstract  boolean getUseWebViewBackgroundForOverscrollBackground();
 
     /**
-     * Sets whether the WebView should save form data. In Android O, the
-     * platform has implemented a fully functional Autofill feature to store
-     * form data. Therefore, the Webview form data save feature is disabled.
+     * Sets whether the WebView should save form data. In {@link android.os.Build.VERSION_CODES#O},
+     * the platform has implemented a fully functional Autofill feature to store form data.
+     * Therefore, the Webview form data save feature is disabled.
      *
-     * Note that the feature will continue to be supported on older versions of
+     * <p>Note that the feature will continue to be supported on older versions of
      * Android as before.
      *
-     * @deprecated In Android O and afterwards, this function does not have
-     * any effect, the form data will be saved to platform's autofill service
-     * if applicable.
+     * @see #getSaveFormData
+     * @deprecated In Android O and afterwards, this function does not have any effect. Form data
+     * will be saved to platform's autofill service if applicable.
      */
     @Deprecated
     public abstract  void setSaveFormData(boolean save);
 
     /**
-     * Gets whether the WebView saves form data.
+     * Gets whether the WebView saves form data. In {@link android.os.Build.VERSION_CODES#O}, the
+     * platform has implemented a fully functional Autofill feature to store form data. Therefore,
+     * the Webview form data save feature is disabled.
+     *
+     * <p>Note that the feature will continue to be supported on older versions of
+     * Android as before.
      *
      * @return whether the WebView saves form data
      * @see #setSaveFormData
+     * @deprecated In Android O and afterwards, this function does not have any effect. Form data
+     * will be filled from the platform's autofill service if applicable.
      */
     @Deprecated
     public abstract boolean getSaveFormData();
diff --git a/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java b/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java
index b82c660..34e0418 100644
--- a/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java
@@ -40,6 +40,8 @@
  */
 @Deprecated
 public class LogcatOnlyProtoLogImpl implements IProtoLog {
+    private static final String LOG_TAG = LogcatOnlyProtoLogImpl.class.getName();
+
     @Override
     public void log(LogLevel logLevel, IProtoLogGroup group, long messageHash, int paramsMask,
             Object[] args) {
@@ -48,19 +50,21 @@
 
     @Override
     public void log(LogLevel logLevel, IProtoLogGroup group, String messageString, Object[] args) {
-        if (REQUIRE_PROTOLOGTOOL) {
-            throw new RuntimeException(
-                    "REQUIRE_PROTOLOGTOOL not set to false before the first log call.");
+        if (REQUIRE_PROTOLOGTOOL && group.isLogToProto()) {
+            Log.w(LOG_TAG, "ProtoLog message not processed. Failed to log it to proto. "
+                    + "Logging it below to logcat instead.");
         }
 
-        String formattedString = TextUtils.formatSimple(messageString, args);
-        switch (logLevel) {
-            case VERBOSE -> Log.v(group.getTag(), formattedString);
-            case INFO -> Log.i(group.getTag(), formattedString);
-            case DEBUG -> Log.d(group.getTag(), formattedString);
-            case WARN -> Log.w(group.getTag(), formattedString);
-            case ERROR -> Log.e(group.getTag(), formattedString);
-            case WTF -> Log.wtf(group.getTag(), formattedString);
+        if (group.isLogToLogcat() || group.isLogToProto()) {
+            String formattedString = TextUtils.formatSimple(messageString, args);
+            switch (logLevel) {
+                case VERBOSE -> Log.v(group.getTag(), formattedString);
+                case INFO -> Log.i(group.getTag(), formattedString);
+                case DEBUG -> Log.d(group.getTag(), formattedString);
+                case WARN -> Log.w(group.getTag(), formattedString);
+                case ERROR -> Log.e(group.getTag(), formattedString);
+                case WTF -> Log.wtf(group.getTag(), formattedString);
+            }
         }
     }
 
diff --git a/core/java/com/android/internal/protolog/ProtoLog.java b/core/java/com/android/internal/protolog/ProtoLog.java
index 660d3c9..bf77db7 100644
--- a/core/java/com/android/internal/protolog/ProtoLog.java
+++ b/core/java/com/android/internal/protolog/ProtoLog.java
@@ -63,6 +63,9 @@
      * @param groups The ProtoLog groups that will be used in the process.
      */
     public static void init(IProtoLogGroup... groups) {
+        // These tracing instances are only used when we cannot or do not preprocess the source
+        // files to extract out the log strings. Otherwise, the trace calls are replaced with calls
+        // directly to the generated tracing implementations.
         if (android.tracing.Flags.perfettoProtologTracing()) {
             synchronized (sInitLock) {
                 if (sProtoLogInstance != null) {
@@ -76,8 +79,6 @@
                 sProtoLogInstance = new PerfettoProtoLogImpl(groups);
             }
         } else {
-            // The first call to ProtoLog is likely to flip REQUIRE_PROTOLOGTOOL, which is when this
-            // static block will be executed before REQUIRE_PROTOLOGTOOL is actually set.
             sProtoLogInstance = new LogcatOnlyProtoLogImpl();
         }
     }
diff --git a/core/jni/android_graphics_SurfaceTexture.cpp b/core/jni/android_graphics_SurfaceTexture.cpp
index 50832a5..8dd63cc 100644
--- a/core/jni/android_graphics_SurfaceTexture.cpp
+++ b/core/jni/android_graphics_SurfaceTexture.cpp
@@ -256,9 +256,21 @@
     }
 }
 
-static void SurfaceTexture_init(JNIEnv* env, jobject thiz, jboolean isDetached,
-        jint texName, jboolean singleBufferMode, jobject weakThiz)
-{
+static void SurfaceTexture_init(JNIEnv* env, jobject thiz, jboolean isDetached, jint texName,
+                                jboolean singleBufferMode, jobject weakThiz) {
+#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
+    sp<SurfaceTexture> surfaceTexture;
+    if (isDetached) {
+        surfaceTexture = new SurfaceTexture(GL_TEXTURE_EXTERNAL_OES, true, !singleBufferMode);
+    } else {
+        surfaceTexture =
+                new SurfaceTexture(texName, GL_TEXTURE_EXTERNAL_OES, true, !singleBufferMode);
+    }
+
+    if (singleBufferMode) {
+        surfaceTexture->setMaxBufferCount(1);
+    }
+#else
     sp<IGraphicBufferProducer> producer;
     sp<IGraphicBufferConsumer> consumer;
     BufferQueue::createBufferQueue(&producer, &consumer);
@@ -275,6 +287,7 @@
         surfaceTexture = new SurfaceTexture(consumer, texName,
                 GL_TEXTURE_EXTERNAL_OES, true, !singleBufferMode);
     }
+#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
 
     if (surfaceTexture == 0) {
         jniThrowException(env, OutOfResourcesException,
@@ -287,11 +300,27 @@
             createProcessUniqueId()));
 
     // If the current context is protected, inform the producer.
+#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
+    surfaceTexture->setConsumerIsProtected(isProtectedContext());
+
+    SurfaceTexture_setSurfaceTexture(env, thiz, surfaceTexture);
+    sp<Surface> surface = surfaceTexture->getSurface();
+    if (nullptr == surface) {
+        jniThrowException(env, IllegalStateException, "Unable to get surface from SurfaceTexture");
+        return;
+    }
+    sp<IGraphicBufferProducer> igbp = surface->getIGraphicBufferProducer();
+    if (nullptr == igbp) {
+        jniThrowException(env, IllegalStateException, "Unable to get IGBP from Surface");
+        return;
+    }
+    SurfaceTexture_setProducer(env, thiz, igbp);
+#else
     consumer->setConsumerIsProtected(isProtectedContext());
 
     SurfaceTexture_setSurfaceTexture(env, thiz, surfaceTexture);
     SurfaceTexture_setProducer(env, thiz, producer);
-
+#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
     jclass clazz = env->GetObjectClass(thiz);
     if (clazz == NULL) {
         jniThrowRuntimeException(env,
diff --git a/libs/hwui/tests/common/TestContext.cpp b/libs/hwui/tests/common/TestContext.cpp
index fd596d9..e427c97 100644
--- a/libs/hwui/tests/common/TestContext.cpp
+++ b/libs/hwui/tests/common/TestContext.cpp
@@ -16,6 +16,7 @@
 
 #include "tests/common/TestContext.h"
 
+#include <com_android_graphics_libgui_flags.h>
 #include <cutils/trace.h>
 
 namespace android {
@@ -101,6 +102,14 @@
 }
 
 void TestContext::createOffscreenSurface() {
+#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
+    mConsumer = new BufferItemConsumer(GRALLOC_USAGE_HW_COMPOSER, 4);
+    const ui::Size& resolution = getActiveDisplayResolution();
+    mConsumer->setDefaultBufferSize(resolution.getWidth(), resolution.getHeight());
+    mSurface = mConsumer->getSurface();
+    mSurface->setMaxDequeuedBufferCount(3);
+    mSurface->setAsyncMode(true);
+#else
     sp<IGraphicBufferProducer> producer;
     sp<IGraphicBufferConsumer> consumer;
     BufferQueue::createBufferQueue(&producer, &consumer);
@@ -110,6 +119,7 @@
     const ui::Size& resolution = getActiveDisplayResolution();
     mConsumer->setDefaultBufferSize(resolution.getWidth(), resolution.getHeight());
     mSurface = new Surface(producer);
+#endif  // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
 }
 
 void TestContext::waitForVsync() {
@@ -144,4 +154,4 @@
 
 }  // namespace test
 }  // namespace uirenderer
-}  // namespace android
+}  // namespace android
\ No newline at end of file
diff --git a/media/jni/android_media_ImageReader.cpp b/media/jni/android_media_ImageReader.cpp
index 371e3d2..019b1e0 100644
--- a/media/jni/android_media_ImageReader.cpp
+++ b/media/jni/android_media_ImageReader.cpp
@@ -17,35 +17,31 @@
 //#define LOG_NDEBUG 0
 #define LOG_TAG "ImageReader_JNI"
 #define ATRACE_TAG ATRACE_TAG_CAMERA
-#include "android_media_Utils.h"
+#include <android/hardware_buffer_jni.h>
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/android_graphics_GraphicBuffer.h>
+#include <android_runtime/android_hardware_HardwareBuffer.h>
+#include <android_runtime/android_view_Surface.h>
+#include <com_android_graphics_libgui_flags.h>
 #include <cutils/atomic.h>
-#include <utils/Log.h>
-#include <utils/misc.h>
+#include <grallocusage/GrallocUsageConversion.h>
+#include <gui/BufferItemConsumer.h>
+#include <gui/Surface.h>
+#include <inttypes.h>
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include <private/android/AHardwareBufferHelpers.h>
+#include <stdint.h>
+#include <ui/Rect.h>
 #include <utils/List.h>
-#include <utils/Trace.h>
+#include <utils/Log.h>
 #include <utils/String8.h>
+#include <utils/Trace.h>
+#include <utils/misc.h>
 
 #include <cstdio>
 
-#include <gui/BufferItemConsumer.h>
-#include <gui/Surface.h>
-
-#include <android_runtime/AndroidRuntime.h>
-#include <android_runtime/android_view_Surface.h>
-#include <android_runtime/android_graphics_GraphicBuffer.h>
-#include <android_runtime/android_hardware_HardwareBuffer.h>
-#include <grallocusage/GrallocUsageConversion.h>
-
-#include <private/android/AHardwareBufferHelpers.h>
-
-#include <jni.h>
-#include <nativehelper/JNIHelp.h>
-
-#include <stdint.h>
-#include <inttypes.h>
-#include <android/hardware_buffer_jni.h>
-
-#include <ui/Rect.h>
+#include "android_media_Utils.h"
 
 #define ANDROID_MEDIA_IMAGEREADER_CTX_JNI_ID       "mNativeContext"
 #define ANDROID_MEDIA_SURFACEIMAGE_BUFFER_JNI_ID   "mNativeBuffer"
@@ -393,18 +389,25 @@
     }
     sp<JNIImageReaderContext> ctx(new JNIImageReaderContext(env, weakThiz, clazz, maxImages));
 
-    sp<IGraphicBufferProducer> gbProducer;
-    sp<IGraphicBufferConsumer> gbConsumer;
-    BufferQueue::createBufferQueue(&gbProducer, &gbConsumer);
-    sp<BufferItemConsumer> bufferConsumer;
     String8 consumerName = String8::format("ImageReader-%dx%df%xm%d-%d-%d",
             width, height, nativeHalFormat, maxImages, getpid(),
             createProcessUniqueId());
     uint64_t consumerUsage =
             android_hardware_HardwareBuffer_convertToGrallocUsageBits(ndkUsage);
 
+#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
+    sp<BufferItemConsumer> bufferConsumer = new BufferItemConsumer(consumerUsage, maxImages,
+                                                                   /*controlledByApp*/ true);
+    sp<IGraphicBufferProducer> gbProducer =
+            bufferConsumer->getSurface()->getIGraphicBufferProducer();
+#else
+    sp<IGraphicBufferProducer> gbProducer;
+    sp<IGraphicBufferConsumer> gbConsumer;
+    BufferQueue::createBufferQueue(&gbProducer, &gbConsumer);
+    sp<BufferItemConsumer> bufferConsumer;
     bufferConsumer = new BufferItemConsumer(gbConsumer, consumerUsage, maxImages,
             /*controlledByApp*/true);
+#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
     if (bufferConsumer == nullptr) {
         jniThrowExceptionFmt(env, "java/lang/RuntimeException",
                 "Failed to allocate native buffer consumer for hal format 0x%x and usage 0x%x",
@@ -413,7 +416,11 @@
     }
 
     if (consumerUsage & GRALLOC_USAGE_PROTECTED) {
+#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
+        bufferConsumer->setConsumerIsProtected(true);
+#else
         gbConsumer->setConsumerIsProtected(true);
+#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
     }
 
     ctx->setBufferConsumer(bufferConsumer);
diff --git a/media/mca/filterfw/native/core/gl_env.cpp b/media/mca/filterfw/native/core/gl_env.cpp
index 1bb82f8..4637ccd 100644
--- a/media/mca/filterfw/native/core/gl_env.cpp
+++ b/media/mca/filterfw/native/core/gl_env.cpp
@@ -15,21 +15,23 @@
  */
 // #define LOG_NDEBUG 0
 
-#include "base/logging.h"
-#include "base/utilities.h"
 #include "core/gl_env.h"
-#include "core/shader_program.h"
-#include "core/vertex_frame.h"
-#include "system/window.h"
+
+#include <EGL/eglext.h>
+#include <com_android_graphics_libgui_flags.h>
+#include <gui/BufferQueue.h>
+#include <gui/GLConsumer.h>
+#include <gui/IGraphicBufferProducer.h>
+#include <gui/Surface.h>
 
 #include <map>
 #include <string>
-#include <EGL/eglext.h>
 
-#include <gui/BufferQueue.h>
-#include <gui/Surface.h>
-#include <gui/GLConsumer.h>
-#include <gui/IGraphicBufferProducer.h>
+#include "base/logging.h"
+#include "base/utilities.h"
+#include "core/shader_program.h"
+#include "core/vertex_frame.h"
+#include "system/window.h"
 
 namespace android {
 namespace filterfw {
@@ -165,12 +167,18 @@
   }
 
   // Create dummy surface using a GLConsumer
+#if COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
+  surfaceTexture_ = new GLConsumer(0, GLConsumer::TEXTURE_EXTERNAL, /*useFenceSync=*/true,
+                                   /*isControlledByApp=*/false);
+  window_ = surfaceTexture_->getSurface();
+#else
   sp<IGraphicBufferProducer> producer;
   sp<IGraphicBufferConsumer> consumer;
   BufferQueue::createBufferQueue(&producer, &consumer);
   surfaceTexture_ = new GLConsumer(consumer, 0, GLConsumer::TEXTURE_EXTERNAL,
           true, false);
   window_ = new Surface(producer);
+#endif // COM_ANDROID_GRAPHICS_LIBGUI_FLAGS(WB_CONSUMER_BASE_OWNS_BQ)
 
   surfaces_[0] = SurfaceWindowPair(eglCreateWindowSurface(display(), config, window_.get(), NULL), NULL);
   if (CheckEGLError("eglCreateWindowSurface")) return false;
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
index d36b55f..9fa8fc3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
@@ -286,8 +286,7 @@
 
     /**
      * Returns the mode icon -- which can be either app-provided (via {@code addAutomaticZenRule}),
-     * user-chosen (via the icon picker in Settings), the app's launcher icon for implicit rules
-     * (in its monochrome variant, if available), or a default icon based on the mode type.
+     * user-chosen (via the icon picker in Settings), or a default icon based on the mode type.
      */
     @NonNull
     public ListenableFuture<Drawable> getIcon(@NonNull Context context,
@@ -300,23 +299,6 @@
         return iconLoader.getIcon(context, mRule);
     }
 
-    /**
-     * Returns an alternative mode icon. The difference with {@link #getIcon} is that it's the
-     * basic DND icon not only for Manual DND, but also for <em>implicit rules</em>. As such, it's
-     * suitable for places where showing the launcher icon of an app could be confusing, such as
-     * the status bar or lockscreen.
-     */
-    @NonNull
-    public ListenableFuture<Drawable> getLockscreenIcon(@NonNull Context context,
-            @NonNull ZenIconLoader iconLoader) {
-        if (mKind == Kind.MANUAL_DND || mKind == Kind.IMPLICIT) {
-            return Futures.immediateFuture(requireNonNull(
-                    context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
-        }
-
-        return iconLoader.getIcon(context, mRule);
-    }
-
     @NonNull
     public ZenPolicy getPolicy() {
         switch (mRule.getInterruptionFilter()) {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
index f533e77..32216fa 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
@@ -322,41 +322,6 @@
         verify(iconLoader).getIcon(any(), eq(IMPLICIT_ZEN_RULE));
     }
 
-    @Test
-    public void getLockscreenIcon_normalMode_loadsIconNormally() {
-        ZenIconLoader iconLoader = mock(ZenIconLoader.class);
-        ZenMode mode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, false));
-
-        ListenableFuture<Drawable> unused = mode.getLockscreenIcon(
-                RuntimeEnvironment.getApplication(), iconLoader);
-
-        verify(iconLoader).getIcon(any(), eq(ZEN_RULE));
-    }
-
-    @Test
-    public void getLockscreenIcon_manualDnd_returnsFixedIcon() {
-        ZenIconLoader iconLoader = mock(ZenIconLoader.class);
-
-        ListenableFuture<Drawable> future = TestModeBuilder.MANUAL_DND_INACTIVE.getLockscreenIcon(
-                RuntimeEnvironment.getApplication(), iconLoader);
-
-        assertThat(future.isDone()).isTrue();
-        verify(iconLoader, never()).getIcon(any(), any());
-    }
-
-    @Test
-    public void getLockscreenIcon_implicitMode_returnsFixedIcon() {
-        ZenIconLoader iconLoader = mock(ZenIconLoader.class);
-        ZenMode mode = new ZenMode(IMPLICIT_RULE_ID, IMPLICIT_ZEN_RULE,
-                zenConfigRuleFor(IMPLICIT_ZEN_RULE, false));
-
-        ListenableFuture<Drawable> future = mode.getLockscreenIcon(
-                RuntimeEnvironment.getApplication(), iconLoader);
-
-        assertThat(future.isDone()).isTrue();
-        verify(iconLoader, never()).getIcon(any(), any());
-    }
-
     private static void assertUnparceledIsEqualToOriginal(String type, ZenMode original) {
         Parcel parcel = Parcel.obtain();
         try {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index 5cc194d..2b5953c 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -523,8 +523,8 @@
     fun convert(progress: Float): Float
 
     companion object {
-        /** Keeps scrolling linearly */
-        val Default = linear()
+        /** Starts linearly with some resistance and slowly approaches to 0.2f */
+        val Default = tanh(maxProgress = 0.2f, tilt = 3f)
 
         /**
          * The scroll stays linear, with [factor] you can control how much resistance there is.
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 682fe95..770c0f8 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -724,6 +724,7 @@
                 layoutHeight = layoutHeight,
                 sceneTransitions = {
                     overscroll(SceneB, Orientation.Vertical) {
+                        progressConverter = ProgressConverter.linear()
                         // On overscroll 100% -> Foo should translate by overscrollTranslateY
                         translate(TestElements.Foo, y = overscrollTranslateY)
                     }
@@ -780,6 +781,7 @@
                     transitions =
                         transitions {
                             overscroll(SceneB, Orientation.Vertical) {
+                                progressConverter = ProgressConverter.linear()
                                 translate(TestElements.Foo, y = overscrollTranslateY)
                             }
                         }
@@ -921,6 +923,7 @@
                 layoutHeight = layoutHeight,
                 sceneTransitions = {
                     overscroll(SceneB, Orientation.Vertical) {
+                        progressConverter = ProgressConverter.linear()
                         // On overscroll 100% -> Foo should translate by layoutHeight
                         translate(TestElements.Foo, y = { absoluteDistance })
                     }
@@ -1015,7 +1018,7 @@
                 layoutHeight = layoutHeight,
                 sceneTransitions = {
                     // Overscroll progress will be linear (by default)
-                    defaultOverscrollProgressConverter = ProgressConverter { it }
+                    defaultOverscrollProgressConverter = ProgressConverter.linear()
 
                     overscroll(SceneB, Orientation.Vertical) {
                         // This override the defaultOverscrollProgressConverter
@@ -1125,6 +1128,7 @@
                         )
 
                     overscroll(SceneB, Orientation.Vertical) {
+                        progressConverter = ProgressConverter.linear()
                         // On overscroll 100% -> Foo should translate by layoutHeight
                         translate(TestElements.Foo, y = { absoluteDistance })
                     }
@@ -1861,6 +1865,7 @@
                     SceneA,
                     transitions {
                         overscroll(SceneB, Orientation.Vertical) {
+                            progressConverter = ProgressConverter.linear()
                             translate(TestElements.Foo, y = 15.dp)
                         }
                     }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 06799bc..e48cd817 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -614,6 +614,7 @@
                         from(SceneA, to = SceneB) { distance = FixedDistance(swipeDistance) }
 
                         overscroll(SceneB, Orientation.Vertical) {
+                            progressConverter = ProgressConverter.linear()
                             translate(TestElements.Foo, x = { 20.dp.toPx() }, y = { 30.dp.toPx() })
                         }
                     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelTest.kt
new file mode 100644
index 0000000..a310ef4
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelTest.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.row.ui.viewmodel
+
+import android.platform.test.annotations.EnableFlags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.notification.row.data.repository.fakeNotificationRowRepository
+import com.android.systemui.statusbar.notification.row.shared.EnRouteContentModel
+import com.android.systemui.statusbar.notification.row.shared.IconModel
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@EnableFlags(RichOngoingNotificationFlag.FLAG_NAME)
+class EnRouteViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val repository = kosmos.fakeNotificationRowRepository
+
+    private var contentModel: EnRouteContentModel?
+        get() = repository.richOngoingContentModel.value as? EnRouteContentModel
+        set(value) {
+            repository.richOngoingContentModel.value = value
+        }
+
+    private lateinit var underTest: EnRouteViewModel
+
+    @Before
+    fun setup() {
+        underTest = kosmos.getEnRouteViewModel(repository)
+    }
+
+    @Test
+    fun viewModelShowsContent() =
+        testScope.runTest {
+            val title by collectLastValue(underTest.title)
+            val text by collectLastValue(underTest.text)
+            contentModel =
+                exampleEnRouteContent(
+                    title = "Example EnRoute Title",
+                    text = "Example EnRoute Text",
+                )
+            assertThat(title).isEqualTo("Example EnRoute Title")
+            assertThat(text).isEqualTo("Example EnRoute Text")
+        }
+
+    private fun exampleEnRouteContent(
+        icon: IconModel = mock(),
+        title: CharSequence = "example text",
+        text: CharSequence = "example title",
+    ) =
+        EnRouteContentModel(
+            smallIcon = icon,
+            title = title,
+            text = text,
+        )
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt
index 789a473..f920b18 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/WeatherData.kt
@@ -4,6 +4,7 @@
 import android.util.Log
 import android.view.View
 import androidx.annotation.VisibleForTesting
+import androidx.core.text.util.LocalePreferences
 
 typealias WeatherTouchAction = (View) -> Unit
 
@@ -54,12 +55,35 @@
             }
         }
 
-        private fun readIntFromBundle(extras: Bundle, key: String): Int? =
+        private fun readIntFromBundle(extras: Bundle, key: String): Int? {
             try {
-                extras.getString(key)?.toInt()
+                return extras.getString(key)?.toInt()
             } catch (e: Exception) {
-                null
+                return null
             }
+        }
+
+        fun getPlaceholderWeatherData(): WeatherData {
+            return getPlaceholderWeatherData(
+                LocalePreferences.getTemperatureUnit() == LocalePreferences.TemperatureUnit.CELSIUS
+            )
+        }
+
+        private const val DESCRIPTION_PLACEHODLER = ""
+        private const val TEMPERATURE_FAHRENHEIT_PLACEHOLDER = 58
+        private const val TEMPERATURE_CELSIUS_PLACEHOLDER = 21
+        private val WEATHERICON_PLACEHOLDER = WeatherData.WeatherStateIcon.MOSTLY_SUNNY
+
+        fun getPlaceholderWeatherData(useCelsius: Boolean): WeatherData {
+            return WeatherData(
+                description = DESCRIPTION_PLACEHODLER,
+                state = WEATHERICON_PLACEHOLDER,
+                temperature =
+                    if (useCelsius) TEMPERATURE_CELSIUS_PLACEHOLDER
+                    else TEMPERATURE_FAHRENHEIT_PLACEHOLDER,
+                useCelsius = useCelsius,
+            )
+        }
     }
 
     // Values for WeatherStateIcon must stay in sync with go/g3-WeatherStateIcon
diff --git a/packages/SystemUI/res/layout/notification_template_en_route_contracted.xml b/packages/SystemUI/res/layout/notification_template_en_route_contracted.xml
new file mode 100644
index 0000000..59cfecc
--- /dev/null
+++ b/packages/SystemUI/res/layout/notification_template_en_route_contracted.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<com.android.systemui.statusbar.notification.row.ui.view.EnRouteView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/status_bar_latest_event_content"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_weight="1"
+    android:minHeight="@*android:dimen/notification_headerless_min_height"
+    android:tag="enroute"
+    >
+
+    <include layout="@*android:layout/notification_template_material_base" />
+
+</com.android.systemui.statusbar.notification.row.ui.view.EnRouteView>
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 5dcf161..c1eae2e 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -477,6 +477,12 @@
         smallClockFrame?.viewTreeObserver?.removeOnGlobalLayoutListener(onGlobalLayoutListener)
     }
 
+    fun setFallbackWeatherData(data: WeatherData) {
+        if (weatherData != null) return
+        weatherData = data
+        clock?.run { events.onWeatherDataChanged(data) }
+    }
+
     /**
      * Sets this clock as showing in a secondary display.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 51ce355..9ccfb50 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -79,6 +79,7 @@
 import com.android.systemui.monet.ColorScheme
 import com.android.systemui.monet.Style
 import com.android.systemui.plugins.clocks.ClockController
+import com.android.systemui.plugins.clocks.WeatherData
 import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shared.clocks.ClockRegistry
@@ -188,6 +189,7 @@
     init {
         coroutineScope = CoroutineScope(applicationScope.coroutineContext + Job())
         disposables += DisposableHandle { coroutineScope.cancel() }
+        clockController.setFallbackWeatherData(WeatherData.getPlaceholderWeatherData())
 
         if (KeyguardBottomAreaRefactor.isEnabled) {
             quickAffordancesCombinedViewModel.enablePreviewMode(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
index 5f5b265..3f18fc2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt
@@ -57,7 +57,7 @@
                     isActivated = modes.isNotEmpty(),
                     icon =
                         if (Flags.modesApi() && Flags.modesUi() && Flags.modesUiIcons())
-                            zenModeInteractor.getActiveModeIcon(context, modes)
+                            zenModeInteractor.getActiveModeIcon(modes)
                         else null,
                     activeModes = modes.map { it.name }
                 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
index f74c9a6..e9292f8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
@@ -79,6 +79,7 @@
             // NOTE: NotificationEntry.isClearable will internally check group children to ensure
             //  the group itself definitively clearable.
             val isClearable = !isSensitiveContentProtectionActive && entry.isClearable
+                    && !entry.isSensitive.value
             when {
                 isSilent && isClearable -> hasClearableSilentNotifs = true
                 isSilent && !isClearable -> hasNonClearableSilentNotifs = true
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
index bf5b3a3..da29b0f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt
@@ -22,6 +22,7 @@
 import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.shared.EnRouteContentModel
 import com.android.systemui.statusbar.notification.row.shared.IconModel
 import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
 import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
@@ -68,12 +69,13 @@
         builder: Notification.Builder,
         systemUIContext: Context,
         packageContext: Context
-    ): RichOngoingContentModel? =
+    ): RichOngoingContentModel? {
+        val sbn = entry.sbn
+        val notification = sbn.notification
+        val icon = IconModel(notification.smallIcon)
+
         try {
-            val sbn = entry.sbn
-            val notification = sbn.notification
-            val icon = IconModel(notification.smallIcon)
-            if (sbn.packageName == "com.google.android.deskclock") {
+            return if (sbn.packageName == "com.google.android.deskclock") {
                 when (notification.channelId) {
                     "Timers v2" -> {
                         parseTimerNotification(notification, icon)
@@ -87,11 +89,14 @@
                         null
                     }
                 }
+            } else if (builder.style is Notification.EnRouteStyle) {
+                parseEnRouteNotification(notification, icon)
             } else null
         } catch (e: Exception) {
             Log.e("RONs", "Error parsing RON", e)
-            null
+            return null
         }
+    }
 
     /**
      * FOR PROTOTYPING ONLY: create a RON TimerContentModel using the time information available
@@ -199,4 +204,15 @@
             .plusMinutes(minute.toLong())
             .plusSeconds(second.toLong())
     }
+
+    private fun parseEnRouteNotification(
+        notification: Notification,
+        icon: IconModel,
+    ): EnRouteContentModel {
+        return EnRouteContentModel(
+            smallIcon = icon,
+            title = notification.extras.getCharSequence(Notification.EXTRA_TITLE),
+            text = notification.extras.getCharSequence(Notification.EXTRA_TEXT),
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt
index 828fc21..2c462b7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt
@@ -27,12 +27,15 @@
 import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.InflatedContentViewHolder
 import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.KeepExistingView
 import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.NullContentView
+import com.android.systemui.statusbar.notification.row.shared.EnRouteContentModel
 import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
 import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
-import com.android.systemui.statusbar.notification.row.shared.StopwatchContentModel
 import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
+import com.android.systemui.statusbar.notification.row.ui.view.EnRouteView
 import com.android.systemui.statusbar.notification.row.ui.view.TimerView
+import com.android.systemui.statusbar.notification.row.ui.viewbinder.EnRouteViewBinder
 import com.android.systemui.statusbar.notification.row.ui.viewbinder.TimerViewBinder
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.EnRouteViewModel
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.RichOngoingViewModelComponent
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.TimerViewModel
 import javax.inject.Inject
@@ -119,7 +122,15 @@
                     parentView,
                     viewType
                 )
-            is StopwatchContentModel -> TODO("Not yet implemented")
+            is EnRouteContentModel ->
+                inflateEnRouteView(
+                    existingView,
+                    component::createEnRouteViewModel,
+                    systemUiContext,
+                    parentView,
+                    viewType
+                )
+            else -> TODO("Not yet implemented")
         }
     }
 
@@ -131,7 +142,8 @@
         if (RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()) return false
         return when (contentModel) {
             is TimerContentModel -> canKeepTimerView(contentModel, existingView, viewType)
-            is StopwatchContentModel -> TODO("Not yet implemented")
+            is EnRouteContentModel -> canKeepEnRouteView(contentModel, existingView, viewType)
+            else -> TODO("Not yet implemented")
         }
     }
 
@@ -167,4 +179,38 @@
         existingView: View?,
         viewType: RichOngoingNotificationViewType
     ): Boolean = true
+
+    private fun inflateEnRouteView(
+        existingView: View?,
+        createViewModel: () -> EnRouteViewModel,
+        systemUiContext: Context,
+        parentView: ViewGroup,
+        viewType: RichOngoingNotificationViewType,
+    ): ContentViewInflationResult {
+        if (existingView is EnRouteView && !existingView.isReinflateNeeded())
+            return KeepExistingView
+        return when (viewType) {
+            RichOngoingNotificationViewType.Contracted -> {
+                val newView =
+                    LayoutInflater.from(systemUiContext)
+                        .inflate(
+                            R.layout.notification_template_en_route_contracted,
+                            parentView,
+                            /* attachToRoot= */ false
+                        ) as EnRouteView
+
+                InflatedContentViewHolder(newView) {
+                    EnRouteViewBinder.bindWhileAttached(newView, createViewModel())
+                }
+            }
+            RichOngoingNotificationViewType.Expanded,
+            RichOngoingNotificationViewType.HeadsUp -> NullContentView
+        }
+    }
+
+    private fun canKeepEnRouteView(
+        contentModel: EnRouteContentModel,
+        existingView: View?,
+        viewType: RichOngoingNotificationViewType
+    ): Boolean = true
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt
index 4705ace..72823a7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.statusbar.notification.row.domain.interactor
 
 import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
+import com.android.systemui.statusbar.notification.row.shared.EnRouteContentModel
 import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
@@ -26,4 +27,8 @@
     /** Content of a rich ongoing timer notification. */
     val timerContentModel: Flow<TimerContentModel> =
         repository.richOngoingContentModel.filterIsInstance<TimerContentModel>()
+
+    /** Content of a rich ongoing timer notification. */
+    val enRouteContentModel: Flow<EnRouteContentModel> =
+        repository.richOngoingContentModel.filterIsInstance<EnRouteContentModel>()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/EnRouteContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/EnRouteContentModel.kt
new file mode 100644
index 0000000..7e78cca
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/EnRouteContentModel.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.shared
+
+/**
+ * Represents something en route.
+ *
+ * @param smallIcon the main small icon of the EnRoute notification.
+ * @param title the title of the EnRoute notification.
+ * @param text the text of the EnRoute notification.
+ */
+data class EnRouteContentModel(
+    val smallIcon: IconModel,
+    val title: CharSequence?,
+    val text: CharSequence?,
+) : RichOngoingContentModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/EnRouteView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/EnRouteView.kt
new file mode 100644
index 0000000..e5c2b5f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/EnRouteView.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.view
+
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.util.AttributeSet
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import com.android.internal.R
+import com.android.internal.widget.NotificationExpandButton
+
+class EnRouteView
+@JvmOverloads
+constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0,
+    defStyleRes: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
+
+    private val configTracker = ConfigurationTracker(resources)
+
+    private lateinit var icon: ImageView
+    private lateinit var title: TextView
+    private lateinit var text: TextView
+    private lateinit var expandButton: NotificationExpandButton
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        icon = requireViewById(R.id.icon)
+        title = requireViewById(R.id.title)
+        text = requireViewById(R.id.text)
+
+        expandButton = requireViewById(R.id.expand_button)
+        expandButton.setExpanded(false)
+    }
+
+    /** the resources configuration has changed such that the view needs to be reinflated */
+    fun isReinflateNeeded(): Boolean = configTracker.hasUnhandledConfigChange()
+
+    fun setIcon(icon: Icon?) {
+        this.icon.setImageIcon(icon)
+    }
+
+    fun setTitle(title: CharSequence?) {
+        this.title.text = title
+    }
+
+    fun setText(text: CharSequence?) {
+        this.text.text = text
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/EnRouteViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/EnRouteViewBinder.kt
new file mode 100644
index 0000000..3b8957c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/EnRouteViewBinder.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.viewbinder
+
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.notification.row.ui.view.EnRouteView
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.EnRouteViewModel
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+/** Binds a [EnRouteView] to its [view model][EnRouteViewModel]. */
+object EnRouteViewBinder {
+    fun bindWhileAttached(
+        view: EnRouteView,
+        viewModel: EnRouteViewModel,
+    ): DisposableHandle {
+        return view.repeatWhenAttached { lifecycleScope.launch { bind(view, viewModel) } }
+    }
+
+    suspend fun bind(
+        view: EnRouteView,
+        viewModel: EnRouteViewModel,
+    ) = coroutineScope {
+        launch { viewModel.icon.collect { view.setIcon(it) } }
+        launch { viewModel.title.collect { view.setTitle(it) } }
+        launch { viewModel.text.collect { view.setText(it) } }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModel.kt
new file mode 100644
index 0000000..307a983
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModel.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.viewmodel
+
+import android.graphics.drawable.Icon
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.notification.row.domain.interactor.NotificationRowInteractor
+import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+
+/** A view model for EnRoute notifications. */
+class EnRouteViewModel
+@Inject
+constructor(
+    dumpManager: DumpManager,
+    rowInteractor: NotificationRowInteractor,
+) : FlowDumperImpl(dumpManager) {
+    init {
+        /* check if */ RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()
+    }
+
+    val icon: Flow<Icon?> = rowInteractor.enRouteContentModel.mapNotNull { it.smallIcon.icon }
+
+    val title: Flow<CharSequence?> = rowInteractor.enRouteContentModel.map { it.title }
+
+    val text: Flow<CharSequence?> = rowInteractor.enRouteContentModel.map { it.text }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt
index dad52a3..5552d89 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt
@@ -33,4 +33,6 @@
     }
 
     fun createTimerViewModel(): TimerViewModel
+
+    fun createEnRouteViewModel(): EnRouteViewModel
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index 0e984cf..f9937e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -17,7 +17,9 @@
 
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
-import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.ObservableTransitionState.Idle
+import com.android.compose.animation.scene.ObservableTransitionState.Transition
+import com.android.compose.animation.scene.ObservableTransitionState.Transition.ChangeCurrentScene
 import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -66,36 +68,40 @@
         activateFlowDumper()
     }
 
-    private fun expandFractionForScene(scene: SceneKey, shadeExpansion: Float): Float =
-        when (scene) {
+    private fun expandedInScene(scene: SceneKey): Boolean {
+        return when (scene) {
             Scenes.Lockscreen,
-            Scenes.QuickSettings -> 1f
-            else -> shadeExpansion
+            Scenes.Shade,
+            Scenes.QuickSettings -> true
+            else -> false
         }
+    }
 
-    private fun expandFractionForTransition(
-        state: ObservableTransitionState.Transition.ChangeCurrentScene,
+    private fun fullyExpandedDuringSceneChange(change: ChangeCurrentScene): Boolean {
+        // The lockscreen stack is visible during all transitions away from the lockscreen, so keep
+        // the stack expanded until those transitions finish.
+        return (expandedInScene(change.fromScene) && expandedInScene(change.toScene)) ||
+            change.isBetween({ it == Scenes.Lockscreen }, { true })
+    }
+
+    private fun expandFractionDuringSceneChange(
+        change: ChangeCurrentScene,
         shadeExpansion: Float,
-        shadeMode: ShadeMode,
         qsExpansion: Float,
-        quickSettingsScene: SceneKey
-    ): Float =
-        if (
-            state.isBetween({ it == Scenes.Lockscreen }, { it in SceneFamilies.NotifShade }) ||
-                state.isBetween({ it in SceneFamilies.NotifShade }, { it == quickSettingsScene })
-        ) {
+    ): Float {
+        return if (fullyExpandedDuringSceneChange(change)) {
             1f
-        } else if (
-            shadeMode != ShadeMode.Split &&
-                state.isBetween({ it in SceneFamilies.Home }, { it == quickSettingsScene })
-        ) {
+        } else if (change.isBetween({ it == Scenes.Gone }, { it in SceneFamilies.NotifShade })) {
+            shadeExpansion
+        } else if (change.isBetween({ it == Scenes.Gone }, { it == Scenes.QuickSettings })) {
             // during QS expansion, increase fraction at same rate as scrim alpha,
             // but start when scrim alpha is at EXPANSION_FOR_DELAYED_STACK_FADE_IN.
             (qsExpansion / EXPANSION_FOR_MAX_SCRIM_ALPHA - EXPANSION_FOR_DELAYED_STACK_FADE_IN)
                 .coerceIn(0f, 1f)
         } else {
-            shadeExpansion
+            0f
         }
+    }
 
     /**
      * The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning
@@ -109,21 +115,17 @@
                 shadeInteractor.qsExpansion,
                 sceneInteractor.transitionState,
                 sceneInteractor.resolveSceneFamily(SceneFamilies.QuickSettings),
-            ) { shadeExpansion, shadeMode, qsExpansion, transitionState, quickSettingsScene ->
+            ) { shadeExpansion, _, qsExpansion, transitionState, _ ->
                 when (transitionState) {
-                    is ObservableTransitionState.Idle ->
-                        expandFractionForScene(transitionState.currentScene, shadeExpansion)
-                    is ObservableTransitionState.Transition.ChangeCurrentScene ->
-                        expandFractionForTransition(
+                    is Idle -> if (expandedInScene(transitionState.currentScene)) 1f else 0f
+                    is ChangeCurrentScene ->
+                        expandFractionDuringSceneChange(
                             transitionState,
                             shadeExpansion,
-                            shadeMode,
                             qsExpansion,
-                            quickSettingsScene
                         )
-                    is ObservableTransitionState.Transition.ShowOrHideOverlay,
-                    is ObservableTransitionState.Transition.ReplaceOverlay ->
-                        TODO("b/359173565: Handle overlay transitions")
+                    is Transition.ShowOrHideOverlay,
+                    is Transition.ReplaceOverlay -> TODO("b/359173565: Handle overlay transitions")
                 }
             }
             .distinctUntilChanged()
@@ -134,9 +136,7 @@
 
     val shouldResetStackTop: Flow<Boolean> =
         sceneInteractor.transitionState
-            .mapNotNull { state ->
-                state is ObservableTransitionState.Idle && state.currentScene == Scenes.Gone
-            }
+            .mapNotNull { state -> state is Idle && state.currentScene == Scenes.Gone }
             .distinctUntilChanged()
             .dumpWhileCollecting("shouldResetStackTop")
 
@@ -241,7 +241,7 @@
     }
 }
 
-private fun ObservableTransitionState.Transition.ChangeCurrentScene.isBetween(
+private fun ChangeCurrentScene.isBetween(
     a: (SceneKey) -> Boolean,
     b: (SceneKey) -> Boolean
 ): Boolean = (a(fromScene) && b(toScene)) || (b(fromScene) && a(toScene))
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 3dd265b..e3242d1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2365,9 +2365,14 @@
                 // lock screen where users can use the UDFPS affordance to enter the device
                 mStatusBarKeyguardViewManager.reset(true);
             } else if (mState == StatusBarState.KEYGUARD
-                    && !mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()
-                    && mStatusBarKeyguardViewManager.isSecure()) {
-                if (!relockWithPowerButtonImmediately()) {
+                    && !mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()) {
+                boolean needsBouncer = mStatusBarKeyguardViewManager.isSecure();
+                if (relockWithPowerButtonImmediately()) {
+                    // Only request if SIM bouncer is needed
+                    needsBouncer = mStatusBarKeyguardViewManager.needsFullscreenBouncer();
+                }
+
+                if (needsBouncer) {
                     Log.d(TAG, "showBouncerOrLockScreenIfKeyguard, showingBouncer");
                     if (SceneContainerFlag.isEnabled()) {
                         mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
index f16fcb5..d351da6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -95,10 +95,6 @@
         return mode.getIcon(context, iconLoader).await().asIcon()
     }
 
-    suspend fun getLockscreenModeIcon(mode: ZenMode): Icon {
-        return mode.getLockscreenIcon(context, iconLoader).await().asIcon()
-    }
-
     /**
      * Given the list of modes (which may include zero or more currently active modes), returns an
      * icon representing the active mode, if any (or, if multiple modes are active, to the most
@@ -106,8 +102,8 @@
      * standard DND icon for implicit modes, instead of the launcher icon of the associated
      * package).
      */
-    suspend fun getActiveModeIcon(context: Context, modes: List<ZenMode>): Icon? {
-        return getMainActiveMode(modes)?.let { m -> getLockscreenModeIcon(m) }
+    suspend fun getActiveModeIcon(modes: List<ZenMode>): Icon? {
+        return getMainActiveMode(modes)?.let { m -> getModeIcon(m) }
     }
 
     fun activateMode(zenMode: ZenMode) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ActivatableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ActivatableTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
index ad6aca1..3c583f2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
 import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT
 import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
@@ -45,8 +46,8 @@
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyZeroInteractions
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations.initMocks
+import org.mockito.Mockito.`when` as whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -66,6 +67,7 @@
         SensitiveNotificationProtectionController
     @Mock private lateinit var stackController: NotifStackController
     @Mock private lateinit var section: NotifSection
+    @Mock private lateinit var row: ExpandableNotificationRow
 
     @Before
     fun setUp() {
@@ -74,6 +76,8 @@
         whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(false)
 
         entry = NotificationEntryBuilder().setSection(section).build()
+        entry.row = row
+        entry.setSensitive(false, false)
         coordinator =
             StackCoordinator(
                 groupExpansionManagerImpl,
@@ -189,4 +193,17 @@
             .setNotifStats(NotifStats(1, false, false, true, false))
         verifyZeroInteractions(stackController)
     }
+
+    @Test
+    @EnableFlags(
+        FooterViewRefactor.FLAG_NAME
+    )
+    fun testSetNotificationStats_footerFlagOn_nonClearableRedacted() {
+        entry.setSensitive(true, true)
+        whenever(section.bucket).thenReturn(BUCKET_ALERTING)
+        afterRenderListListener.onAfterRenderList(listOf(entry), stackController)
+        verify(activeNotificationsInteractor)
+            .setNotifStats(NotifStats(1, hasNonClearableAlertingNotifs = true, false, false, false))
+        verifyZeroInteractions(stackController)
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelKosmos.kt
new file mode 100644
index 0000000..7e51135
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.viewmodel
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository
+import com.android.systemui.statusbar.notification.row.domain.interactor.getNotificationRowInteractor
+
+fun Kosmos.getEnRouteViewModel(repository: NotificationRowRepository) =
+    EnRouteViewModel(
+        dumpManager = dumpManager,
+        rowInteractor = getNotificationRowInteractor(repository),
+    )
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 58cd2e4..c71b99f 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -331,6 +331,7 @@
     name: "ravenwood-runtime",
     data: [
         "framework-res",
+        "ravenwood-empty-res",
     ],
     libs: [
         "100-framework-minus-apex.ravenwood",
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index 691d06e..7e2ee3e 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -52,10 +52,6 @@
       "host": true
     },
     {
-      "name": "RavenwoodCoreTest",
-      "host": true
-    },
-    {
       "name": "RavenwoodResApkTest",
       "host": true
     },
diff --git a/ravenwood/coretest/Android.bp b/ravenwood/coretest/Android.bp
deleted file mode 100644
index a78c5c1..0000000
--- a/ravenwood/coretest/Android.bp
+++ /dev/null
@@ -1,23 +0,0 @@
-package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "frameworks_base_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["frameworks_base_license"],
-}
-
-android_ravenwood_test {
-    name: "RavenwoodCoreTest",
-
-    static_libs: [
-        "androidx.annotation_annotation",
-        "androidx.test.ext.junit",
-        "androidx.test.rules",
-    ],
-    srcs: [
-        "test/**/*.java",
-    ],
-    sdk_version: "test_current",
-    auto_gen_config: true,
-}
diff --git a/ravenwood/coretest/README.md b/ravenwood/coretest/README.md
deleted file mode 100644
index b60bfbf..0000000
--- a/ravenwood/coretest/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Ravenwood core test
-
-This test contains (non-bivalent) tests for Ravenwood itself -- e.g. tests for the ravenwood rules.
\ No newline at end of file
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodTestRunnerValidationTest.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodTestRunnerValidationTest.java
deleted file mode 100644
index f1e33cb..0000000
--- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodTestRunnerValidationTest.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.ravenwoodtest.coretest;
-
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4; // Intentionally use the deprecated one.
-
-import org.junit.Assume;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-
-/**
- * Test for the test runner validator in RavenwoodRule.
- */
-@RunWith(AndroidJUnit4.class)
-public class RavenwoodTestRunnerValidationTest {
-    // Note the following rules don't have a @Rule, because they need to be applied in a specific
-    // order. So we use a RuleChain instead.
-    private ExpectedException mThrown = ExpectedException.none();
-    private final RavenwoodRule mRavenwood = new RavenwoodRule();
-
-    @Rule
-    public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
-
-    public RavenwoodTestRunnerValidationTest() {
-        Assume.assumeTrue(RavenwoodRule._$RavenwoodPrivate.isOptionalValidationEnabled());
-        // Because RavenwoodRule will throw this error before executing the test method,
-        // we can't do it in the test method itself.
-        // So instead, we initialize it here.
-        mThrown.expectMessage("Switch to androidx.test.ext.junit.runners.AndroidJUnit4");
-    }
-
-    @Test
-    public void testValidateTestRunner() {
-    }
-}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java
deleted file mode 100644
index db95fad..0000000
--- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.ravenwoodtest.coretest.methodvalidation;
-
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-
-/**
- * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
- * This class contains tests for this validator.
- */
-@RunWith(AndroidJUnit4.class)
-public class RavenwoodTestMethodValidation_Fail01_Test {
-    private ExpectedException mThrown = ExpectedException.none();
-    private final RavenwoodRule mRavenwood = new RavenwoodRule();
-
-    @Rule
-    public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
-
-    public RavenwoodTestMethodValidation_Fail01_Test() {
-        mThrown.expectMessage("Method setUp() doesn't have @Before");
-    }
-
-    @SuppressWarnings("JUnit4SetUpNotRun")
-    public void setUp() {
-    }
-
-    @Test
-    public void testEmpty() {
-    }
-}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java
deleted file mode 100644
index ddc66c7..0000000
--- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.ravenwoodtest.coretest.methodvalidation;
-
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-
-/**
- * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
- * This class contains tests for this validator.
- */
-@RunWith(AndroidJUnit4.class)
-public class RavenwoodTestMethodValidation_Fail02_Test {
-    private ExpectedException mThrown = ExpectedException.none();
-    private final RavenwoodRule mRavenwood = new RavenwoodRule();
-
-    @Rule
-    public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
-
-    public RavenwoodTestMethodValidation_Fail02_Test() {
-        mThrown.expectMessage("Method tearDown() doesn't have @After");
-    }
-
-    @SuppressWarnings("JUnit4TearDownNotRun")
-    public void tearDown() {
-    }
-
-    @Test
-    public void testEmpty() {
-    }
-}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java
deleted file mode 100644
index ec8e907..0000000
--- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.ravenwoodtest.coretest.methodvalidation;
-
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-
-/**
- * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
- * This class contains tests for this validator.
- */
-@RunWith(AndroidJUnit4.class)
-public class RavenwoodTestMethodValidation_Fail03_Test {
-    private ExpectedException mThrown = ExpectedException.none();
-    private final RavenwoodRule mRavenwood = new RavenwoodRule();
-
-    @Rule
-    public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
-
-    public RavenwoodTestMethodValidation_Fail03_Test() {
-        mThrown.expectMessage("Method testFoo() doesn't have @Test");
-    }
-
-    @SuppressWarnings("JUnit4TestNotRun")
-    public void testFoo() {
-    }
-
-    @Test
-    public void testEmpty() {
-    }
-}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java
deleted file mode 100644
index d952d07..0000000
--- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.ravenwoodtest.coretest.methodvalidation;
-
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
- * This class contains tests for this validator.
- */
-@RunWith(AndroidJUnit4.class)
-public class RavenwoodTestMethodValidation_OkTest {
-    @Rule
-    public final RavenwoodRule mRavenwood = new RavenwoodRule();
-
-    @Before
-    public void setUp() {
-    }
-
-    @Before
-    public void testSetUp() {
-    }
-
-    @After
-    public void tearDown() {
-    }
-
-    @After
-    public void testTearDown() {
-    }
-
-    @Test
-    public void testEmpty() {
-    }
-}
diff --git a/ravenwood/empty-res/Android.bp b/ravenwood/empty-res/Android.bp
new file mode 100644
index 0000000..3af7690
--- /dev/null
+++ b/ravenwood/empty-res/Android.bp
@@ -0,0 +1,4 @@
+android_app {
+    name: "ravenwood-empty-res",
+    sdk_version: "current",
+}
diff --git a/ravenwood/empty-res/AndroidManifest.xml b/ravenwood/empty-res/AndroidManifest.xml
new file mode 100644
index 0000000..f73460b
--- /dev/null
+++ b/ravenwood/empty-res/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.ravenwood.emptyres">
+</manifest>
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index 3ea4cb7..7b4c173 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -16,9 +16,9 @@
 
 package android.platform.test.ravenwood;
 
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_EMPTY_RESOURCES_APK;
 import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_RESOURCE_APK;
 
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
@@ -39,24 +39,12 @@
 import com.android.internal.os.RuntimeInit;
 import com.android.server.LocalServices;
 
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
 import org.junit.runner.Description;
-import org.junit.runner.RunWith;
 import org.junit.runners.model.Statement;
 
 import java.io.File;
 import java.io.IOException;
 import java.io.PrintStream;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.Executors;
@@ -109,10 +97,6 @@
 
         android.os.Process.init$ravenwood(rule.mUid, rule.mPid);
         android.os.Binder.init$ravenwood();
-//        android.os.SystemProperties.init$ravenwood(
-//                rule.mSystemProperties.getValues(),
-//                rule.mSystemProperties.getKeyReadablePredicate(),
-//                rule.mSystemProperties.getKeyWritablePredicate());
         setSystemProperties(rule.mSystemProperties);
 
         ServiceManager.init$ravenwood();
@@ -131,11 +115,12 @@
 
         // TODO This should be integrated into LoadedApk
         final Supplier<Resources> resourcesSupplier = () -> {
-            final var resApkFile = new File(RAVENWOOD_RESOURCE_APK).getAbsoluteFile();
+            var resApkFile = new File(RAVENWOOD_RESOURCE_APK);
+            if (!resApkFile.isFile()) {
+                resApkFile = new File(RAVENWOOD_EMPTY_RESOURCES_APK);
+            }
             assertTrue(resApkFile.isFile());
-
-            final var res = resApkFile.getAbsolutePath();
-
+            final String res = resApkFile.getAbsolutePath();
             final var emptyPaths = new String[0];
 
             ResourcesManager.getInstance().initializeApplicationPaths(res, emptyPaths);
@@ -243,102 +228,7 @@
 
     public static void validate(Statement base, Description description,
             boolean enableOptionalValidation) {
-        validateTestRunner(base, description, enableOptionalValidation);
-        validateTestAnnotations(base, description, enableOptionalValidation);
-    }
-
-    private static void validateTestRunner(Statement base, Description description,
-            boolean shouldFail) {
-        final var testClass = description.getTestClass();
-        final var runWith = testClass.getAnnotation(RunWith.class);
-        if (runWith == null) {
-            return;
-        }
-
-        // Due to build dependencies, we can't directly refer to androidx classes here,
-        // so just check the class name instead.
-        if (runWith.value().getCanonicalName().equals("androidx.test.runner.AndroidJUnit4")) {
-            var message = "Test " + testClass.getCanonicalName() + " uses deprecated"
-                    + " test runner androidx.test.runner.AndroidJUnit4."
-                    + " Switch to androidx.test.ext.junit.runners.AndroidJUnit4.";
-            if (shouldFail) {
-                Assert.fail(message);
-            } else {
-                System.err.println("Warning: " + message);
-            }
-        }
-    }
-
-    /**
-     * @return if a method has any of annotations.
-     */
-    private static boolean hasAnyAnnotations(Method m, Class<? extends Annotation>... annotations) {
-        for (var anno : annotations) {
-            if (m.getAnnotation(anno) != null) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private static void validateTestAnnotations(Statement base, Description description,
-            boolean enableOptionalValidation) {
-        final var testClass = description.getTestClass();
-
-        final var message = new StringBuilder();
-
-        boolean hasErrors = false;
-        for (Method m : collectMethods(testClass)) {
-            if (Modifier.isPublic(m.getModifiers()) && m.getName().startsWith("test")) {
-                if (!hasAnyAnnotations(m, Test.class, Before.class, After.class,
-                        BeforeClass.class, AfterClass.class)) {
-                    message.append("\nMethod " + m.getName() + "() doesn't have @Test");
-                    hasErrors = true;
-                }
-            }
-            if ("setUp".equals(m.getName())) {
-                if (!hasAnyAnnotations(m, Before.class)) {
-                    message.append("\nMethod " + m.getName() + "() doesn't have @Before");
-                    hasErrors = true;
-                }
-                if (!Modifier.isPublic(m.getModifiers())) {
-                    message.append("\nMethod " + m.getName() + "() must be public");
-                    hasErrors = true;
-                }
-            }
-            if ("tearDown".equals(m.getName())) {
-                if (!hasAnyAnnotations(m, After.class)) {
-                    message.append("\nMethod " + m.getName() + "() doesn't have @After");
-                    hasErrors = true;
-                }
-                if (!Modifier.isPublic(m.getModifiers())) {
-                    message.append("\nMethod " + m.getName() + "() must be public");
-                    hasErrors = true;
-                }
-            }
-        }
-        assertFalse("Problem(s) detected in class " + testClass.getCanonicalName() + ":"
-                + message, hasErrors);
-    }
-
-    /**
-     * Collect all (public or private or any) methods in a class, including inherited methods.
-     */
-    private static List<Method> collectMethods(Class<?> clazz) {
-        var ret = new ArrayList<Method>();
-        collectMethods(clazz, ret);
-        return ret;
-    }
-
-    private static void collectMethods(Class<?> clazz, List<Method> result) {
-        // Class.getMethods() only return public methods, so we need to use getDeclaredMethods()
-        // instead, and recurse.
-        for (var m : clazz.getDeclaredMethods()) {
-            result.add(m);
-        }
-        if (clazz.getSuperclass() != null) {
-            collectMethods(clazz.getSuperclass(), result);
-        }
+        // Nothing to check, for now.
     }
 
     /**
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
index 1298023..9a11a8a 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
@@ -42,12 +42,15 @@
 
     private static final boolean IS_ON_RAVENWOOD = RavenwoodDivergence.isOnRavenwood();
 
-    private static final String RAVEWOOD_RUNTIME_PATH = getRavenwoodRuntimePathInternal();
+    private static final String RAVENWOOD_RUNTIME_PATH = getRavenwoodRuntimePathInternal();
 
     public static final String RAVENWOOD_SYSPROP = "ro.is_on_ravenwood";
 
     public static final String RAVENWOOD_RESOURCE_APK = "ravenwood-res-apks/ravenwood-res.apk";
 
+    public static final String RAVENWOOD_EMPTY_RESOURCES_APK =
+            RAVENWOOD_RUNTIME_PATH + "ravenwood-data/ravenwood-empty-res.apk";
+
     // @GuardedBy("sLock")
     private static boolean sIntegrityChecked = false;
 
@@ -178,7 +181,7 @@
      */
     public static String getRavenwoodRuntimePath() {
         ensureOnRavenwood();
-        return RAVEWOOD_RUNTIME_PATH;
+        return RAVENWOOD_RUNTIME_PATH;
     }
 
     private static String getRavenwoodRuntimePathInternal() {
diff --git a/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java b/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java
index 044239f..b3d3963 100644
--- a/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java
+++ b/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java
@@ -16,6 +16,7 @@
 
 package com.android.ravenwoodtest.servicestest;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 
@@ -62,7 +63,9 @@
         final SerialManager service = (SerialManager)
                 mRavenwood.getContext().getSystemService(Context.SERIAL_SERVICE);
         final String[] ports = service.getSerialPorts();
-        assertEquals(0, ports.length);
+        final String[] refPorts = mRavenwood.getContext().getResources().getStringArray(
+                com.android.internal.R.array.config_serialPorts);
+        assertArrayEquals(refPorts, ports);
     }
 
     @Test
diff --git a/services/core/java/com/android/server/SerialService.java b/services/core/java/com/android/server/SerialService.java
index 82c2038..dbf144f 100644
--- a/services/core/java/com/android/server/SerialService.java
+++ b/services/core/java/com/android/server/SerialService.java
@@ -56,16 +56,11 @@
         }
     }
 
-    @android.ravenwood.annotation.RavenwoodReplace
     private static String[] getSerialPorts(Context context) {
         return context.getResources().getStringArray(
                 com.android.internal.R.array.config_serialPorts);
     }
 
-    private static String[] getSerialPorts$ravenwood(Context context) {
-        return new String[0];
-    }
-
     public static class Lifecycle extends SystemService {
         private SerialService mService;
 
diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java
index e4c65bd2..8c5152f 100644
--- a/services/core/java/com/android/server/app/GameManagerService.java
+++ b/services/core/java/com/android/server/app/GameManagerService.java
@@ -2307,7 +2307,7 @@
                 return;
             }
 
-            final int userId = mContext.getUserId();
+            final int userId = ActivityManager.getCurrentUser();
             final boolean isNotGame = Arrays.stream(packages).noneMatch(
                     p -> isPackageGame(p, userId));
             synchronized (mUidObserverLock) {
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index 008746c..4fa7112 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -122,8 +122,9 @@
             getNotificationShadeSections();
 
     private static List<NotificationSectioner> getNotificationShadeSections() {
+        ArrayList<NotificationSectioner> sectionsList = new ArrayList<>();
         if (android.service.notification.Flags.notificationClassification()) {
-            return List.of(
+            sectionsList.addAll(List.of(
                 new NotificationSectioner("PromotionsSection", 0, (record) ->
                     NotificationChannel.PROMOTIONS_ID.equals(record.getChannel().getId())),
                 new NotificationSectioner("SocialSection", 0, (record) ->
@@ -131,18 +132,36 @@
                 new NotificationSectioner("NewsSection", 0, (record) ->
                     NotificationChannel.NEWS_ID.equals(record.getChannel().getId())),
                 new NotificationSectioner("RecsSection", 0, (record) ->
-                    NotificationChannel.RECS_ID.equals(record.getChannel().getId())),
-                new NotificationSectioner("AlertingSection", 0, (record) ->
-                    record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT),
-                new NotificationSectioner("SilentSection", 1, (record) ->
-                    record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT));
-        } else {
-            return List.of(
-                new NotificationSectioner("AlertingSection", 0, (record) ->
-                    record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT),
-                new NotificationSectioner("SilentSection", 1, (record) ->
-                    record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT));
+                    NotificationChannel.RECS_ID.equals(record.getChannel().getId()))));
         }
+
+        if (Flags.notificationForceGroupConversations()) {
+            // add priority people section
+            sectionsList.add(new NotificationSectioner("PeopleSection(priority)", 1, (record) ->
+                    record.isConversation() && record.getChannel().isImportantConversation()));
+
+            if (android.app.Flags.sortSectionByTime()) {
+                // add single people (alerting) section
+                sectionsList.add(new NotificationSectioner("PeopleSection", 0,
+                        NotificationRecord::isConversation));
+            } else {
+                // add people alerting section
+                sectionsList.add(new NotificationSectioner("PeopleSection(alerting)", 1, (record) ->
+                        record.isConversation()
+                        && record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT));
+                // add people silent section
+                sectionsList.add(new NotificationSectioner("PeopleSection(silent)", 1, (record) ->
+                        record.isConversation()
+                        && record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT));
+            }
+        }
+
+        sectionsList.addAll(List.of(
+            new NotificationSectioner("AlertingSection", 0, (record) ->
+                record.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT),
+            new NotificationSectioner("SilentSection", 1, (record) ->
+                record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT)));
+        return sectionsList;
     }
 
     public GroupHelper(Context context, PackageManager packageManager, int autoGroupAtCount,
@@ -830,61 +849,19 @@
                 }
             }
 
+            // The list of notification operations required after the channel update
             final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>();
 
-            final Set<FullyQualifiedGroupKey> oldGroups =
-                    new HashSet<>(mAggregatedNotifications.keySet());
-            for (FullyQualifiedGroupKey oldFullAggKey : oldGroups) {
-                // Only check aggregate groups that match the same userId & packageName
-                if (pkgName.equals(oldFullAggKey.pkg) && userId == oldFullAggKey.userId) {
-                    final ArrayMap<String, NotificationAttributes> notificationsInAggGroup =
-                            mAggregatedNotifications.get(oldFullAggKey);
-                    if (notificationsInAggGroup == null) {
-                        continue;
-                    }
+            // Check any already auto-grouped notifications that may need to be re-grouped
+            // after the channel update
+            notificationsToMove.addAll(
+                    getAutogroupedNotificationsMoveOps(userId, pkgName,
+                        notificationsToCheck));
 
-                    FullyQualifiedGroupKey newFullAggregateGroupKey = null;
-                    for (String key : notificationsInAggGroup.keySet()) {
-                        if (notificationsToCheck.get(key) != null) {
-                            // check if section changes
-                            NotificationSectioner sectioner = getSection(
-                                    notificationsToCheck.get(key));
-                            if (sectioner == null) {
-                                continue;
-                            }
-                            newFullAggregateGroupKey = new FullyQualifiedGroupKey(userId, pkgName,
-                                    sectioner);
-                            if (!oldFullAggKey.equals(newFullAggregateGroupKey)) {
-                                if (DEBUG) {
-                                    Log.i(TAG, "Change section on channel update: " + key);
-                                }
-                                notificationsToMove.add(
-                                        new NotificationMoveOp(notificationsToCheck.get(key),
-                                            oldFullAggKey, newFullAggregateGroupKey));
-                            }
-                        }
-                    }
-
-                    if (newFullAggregateGroupKey != null) {
-                        // Add any notifications left ungrouped to the new section
-                        ArrayMap<String, NotificationAttributes> ungrouped =
-                            mUngroupedAbuseNotifications.get(newFullAggregateGroupKey);
-                        if (ungrouped != null) {
-                            for (NotificationRecord r : notificationList) {
-                                if (ungrouped.containsKey(r.getKey())) {
-                                    if (DEBUG) {
-                                        Log.i(TAG, "Add previously ungrouped: " + r);
-                                    }
-                                    notificationsToMove.add(
-                                        new NotificationMoveOp(r, null, newFullAggregateGroupKey));
-                                }
-                            }
-                            //Cleanup mUngroupedAbuseNotifications
-                            mUngroupedAbuseNotifications.remove(newFullAggregateGroupKey);
-                        }
-                    }
-                }
-            }
+            // Check any ungrouped notifications that may need to be auto-grouped
+            // after the channel update
+            notificationsToMove.addAll(
+                    getUngroupedNotificationsMoveOps(userId, pkgName, notificationsToCheck));
 
             // Batch move to new section
             if (!notificationsToMove.isEmpty()) {
@@ -894,10 +871,103 @@
     }
 
     @GuardedBy("mAggregatedNotifications")
+    private List<NotificationMoveOp> getAutogroupedNotificationsMoveOps(int userId, String pkgName,
+            ArrayMap<String, NotificationRecord> notificationsToCheck) {
+        final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>();
+        final Set<FullyQualifiedGroupKey> oldGroups =
+                new HashSet<>(mAggregatedNotifications.keySet());
+        // Move auto-grouped updated notifications from the old groups to the new groups (section)
+        for (FullyQualifiedGroupKey oldFullAggKey : oldGroups) {
+            // Only check aggregate groups that match the same userId & packageName
+            if (pkgName.equals(oldFullAggKey.pkg) && userId == oldFullAggKey.userId) {
+                final ArrayMap<String, NotificationAttributes> notificationsInAggGroup =
+                        mAggregatedNotifications.get(oldFullAggKey);
+                if (notificationsInAggGroup == null) {
+                    continue;
+                }
+
+                FullyQualifiedGroupKey newFullAggregateGroupKey = null;
+                for (String key : notificationsInAggGroup.keySet()) {
+                    if (notificationsToCheck.get(key) != null) {
+                        // check if section changes
+                        NotificationSectioner sectioner = getSection(notificationsToCheck.get(key));
+                        if (sectioner == null) {
+                            continue;
+                        }
+                        newFullAggregateGroupKey = new FullyQualifiedGroupKey(userId, pkgName,
+                                sectioner);
+                        if (!oldFullAggKey.equals(newFullAggregateGroupKey)) {
+                            if (DEBUG) {
+                                Log.i(TAG, "Change section on channel update: " + key);
+                            }
+                            notificationsToMove.add(
+                                    new NotificationMoveOp(notificationsToCheck.get(key),
+                                        oldFullAggKey, newFullAggregateGroupKey));
+                            notificationsToCheck.remove(key);
+                        }
+                    }
+                }
+            }
+        }
+        return notificationsToMove;
+    }
+
+    @GuardedBy("mAggregatedNotifications")
+    private List<NotificationMoveOp> getUngroupedNotificationsMoveOps(int userId, String pkgName,
+            final ArrayMap<String, NotificationRecord> notificationsToCheck) {
+        final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>();
+        // Move any remaining ungrouped updated notifications from the old ungrouped list
+        // to the new ungrouped section list, if necessary
+        if (!notificationsToCheck.isEmpty()) {
+            final Set<FullyQualifiedGroupKey> oldUngroupedSectionKeys =
+                    new HashSet<>(mUngroupedAbuseNotifications.keySet());
+            for (FullyQualifiedGroupKey oldFullAggKey : oldUngroupedSectionKeys) {
+                // Only check aggregate groups that match the same userId & packageName
+                if (pkgName.equals(oldFullAggKey.pkg) && userId == oldFullAggKey.userId) {
+                    final ArrayMap<String, NotificationAttributes> ungroupedOld =
+                            mUngroupedAbuseNotifications.get(oldFullAggKey);
+                    if (ungroupedOld == null) {
+                        continue;
+                    }
+
+                    FullyQualifiedGroupKey newFullAggregateGroupKey = null;
+                    final Set<String> ungroupedKeys = new HashSet<>(ungroupedOld.keySet());
+                    for (String key : ungroupedKeys) {
+                        NotificationRecord record = notificationsToCheck.get(key);
+                        if (record != null) {
+                            // check if section changes
+                            NotificationSectioner sectioner = getSection(record);
+                            if (sectioner == null) {
+                                continue;
+                            }
+                            newFullAggregateGroupKey = new FullyQualifiedGroupKey(userId, pkgName,
+                                    sectioner);
+                            if (!oldFullAggKey.equals(newFullAggregateGroupKey)) {
+                                if (DEBUG) {
+                                    Log.i(TAG, "Change ungrouped section: " + key);
+                                }
+                                notificationsToMove.add(
+                                        new NotificationMoveOp(record, oldFullAggKey,
+                                            newFullAggregateGroupKey));
+                                notificationsToCheck.remove(key);
+                                //Remove from previous ungrouped list
+                                ungroupedOld.remove(key);
+                            }
+                        }
+                    }
+                    mUngroupedAbuseNotifications.put(oldFullAggKey, ungroupedOld);
+                }
+            }
+        }
+        return notificationsToMove;
+    }
+
+    @GuardedBy("mAggregatedNotifications")
     private void moveNotificationsToNewSection(final int userId, final String pkgName,
             final List<NotificationMoveOp> notificationsToMove) {
         record GroupUpdateOp(FullyQualifiedGroupKey groupKey, NotificationRecord record,
                              boolean hasSummary) { }
+        // Bundled operations to apply to groups affected by the channel update
         ArrayMap<FullyQualifiedGroupKey, GroupUpdateOp> groupsToUpdate = new ArrayMap<>();
 
         for (NotificationMoveOp moveOp: notificationsToMove) {
@@ -923,35 +993,36 @@
                 // Only add once, for triggering notification
                 if (!groupsToUpdate.containsKey(oldFullAggregateGroupKey)) {
                     groupsToUpdate.put(oldFullAggregateGroupKey,
-                            new GroupUpdateOp(oldFullAggregateGroupKey, record, true));
+                        new GroupUpdateOp(oldFullAggregateGroupKey, record, true));
                 }
             }
 
-            // Add/update aggregate summary for new group
+            // Add moved notifications to the ungrouped list for new group and do grouping
+            // after all notifications have been handled
             if (newFullAggregateGroupKey != null) {
                 final ArrayMap<String, NotificationAttributes> newAggregatedNotificationsAttrs =
                         mAggregatedNotifications.getOrDefault(newFullAggregateGroupKey,
                             new ArrayMap<>());
-                boolean newGroupExists = !newAggregatedNotificationsAttrs.isEmpty();
-                newAggregatedNotificationsAttrs.put(record.getKey(),
-                        new NotificationAttributes(record.getFlags(),
-                            record.getNotification().getSmallIcon(),
-                            record.getNotification().color,
-                            record.getNotification().visibility,
-                            record.getNotification().getGroupAlertBehavior(),
-                            record.getChannel().getId()));
-                mAggregatedNotifications.put(newFullAggregateGroupKey,
-                        newAggregatedNotificationsAttrs);
+                boolean hasSummary = !newAggregatedNotificationsAttrs.isEmpty();
+                ArrayMap<String, NotificationAttributes> ungrouped =
+                        mUngroupedAbuseNotifications.getOrDefault(newFullAggregateGroupKey,
+                            new ArrayMap<>());
+                ungrouped.put(record.getKey(), new NotificationAttributes(
+                        record.getFlags(),
+                        record.getNotification().getSmallIcon(),
+                        record.getNotification().color,
+                        record.getNotification().visibility,
+                        record.getNotification().getGroupAlertBehavior(),
+                        record.getChannel().getId()));
+                mUngroupedAbuseNotifications.put(newFullAggregateGroupKey, ungrouped);
+
+                record.setOverrideGroupKey(null);
 
                 // Only add once, for triggering notification
                 if (!groupsToUpdate.containsKey(newFullAggregateGroupKey)) {
                     groupsToUpdate.put(newFullAggregateGroupKey,
-                            new GroupUpdateOp(newFullAggregateGroupKey, record, newGroupExists));
+                        new GroupUpdateOp(newFullAggregateGroupKey, record, hasSummary));
                 }
-
-                // Add notification to new group. do not request resort
-                record.setOverrideGroupKey(null);
-                mCallback.addAutoGroup(record.getKey(), newFullAggregateGroupKey.toString(), false);
             }
         }
 
@@ -959,18 +1030,26 @@
         for (FullyQualifiedGroupKey groupKey : groupsToUpdate.keySet()) {
             final ArrayMap<String, NotificationAttributes> aggregatedNotificationsAttrs =
                     mAggregatedNotifications.getOrDefault(groupKey, new ArrayMap<>());
-            if (aggregatedNotificationsAttrs.isEmpty()) {
-                mCallback.removeAutoGroupSummary(userId, pkgName, groupKey.toString());
-                mAggregatedNotifications.remove(groupKey);
-            } else {
-                NotificationRecord triggeringNotification = groupsToUpdate.get(groupKey).record;
-                boolean hasSummary = groupsToUpdate.get(groupKey).hasSummary;
+            final ArrayMap<String, NotificationAttributes> ungrouped =
+                    mUngroupedAbuseNotifications.getOrDefault(groupKey, new ArrayMap<>());
+
+            NotificationRecord triggeringNotification = groupsToUpdate.get(groupKey).record;
+            boolean hasSummary = groupsToUpdate.get(groupKey).hasSummary;
+            //Group needs to be created/updated
+            if (ungrouped.size() >= mAutoGroupAtCount
+                    || (hasSummary && !aggregatedNotificationsAttrs.isEmpty())) {
                 NotificationSectioner sectioner = getSection(triggeringNotification);
                 if (sectioner == null) {
                     continue;
                 }
-                updateAggregateAppGroup(groupKey, triggeringNotification.getKey(), hasSummary,
-                        sectioner.mSummaryId);
+                aggregateUngroupedNotifications(groupKey, triggeringNotification.getKey(),
+                        ungrouped, hasSummary, sectioner.mSummaryId);
+            } else {
+                // Remove empty groups
+                if (aggregatedNotificationsAttrs.isEmpty() && hasSummary) {
+                    mCallback.removeAutoGroupSummary(userId, pkgName, groupKey.toString());
+                    mAggregatedNotifications.remove(groupKey);
+                }
             }
         }
     }
@@ -1327,8 +1406,10 @@
         }
 
         private boolean isNotificationGroupable(final NotificationRecord record) {
-            if (record.isConversation()) {
-                return false;
+            if (!Flags.notificationForceGroupConversations()) {
+                if (record.isConversation()) {
+                    return false;
+                }
             }
 
             Notification notification = record.getSbn().getNotification();
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 06d1285..6ff0e04 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -765,7 +765,9 @@
             try {
                 ApplicationInfo applicationInfo = mPm.getApplicationInfo(pkg, 0);
                 rule.name = applicationInfo.loadLabel(mPm).toString();
-                rule.iconResName = drawableResIdToResName(pkg, applicationInfo.icon);
+                if (!Flags.modesUi()) {
+                    rule.iconResName = drawableResIdToResName(pkg, applicationInfo.icon);
+                }
             } 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");
@@ -1750,6 +1752,15 @@
                                     manualRulePolicy.overwrittenWith(automaticRule.zenPolicy);
                         }
                     }
+
+                    if (Flags.modesApi() && Flags.modesUi()
+                            && config.version < ZenModeConfig.XML_VERSION_MODES_UI) {
+                        // Clear icons from implicit rules. App icons are not suitable for some
+                        // surfaces, so juse use a default (the user can select a different one).
+                        if (ZenModeConfig.isImplicitRuleId(automaticRule.id)) {
+                            automaticRule.iconResName = null;
+                        }
+                    }
                 }
             }
 
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 7265cff..aac2c40 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -149,3 +149,10 @@
   description: "This flag enables forced auto-grouping singleton groups"
   bug: "336488844"
 }
+
+flag {
+  name: "notification_force_group_conversations"
+  namespace: "systemui"
+  description: "This flag enables forced auto-grouping conversations"
+  bug: "336488844"
+}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 2294d65..47d4740 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -233,7 +233,6 @@
 import static com.android.server.wm.StartingData.AFTER_TRANSACTION_REMOVE_DIRECTLY;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION;
 import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_VISIBLE;
 import static com.android.server.wm.TaskPersister.DEBUG;
@@ -5542,10 +5541,7 @@
             return false;
         }
         if (!mDisplayContent.mAppTransition.isTransitionSet()) {
-            // Defer committing visibility for non-home app which is animating by recents.
-            if (isActivityTypeHome() || !isAnimating(PARENTS, ANIMATION_TYPE_RECENTS)) {
-                return false;
-            }
+            return false;
         }
         if (mWaitForEnteringPinnedMode && mVisible == visible) {
             // If the visibility is not changed during enter PIP, we don't want to include it in
@@ -5699,8 +5695,7 @@
     private void postApplyAnimation(boolean visible, boolean fromTransition) {
         final boolean usingShellTransitions = mTransitionController.isShellTransitionsEnabled();
         final boolean delayed = !usingShellTransitions && isAnimating(PARENTS | CHILDREN,
-                ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_WINDOW_ANIMATION
-                        | ANIMATION_TYPE_RECENTS);
+                ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_WINDOW_ANIMATION);
         if (!delayed && !usingShellTransitions) {
             // We aren't delayed anything, but exiting windows rely on the animation finished
             // callback being called in case the ActivityRecord was pretending to be delayed,
@@ -5722,7 +5717,7 @@
         // animation and aren't in RESUMED state. Otherwise, we'll update client visibility in
         // onAnimationFinished or activityStopped.
         if (visible || (mState != RESUMED && (usingShellTransitions || !isAnimating(
-                PARENTS, ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS)))) {
+                PARENTS, ANIMATION_TYPE_APP_TRANSITION)))) {
             setClientVisible(visible);
         }
 
@@ -7700,7 +7695,7 @@
                 // Ensure that the activity content is hidden when the decor surface is boosted to
                 // prevent UI redressing attack.
                 && !isDecorSurfaceBoosted)
-                || isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS
+                || isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION
                         | ANIMATION_TYPE_PREDICT_BACK);
 
         if (mSurfaceControl != null) {
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index 06bdc04..197bd5a 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -68,7 +68,6 @@
 import static com.android.server.wm.NonAppWindowAnimationAdapter.shouldAttachNavBarToApp;
 import static com.android.server.wm.NonAppWindowAnimationAdapter.shouldStartNonAppWindowAnimationsForKeyguardExit;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
 import static com.android.server.wm.WallpaperAnimationAdapter.shouldStartWallpaperAnimation;
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -173,41 +172,6 @@
                 || !transitionGoodToGoForTaskFragments()) {
             return;
         }
-        final boolean isRecentsInOpening = mDisplayContent.mOpeningApps.stream().anyMatch(
-                ConfigurationContainer::isActivityTypeRecents);
-        // In order to avoid visual clutter caused by a conflict between app transition
-        // animation and recents animation, app transition is delayed until recents finishes.
-        // One exceptional case. When 3P launcher is used and a user taps a task screenshot in
-        // task switcher (isRecentsInOpening=true), app transition must start even though
-        // recents is running. Otherwise app transition is blocked until timeout (b/232984498).
-        // When 1P launcher is used, this animation is controlled by the launcher outside of
-        // the app transition, so delaying app transition doesn't cause visible delay. After
-        // recents finishes, app transition is handled just to commit visibility on apps.
-        if (!isRecentsInOpening) {
-            final ArraySet<WindowContainer> participants = new ArraySet<>();
-            participants.addAll(mDisplayContent.mOpeningApps);
-            participants.addAll(mDisplayContent.mChangingContainers);
-            boolean deferForRecents = false;
-            for (int i = 0; i < participants.size(); i++) {
-                WindowContainer wc = participants.valueAt(i);
-                final ActivityRecord activity = getAppFromContainer(wc);
-                if (activity == null) {
-                    continue;
-                }
-                // Don't defer recents animation if one of activity isn't running for it, that one
-                // might be started from quickstep.
-                if (!activity.isAnimating(PARENTS, ANIMATION_TYPE_RECENTS)) {
-                    deferForRecents = false;
-                    break;
-                }
-                deferForRecents = true;
-            }
-            if (deferForRecents) {
-                ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
-                        "Delaying app transition for recents animation to finish");
-                return;
-            }
-        }
 
         Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "AppTransitionReady");
 
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java
index 9cfd396..57f9be0 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimator.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java
@@ -32,8 +32,8 @@
 import android.view.SurfaceControl.Transaction;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.common.LogLevel;
 import com.android.internal.protolog.ProtoLog;
+import com.android.internal.protolog.common.LogLevel;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
@@ -585,7 +585,6 @@
             ANIMATION_TYPE_APP_TRANSITION,
             ANIMATION_TYPE_SCREEN_ROTATION,
             ANIMATION_TYPE_DIMMER,
-            ANIMATION_TYPE_RECENTS,
             ANIMATION_TYPE_WINDOW_ANIMATION,
             ANIMATION_TYPE_INSETS_CONTROL,
             ANIMATION_TYPE_TOKEN_TRANSFORM,
@@ -604,7 +603,6 @@
             case ANIMATION_TYPE_APP_TRANSITION: return "app_transition";
             case ANIMATION_TYPE_SCREEN_ROTATION: return "screen_rotation";
             case ANIMATION_TYPE_DIMMER: return "dimmer";
-            case ANIMATION_TYPE_RECENTS: return "recents_animation";
             case ANIMATION_TYPE_WINDOW_ANIMATION: return "window_animation";
             case ANIMATION_TYPE_INSETS_CONTROL: return "insets_animation";
             case ANIMATION_TYPE_TOKEN_TRANSFORM: return "token_transform";
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index f000223..21be0fc 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -94,7 +94,6 @@
 import static com.android.server.wm.LockTaskController.LOCK_TASK_AUTH_LAUNCHABLE;
 import static com.android.server.wm.LockTaskController.LOCK_TASK_AUTH_LAUNCHABLE_PRIV;
 import static com.android.server.wm.LockTaskController.LOCK_TASK_AUTH_PINNABLE;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
 import static com.android.server.wm.TaskProto.AFFINITY;
 import static com.android.server.wm.TaskProto.BOUNDS;
 import static com.android.server.wm.TaskProto.CREATED_BY_ORGANIZER;
@@ -2966,8 +2965,7 @@
 
     /** Checking if self or its child tasks are animated by recents animation. */
     boolean isAnimatingByRecents() {
-        return isAnimating(CHILDREN, ANIMATION_TYPE_RECENTS)
-                || mTransitionController.isTransientHide(this);
+        return mTransitionController.isTransientHide(this);
     }
 
     WindowState getTopVisibleAppMainWindow() {
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index 03342d3..13334a5 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -19,7 +19,6 @@
 import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_TRANSACTIONS;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_SCREEN_ROTATION;
 import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN;
 import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
@@ -218,8 +217,8 @@
     private void updateRunningExpensiveAnimationsLegacy() {
         final boolean runningExpensiveAnimations =
                 mService.mRoot.isAnimating(TRANSITION | CHILDREN /* flags */,
-                        ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_SCREEN_ROTATION
-                                | ANIMATION_TYPE_RECENTS /* typesToCheck */);
+                        ANIMATION_TYPE_APP_TRANSITION
+                                | ANIMATION_TYPE_SCREEN_ROTATION /* typesToCheck */);
         if (runningExpensiveAnimations && !mRunningExpensiveAnimations) {
             mService.mSnapshotController.setPause(true);
             mTransaction.setEarlyWakeupStart();
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index a980b77..6995027 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -51,7 +51,6 @@
 import static com.android.server.wm.IdentifierProto.USER_ID;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
 import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN;
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
 import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
@@ -1242,8 +1241,7 @@
      */
     boolean inTransitionSelfOrParent() {
         if (!mTransitionController.isShellTransitionsEnabled()) {
-            return isAnimating(PARENTS | TRANSITION,
-                    ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS);
+            return isAnimating(PARENTS | TRANSITION, ANIMATION_TYPE_APP_TRANSITION);
         }
         return inTransition();
     }
diff --git a/services/core/java/com/android/server/wm/WindowContainerThumbnail.java b/services/core/java/com/android/server/wm/WindowContainerThumbnail.java
index ad88062..80f3c44 100644
--- a/services/core/java/com/android/server/wm/WindowContainerThumbnail.java
+++ b/services/core/java/com/android/server/wm/WindowContainerThumbnail.java
@@ -20,7 +20,7 @@
 import static android.view.SurfaceControl.METADATA_WINDOW_TYPE;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_TRANSACTIONS;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
+import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.WindowContainerThumbnailProto.HEIGHT;
 import static com.android.server.wm.WindowContainerThumbnailProto.SURFACE_ANIMATOR;
 import static com.android.server.wm.WindowContainerThumbnailProto.WIDTH;
@@ -118,7 +118,7 @@
                         mWindowContainer.getDisplayContent().mAppTransition.canSkipFirstFrame(),
                         mWindowContainer.getDisplayContent().getWindowCornerRadius()),
                 mWindowContainer.mWmService.mSurfaceAnimationRunner), false /* hidden */,
-                ANIMATION_TYPE_RECENTS);
+                ANIMATION_TYPE_APP_TRANSITION);
     }
 
     private void onAnimationFinished(@AnimationType int type, AnimationAdapter anim) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index eeda2a7..9b55ed2 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -129,7 +129,6 @@
 import static com.android.server.wm.SensitiveContentPackages.PackageInfo;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION;
 import static com.android.server.wm.WindowContainer.AnimationFlags.CHILDREN;
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
@@ -2725,8 +2724,7 @@
                         win.mTransitionController.mAnimatingExitWindows.add(win);
                         reason = "inTransition";
                     }
-                } else if (win.isAnimating(PARENTS | TRANSITION,
-                        ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS)) {
+                } else if (win.isAnimating(PARENTS | TRANSITION, ANIMATION_TYPE_APP_TRANSITION)) {
                     // Already animating as part of a legacy app-transition.
                     reason = "inLegacyTransition";
                 }
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 81bce18..c8e4b0a 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -128,7 +128,6 @@
 import static com.android.server.wm.MoveAnimationSpecProto.TO;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_ALL;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_STARTING_REVEAL;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION;
 import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
@@ -581,7 +580,7 @@
      * is guaranteed to be cleared.
      */
     static final int EXIT_ANIMATING_TYPES = ANIMATION_TYPE_APP_TRANSITION
-            | ANIMATION_TYPE_WINDOW_ANIMATION | ANIMATION_TYPE_RECENTS;
+            | ANIMATION_TYPE_WINDOW_ANIMATION;
 
     /** Currently running an exit animation? */
     boolean mAnimatingExit;
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
index 0787058..2c78504 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -33,11 +33,15 @@
 import android.content.res.Configuration;
 import android.graphics.Insets;
 import android.os.RemoteException;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 import android.util.Log;
 import android.view.WindowManagerGlobal;
 import android.view.WindowManagerPolicyConstants;
 import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.Flags;
 import android.view.inputmethod.InputMethodManager;
 
 import androidx.annotation.NonNull;
@@ -56,6 +60,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -89,6 +94,9 @@
     private String mInputMethodId;
     private boolean mShowImeWithHardKeyboardEnabled;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Before
     public void setUp() throws Exception {
         mInstrumentation = InstrumentationRegistry.getInstrumentation();
@@ -155,7 +163,13 @@
                 () -> assertThat(mUiDevice.pressHome()).isTrue(),
                 true /* expected */,
                 false /* inputViewStarted */);
-        assertThat(mInputMethodService.isInputViewShown()).isFalse();
+        if (Flags.refactorInsetsController()) {
+            // The IME visibility is only sent at the end of the animation. Therefore, we have to
+            // wait until the visibility was sent to the server and the IME window hidden.
+            eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
+        } else {
+            assertThat(mInputMethodService.isInputViewShown()).isFalse();
+        }
     }
 
     /**
@@ -182,8 +196,13 @@
 
     /**
      * This checks the result of calling IMS#requestShowSelf and IMS#requestHideSelf.
+     *
+     * With the refactor in b/298172246, all calls to IMMS#{show,hide}MySoftInputLocked
+     * will be just apply the requested visibility (by using the callback). Therefore, we will
+     * lose flags like HIDE_IMPLICIT_ONLY.
      */
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testShowHideSelf() throws Exception {
         setShowImeWithHardKeyboard(true /* enabled */);
 
@@ -375,8 +394,13 @@
     /**
      * This checks that an implicit show request when the IME is not previously shown,
      * and it should be shown in fullscreen mode, results in the IME not being shown.
+     *
+     * With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput
+     * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like
+     * SHOW_IMPLICIT.
      */
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testShowSoftInputImplicitly_fullScreenMode() throws Exception {
         setShowImeWithHardKeyboard(true /* enabled */);
 
@@ -425,8 +449,13 @@
     /**
      * This checks that an implicit show request when a hard keyboard is connected,
      * results in the IME not being shown.
+     *
+     * With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput
+     * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like
+     * SHOW_IMPLICIT.
      */
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testShowSoftInputImplicitly_withHardKeyboard() throws Exception {
         setShowImeWithHardKeyboard(false /* enabled */);
 
@@ -484,8 +513,13 @@
      * This checks that an implicit show request followed by connecting a hard keyboard
      * and a configuration change, does not trigger IMS#onFinishInputView,
      * but results in the IME being hidden.
+     *
+     * With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput
+     * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like
+     * SHOW_IMPLICIT.
      */
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testShowSoftInputImplicitly_thenConfigurationChanged() throws Exception {
         setShowImeWithHardKeyboard(false /* enabled */);
 
@@ -567,8 +601,13 @@
      * This checks that a forced show request directly followed by an explicit show request,
      * and then a hide not always request, still results in the IME being shown
      * (i.e. the explicit show request retains the forced state).
+     *
+     * With the refactor in b/298172246, all calls from InputMethodManager#{show,hide}SoftInput
+     * will be redirected to InsetsController#{show,hide}. Therefore, we will lose flags like
+     * HIDE_NOT_ALWAYS.
      */
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testShowSoftInputForced_testShowSoftInputExplicitly_thenHideSoftInputNotAlways()
             throws Exception {
         setShowImeWithHardKeyboard(true /* enabled */);
@@ -734,7 +773,13 @@
         backButtonUiObject.click();
         mInstrumentation.waitForIdleSync();
 
-        assertThat(mInputMethodService.isInputViewShown()).isFalse();
+        if (Flags.refactorInsetsController()) {
+            // The IME visibility is only sent at the end of the animation. Therefore, we have to
+            // wait until the visibility was sent to the server and the IME window hidden.
+            eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
+        } else {
+            assertThat(mInputMethodService.isInputViewShown()).isFalse();
+        }
     }
 
     /**
@@ -766,7 +811,13 @@
         backButtonUiObject.longClick();
         mInstrumentation.waitForIdleSync();
 
-        assertThat(mInputMethodService.isInputViewShown()).isFalse();
+        if (Flags.refactorInsetsController()) {
+            // The IME visibility is only sent at the end of the animation. Therefore, we have to
+            // wait until the visibility was sent to the server and the IME window hidden.
+            eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
+        } else {
+            assertThat(mInputMethodService.isInputViewShown()).isFalse();
+        }
     }
 
     /**
@@ -848,7 +899,13 @@
         assertWithMessage("Input Method Switcher Menu is shown")
                 .that(isInputMethodPickerShown(imm))
                 .isTrue();
-        assertThat(mInputMethodService.isInputViewShown()).isTrue();
+        if (Flags.refactorInsetsController()) {
+            // The IME visibility is only sent at the end of the animation. Therefore, we have to
+            // wait until the visibility was sent to the server and the IME window hidden.
+            eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse());
+        } else {
+            assertThat(mInputMethodService.isInputViewShown()).isTrue();
+        }
 
         // Hide the Picker menu before finishing.
         mUiDevice.pressBack();
diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
index 0703db2..18811de 100644
--- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java
@@ -223,6 +223,11 @@
             mShutDownActionReceiver = receiver;
             return null;
         }
+
+        @Override
+        public int getUserId() {
+            return 0;
+        }
     }
 
     @Before
@@ -237,7 +242,7 @@
         mPackageCategories = new HashMap<>();
         mPackageUids = new HashMap<>();
         mPackageName = mMockContext.getPackageName();
-        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_GAME);
+        mockAppCategory(mPackageName, DEFAULT_PACKAGE_UID, ApplicationInfo.CATEGORY_GAME, -1);
         LocalServices.addService(PowerManagerInternal.class, mMockPowerManager);
 
         mSetFlagsRule.enableFlags(Flags.FLAG_GAME_DEFAULT_FRAME_RATE);
@@ -245,7 +250,12 @@
     }
 
     private void mockAppCategory(String packageName, int packageUid,
-            @ApplicationInfo.Category int category)
+            @ApplicationInfo.Category int category) throws Exception {
+        mockAppCategory(packageName, packageUid, category, -1 /*userId*/);
+    }
+
+    private void mockAppCategory(String packageName, int packageUid,
+            @ApplicationInfo.Category int category, int userId)
             throws Exception {
         reset(mMockPackageManager);
         mPackageCategories.put(packageName, category);
@@ -259,8 +269,15 @@
             ApplicationInfo applicationInfo = new ApplicationInfo();
             applicationInfo.packageName = packageName;
             applicationInfo.category = category;
-            when(mMockPackageManager.getApplicationInfoAsUser(eq(packageName), anyInt(), anyInt()))
-                    .thenReturn(applicationInfo);
+            if (userId == -1) {
+                when(mMockPackageManager.getApplicationInfoAsUser(eq(packageName), anyInt(),
+                        anyInt()))
+                        .thenReturn(applicationInfo);
+            } else {
+                when(mMockPackageManager.getApplicationInfoAsUser(eq(packageName), anyInt(),
+                        eq(userId)))
+                        .thenReturn(applicationInfo);
+            }
 
             final PackageInfo pi = new PackageInfo();
             pi.packageName = packageName;
@@ -2331,10 +2348,12 @@
 
     @Test
     public void testGamePowerMode_twoGames() throws Exception {
-        GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+        GameManagerService gameManagerService = new GameManagerService(mMockContext,
+                mTestLooper.getLooper());
         String someGamePkg = "some.game";
         int somePackageId = DEFAULT_PACKAGE_UID + 1;
-        mockAppCategory(someGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME);
+        mockAppCategory(someGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME,
+                ActivityManager.getCurrentUser());
         HashMap<Integer, Boolean> powerState = new HashMap<>();
         doAnswer(inv -> powerState.put(inv.getArgument(0), inv.getArgument(1)))
                 .when(mMockPowerManager).setPowerMode(anyInt(), anyBoolean());
@@ -2354,10 +2373,12 @@
 
     @Test
     public void testGamePowerMode_twoGamesOverlap() throws Exception {
-        GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+        GameManagerService gameManagerService = new GameManagerService(mMockContext,
+                mTestLooper.getLooper());
         String someGamePkg = "some.game";
         int somePackageId = DEFAULT_PACKAGE_UID + 1;
-        mockAppCategory(someGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME);
+        mockAppCategory(someGamePkg, somePackageId, ApplicationInfo.CATEGORY_GAME,
+                ActivityManager.getCurrentUser());
         gameManagerService.mUidObserver.onUidStateChanged(
                 DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);
         gameManagerService.mUidObserver.onUidStateChanged(
@@ -2372,7 +2393,8 @@
 
     @Test
     public void testGamePowerMode_noPackage() throws Exception {
-        GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+        GameManagerService gameManagerService = new GameManagerService(mMockContext,
+                mTestLooper.getLooper());
         String[] packages = {};
         when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);
         gameManagerService.mUidObserver.onUidStateChanged(
@@ -2383,23 +2405,24 @@
     @Test
     public void testGamePowerMode_gameAndNotGameApps_flagOn() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_DISABLE_GAME_MODE_WHEN_APP_TOP);
-        GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
-
+        GameManagerService gameManagerService = new GameManagerService(mMockContext,
+                mTestLooper.getLooper());
+        int userId = ActivityManager.getCurrentUser();
         String nonGamePkg1 = "not.game1";
         int nonGameUid1 = DEFAULT_PACKAGE_UID + 1;
-        mockAppCategory(nonGamePkg1, nonGameUid1, ApplicationInfo.CATEGORY_IMAGE);
+        mockAppCategory(nonGamePkg1, nonGameUid1, ApplicationInfo.CATEGORY_IMAGE, userId);
 
         String nonGamePkg2 = "not.game2";
         int nonGameUid2 = DEFAULT_PACKAGE_UID + 2;
-        mockAppCategory(nonGamePkg2, nonGameUid2, ApplicationInfo.CATEGORY_IMAGE);
+        mockAppCategory(nonGamePkg2, nonGameUid2, ApplicationInfo.CATEGORY_IMAGE, userId);
 
         String gamePkg1 = "game1";
         int gameUid1 = DEFAULT_PACKAGE_UID + 3;
-        mockAppCategory(gamePkg1, gameUid1, ApplicationInfo.CATEGORY_GAME);
+        mockAppCategory(gamePkg1, gameUid1, ApplicationInfo.CATEGORY_GAME, userId);
 
         String gamePkg2 = "game2";
         int gameUid2 = DEFAULT_PACKAGE_UID + 4;
-        mockAppCategory(gamePkg2, gameUid2, ApplicationInfo.CATEGORY_GAME);
+        mockAppCategory(gamePkg2, gameUid2, ApplicationInfo.CATEGORY_GAME, userId);
 
         // non-game1 top and background with no-op
         gameManagerService.mUidObserver.onUidStateChanged(
@@ -2470,15 +2493,17 @@
     @Test
     public void testGamePowerMode_gameAndNotGameApps_flagOff() throws Exception {
         mSetFlagsRule.disableFlags(Flags.FLAG_DISABLE_GAME_MODE_WHEN_APP_TOP);
-        GameManagerService gameManagerService = createServiceAndStartUser(USER_ID_1);
+        int userId = ActivityManager.getCurrentUser();
+        GameManagerService gameManagerService = new GameManagerService(mMockContext,
+                mTestLooper.getLooper());
 
         String nonGamePkg1 = "not.game1";
         int nonGameUid1 = DEFAULT_PACKAGE_UID + 1;
-        mockAppCategory(nonGamePkg1, nonGameUid1, ApplicationInfo.CATEGORY_IMAGE);
+        mockAppCategory(nonGamePkg1, nonGameUid1, ApplicationInfo.CATEGORY_IMAGE, userId);
 
         String gamePkg1 = "game1";
         int gameUid1 = DEFAULT_PACKAGE_UID + 3;
-        mockAppCategory(gamePkg1, gameUid1, ApplicationInfo.CATEGORY_GAME);
+        mockAppCategory(gamePkg1, gameUid1, ApplicationInfo.CATEGORY_GAME, userId);
 
         // non-game1 top and background with no-op
         gameManagerService.mUidObserver.onUidStateChanged(
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index 51f64ba..b97a268 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.notification;
 
+import static android.app.Flags.FLAG_SORT_SECTION_BY_TIME;
 import static android.app.Notification.COLOR_DEFAULT;
 import static android.app.Notification.FLAG_AUTO_CANCEL;
 import static android.app.Notification.FLAG_BUBBLE;
@@ -36,6 +37,7 @@
 import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
 import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
 
+import static com.android.server.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS;
 import static com.android.server.notification.GroupHelper.AGGREGATE_GROUP_KEY;
 import static com.android.server.notification.GroupHelper.AUTOGROUP_KEY;
 import static com.android.server.notification.GroupHelper.BASE_FLAGS;
@@ -2204,7 +2206,7 @@
         verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
                 eq(expectedGroupKey_silent), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
         verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
-                eq(expectedGroupKey_silent), eq(false));
+                eq(expectedGroupKey_silent), eq(true));
 
         // Check that the alerting section group is removed
         verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg),
@@ -2264,13 +2266,15 @@
                 notificationList);
 
         // Check that channel1's notifications are moved to the silent section group
-        expectedSummaryAttr = new NotificationAttributes(BASE_FLAGS,
-                mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT,
-                "TEST_CHANNEL_ID1");
-        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
-                eq(expectedGroupKey_silent), anyInt(), eq(expectedSummaryAttr));
-        verify(mCallback, times(AUTOGROUP_AT_COUNT/2 + 1)).addAutoGroup(anyString(),
-                eq(expectedGroupKey_silent), eq(false));
+        // But not enough to auto-group => remove override group key
+        verify(mCallback, never()).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                anyString(), anyInt(), any());
+        verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean());
+        for (NotificationRecord record: notificationList) {
+            if (record.getChannel().getId().equals(channel1.getId())) {
+                assertThat(record.getSbn().getOverrideGroupKey()).isNull();
+            }
+        }
 
         // Check that the alerting section group is not removed, only updated
         expectedSummaryAttr = new NotificationAttributes(BASE_FLAGS,
@@ -2343,7 +2347,7 @@
         verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
                 eq(expectedGroupKey_silent), anyInt(), eq(getNotificationAttributes(BASE_FLAGS)));
         verify(mCallback, times(numSilentGroupNotifications)).addAutoGroup(anyString(),
-                eq(expectedGroupKey_silent), eq(false));
+                eq(expectedGroupKey_silent), eq(true));
 
         // Check that the alerting section group is removed
         verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg),
@@ -2353,6 +2357,60 @@
     }
 
     @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void testAutogroup_updateChannel_reachedMinAutogroupCount() {
+        final String pkg = "package";
+        final NotificationChannel channel1 = new NotificationChannel("TEST_CHANNEL_ID1",
+                "TEST_CHANNEL_ID1", IMPORTANCE_DEFAULT);
+        final NotificationChannel channel2 = new NotificationChannel("TEST_CHANNEL_ID2",
+                "TEST_CHANNEL_ID2", IMPORTANCE_LOW);
+        final List<NotificationRecord> notificationList = new ArrayList<>();
+        // Post notifications with different channels that would autogroup in different sections
+        NotificationRecord r;
+        // Not enough notifications to autogroup initially
+        for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+            if (i % 2 == 0) {
+                r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM, null, false, channel1);
+            } else {
+                r = getNotificationRecord(pkg, i, String.valueOf(i),
+                    UserHandle.SYSTEM, null, false, channel2);
+            }
+            notificationList.add(r);
+            mGroupHelper.onNotificationPosted(r, false);
+        }
+        verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(),
+                anyString(), anyInt(), any());
+        verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean());
+        verify(mCallback, never()).removeAutoGroup(anyString());
+        verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+        verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+                any());
+        Mockito.reset(mCallback);
+
+        // Update channel1's importance
+        final String expectedGroupKey_silent = GroupHelper.getFullAggregateGroupKey(pkg,
+                AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
+        channel1.setImportance(IMPORTANCE_LOW);
+        for (NotificationRecord record: notificationList) {
+            if (record.getChannel().getId().equals(channel1.getId())) {
+                record.updateNotificationChannel(channel1);
+            }
+        }
+        mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel1,
+                notificationList);
+
+        // Check that channel1's notifications are moved to the silent section & autogroup all
+        NotificationAttributes expectedSummaryAttr = new NotificationAttributes(BASE_FLAGS,
+                mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT,
+                "TEST_CHANNEL_ID1");
+        verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+                eq(expectedGroupKey_silent), eq(true));
+        verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+                eq(expectedGroupKey_silent), anyInt(), eq(expectedSummaryAttr));
+    }
+
+    @Test
     @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
             Flags.FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS})
     public void testNoGroup_singletonGroup_underLimit() {
@@ -2519,7 +2577,11 @@
         assertThat(cachedSummary).isNull();
     }
 
-    private void checkNonGroupableNotifications() {
+    @Test
+    @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+    @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS)
+    public void testNonGroupableNotifications() {
+        // Check that there is no valid section for: conversations, calls, foreground services
         NotificationRecord notification_conversation = mock(NotificationRecord.class);
         when(notification_conversation.isConversation()).thenReturn(true);
         assertThat(GroupHelper.getSection(notification_conversation)).isNull();
@@ -2592,8 +2654,6 @@
                 "", false, recsChannel);
         assertThat(GroupHelper.getSection(notification_recs).mName).isEqualTo(
                 "AlertingSection");
-
-        checkNonGroupableNotifications();
     }
 
     @Test
@@ -2638,8 +2698,86 @@
                 "", false, recsChannel);
         assertThat(GroupHelper.getSection(notification_recs).mName).isEqualTo(
                 "RecsSection");
+    }
 
-        checkNonGroupableNotifications();
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS})
+    public void testNonGroupableNotifications_forceGroupConversations() {
+        // Check that there is no valid section for: calls, foreground services
+        NotificationRecord notification_call = spy(getNotificationRecord(mPkg, 0, "", mUser,
+                "", false, IMPORTANCE_LOW));
+        Notification n = mock(Notification.class);
+        StatusBarNotification sbn = spy(getSbn("package", 0, "0", UserHandle.SYSTEM));
+        when(notification_call.isConversation()).thenReturn(false);
+        when(notification_call.getNotification()).thenReturn(n);
+        when(notification_call.getSbn()).thenReturn(sbn);
+        when(sbn.getNotification()).thenReturn(n);
+        when(n.isStyle(Notification.CallStyle.class)).thenReturn(true);
+        assertThat(GroupHelper.getSection(notification_call)).isNull();
+
+        NotificationRecord notification_colorFg = spy(getNotificationRecord(mPkg, 0, "", mUser,
+                "", false, IMPORTANCE_LOW));
+        sbn = spy(getSbn("package", 0, "0", UserHandle.SYSTEM));
+        n = mock(Notification.class);
+        when(notification_colorFg.isConversation()).thenReturn(false);
+        when(notification_colorFg.getNotification()).thenReturn(n);
+        when(notification_colorFg.getSbn()).thenReturn(sbn);
+        when(sbn.getNotification()).thenReturn(n);
+        when(n.isForegroundService()).thenReturn(true);
+        when(n.isColorized()).thenReturn(true);
+        when(n.isStyle(Notification.CallStyle.class)).thenReturn(false);
+        assertThat(GroupHelper.getSection(notification_colorFg)).isNull();
+    }
+
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS})
+    @DisableFlags(FLAG_SORT_SECTION_BY_TIME)
+    public void testConversationGroupSections_disableSortSectionByTime() {
+        // Check that there are separate sections for conversations: alerting and silent
+        NotificationRecord notification_conversation_silent = getNotificationRecord(mPkg, 0, "",
+                mUser, "", false, IMPORTANCE_LOW);
+        notification_conversation_silent = spy(notification_conversation_silent);
+        when(notification_conversation_silent.isConversation()).thenReturn(true);
+        assertThat(GroupHelper.getSection(notification_conversation_silent).mName).isEqualTo(
+                "PeopleSection(silent)");
+
+        // Check that there is a correct section for conversations
+        NotificationRecord notification_conversation_alerting = getNotificationRecord(mPkg, 0, "",
+                mUser, "", false, IMPORTANCE_DEFAULT);
+        notification_conversation_alerting = spy(notification_conversation_alerting);
+        when(notification_conversation_alerting.isConversation()).thenReturn(true);
+        assertThat(GroupHelper.getSection(notification_conversation_alerting).mName).isEqualTo(
+                "PeopleSection(alerting)");
+    }
+
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+            FLAG_NOTIFICATION_FORCE_GROUP_CONVERSATIONS,
+            FLAG_SORT_SECTION_BY_TIME})
+    public void testConversationGroupSections() {
+        // Check that there is a single section for silent/alerting conversations
+        NotificationRecord notification_conversation_silent = getNotificationRecord(mPkg, 0, "",
+                mUser, "", false, IMPORTANCE_LOW);
+        notification_conversation_silent = spy(notification_conversation_silent);
+        when(notification_conversation_silent.isConversation()).thenReturn(true);
+        assertThat(GroupHelper.getSection(notification_conversation_silent).mName).isEqualTo(
+                "PeopleSection");
+
+        NotificationRecord notification_conversation_alerting = getNotificationRecord(mPkg, 0, "",
+                mUser, "", false, IMPORTANCE_DEFAULT);
+        notification_conversation_alerting = spy(notification_conversation_alerting);
+        when(notification_conversation_alerting.isConversation()).thenReturn(true);
+        assertThat(GroupHelper.getSection(notification_conversation_alerting).mName).isEqualTo(
+                "PeopleSection");
+
+        // Check that there is a section for priority conversations
+        NotificationRecord notification_conversation_prio = getNotificationRecord(mPkg, 0, "",
+                mUser, "", false, IMPORTANCE_DEFAULT);
+        notification_conversation_prio = spy(notification_conversation_prio);
+        when(notification_conversation_prio.isConversation()).thenReturn(true);
+        notification_conversation_prio.getChannel().setImportantConversation(true);
+        assertThat(GroupHelper.getSection(notification_conversation_prio).mName).isEqualTo(
+                "PeopleSection(priority)");
     }
 
 }
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 6f10370..c1e3f47 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -2186,6 +2186,80 @@
     }
 
     @Test
+    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    public void testReadXml_upgradeToModesUi_resetsImplicitRuleIcon() throws Exception {
+        setupZenConfig();
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        ZenRule implicitRuleWithModesUi = expectedImplicitRule("pkg",
+                ZEN_MODE_IMPORTANT_INTERRUPTIONS, POLICY, null);
+
+        // Add one implicit rule in the pre-MODES_UI configuration.
+        ZenRule implicitRuleBeforeModesUi = implicitRuleWithModesUi.copy();
+        implicitRuleBeforeModesUi.iconResName = "pkg_icon";
+        mZenModeHelper.mConfig.automaticRules.put(implicitRuleBeforeModesUi.id,
+                implicitRuleBeforeModesUi);
+        // Plus one other normal rule.
+        ZenRule anotherRule = newZenRule("other_pkg", Instant.now(), null);
+        anotherRule.id = "other_rule";
+        anotherRule.iconResName = "other_icon";
+        anotherRule.type = TYPE_IMMERSIVE;
+        mZenModeHelper.mConfig.automaticRules.put(anotherRule.id, anotherRule);
+
+        // Write with pre-modes-ui = (modes_api) version, then re-read.
+        ByteArrayOutputStream baos = writeXmlAndPurge(ZenModeConfig.XML_VERSION_MODES_API);
+        TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setInput(new BufferedInputStream(
+                new ByteArrayInputStream(baos.toByteArray())), null);
+        parser.nextTag();
+        mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL);
+
+        // Implicit rule was updated.
+        assertThat(mZenModeHelper.mConfig.automaticRules.get(implicitRuleBeforeModesUi.id))
+                .isEqualTo(implicitRuleWithModesUi);
+
+        // The other rule was untouched.
+        assertThat(mZenModeHelper.mConfig.automaticRules.get(anotherRule.id))
+                .isEqualTo(anotherRule);
+    }
+
+    @Test
+    @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+    public void testReadXml_onModesUi_implicitRulesUntouched() throws Exception {
+        setupZenConfig();
+        mZenModeHelper.mConfig.automaticRules.clear();
+
+        // Add one implicit rule already in its post-modes-UI configuration, also customized with
+        // an icon;
+        ZenRule implicitRuleWithModesUi = expectedImplicitRule("pkg",
+                ZEN_MODE_IMPORTANT_INTERRUPTIONS, POLICY, null);
+        implicitRuleWithModesUi.iconResName = "icon_chosen_by_user";
+        mZenModeHelper.mConfig.automaticRules.put(implicitRuleWithModesUi.id,
+                implicitRuleWithModesUi);
+
+        // Plus one other normal rule.
+        ZenRule anotherRule = newZenRule("other_pkg", Instant.now(), null);
+        anotherRule.id = "other_rule";
+        anotherRule.iconResName = "other_icon";
+        anotherRule.type = TYPE_IMMERSIVE;
+        mZenModeHelper.mConfig.automaticRules.put(anotherRule.id, anotherRule);
+
+        // Write with modes_ui version, then re-read.
+        ByteArrayOutputStream baos = writeXmlAndPurge(ZenModeConfig.XML_VERSION_MODES_UI);
+        TypedXmlPullParser parser = Xml.newFastPullParser();
+        parser.setInput(new BufferedInputStream(
+                new ByteArrayInputStream(baos.toByteArray())), null);
+        parser.nextTag();
+        mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL);
+
+        // Both rules were untouched
+        assertThat(mZenModeHelper.mConfig.automaticRules.get(implicitRuleWithModesUi.id))
+                .isEqualTo(implicitRuleWithModesUi);
+        assertThat(mZenModeHelper.mConfig.automaticRules.get(anotherRule.id))
+                .isEqualTo(anotherRule);
+    }
+
+    @Test
     public void testCountdownConditionSubscription() throws Exception {
         ZenModeConfig config = new ZenModeConfig();
         mZenModeHelper.mConfig = config;
@@ -6852,7 +6926,9 @@
         rule.zenPolicy = policy;
         rule.pkg = ownerPkg;
         rule.name = CUSTOM_APP_LABEL;
-        rule.iconResName = ICON_RES_NAME;
+        if (!Flags.modesUi()) {
+            rule.iconResName = ICON_RES_NAME;
+        }
         rule.triggerDescription = mContext.getString(R.string.zen_mode_implicit_trigger_description,
                 CUSTOM_APP_LABEL);
         rule.type = AutomaticZenRule.TYPE_OTHER;
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index 9950541..b6e393d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -39,10 +39,8 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
 import static com.android.server.wm.WindowContainer.POSITION_TOP;
 
 import static org.junit.Assert.assertEquals;
@@ -51,9 +49,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeFalse;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
 
 import android.graphics.Rect;
 import android.os.Binder;
@@ -377,41 +373,6 @@
     }
 
     @Test
-    public void testDelayWhileRecents() {
-        final DisplayContent dc = createNewDisplay(Display.STATE_ON);
-        doReturn(false).when(dc).onDescendantOrientationChanged(any());
-        final Task task = createTask(dc);
-
-        // Simulate activity1 launches activity2.
-        final ActivityRecord activity1 = createActivityRecord(task);
-        activity1.setVisible(true);
-        activity1.setVisibleRequested(false);
-        activity1.allDrawn = true;
-        final ActivityRecord activity2 = createActivityRecord(task);
-        activity2.setVisible(false);
-        activity2.setVisibleRequested(true);
-        activity2.allDrawn = true;
-
-        dc.mClosingApps.add(activity1);
-        dc.mOpeningApps.add(activity2);
-        dc.prepareAppTransition(TRANSIT_OPEN);
-        assertTrue(dc.mAppTransition.containsTransitRequest(TRANSIT_OPEN));
-
-        // Wait until everything in animation handler get executed to prevent the exiting window
-        // from being removed during WindowSurfacePlacer Traversal.
-        waitUntilHandlersIdle();
-
-        // Start recents
-        doReturn(true).when(task)
-                .isSelfAnimating(anyInt(), eq(ANIMATION_TYPE_RECENTS));
-
-        dc.mAppTransitionController.handleAppTransitionReady();
-
-        verify(activity1, never()).commitVisibility(anyBoolean(), anyBoolean(), anyBoolean());
-        verify(activity2, never()).commitVisibility(anyBoolean(), anyBoolean(), anyBoolean());
-    }
-
-    @Test
     public void testGetAnimationStyleResId() {
         // Verify getAnimationStyleResId will return as LayoutParams.windowAnimations when without
         // specifying window type.
diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
index 6c8a7ac..9981a4d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimatorTest.java
@@ -23,7 +23,6 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -96,18 +95,18 @@
     @Test
     public void testRunAnimation() {
         mAnimatable.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */,
-                ANIMATION_TYPE_RECENTS);
+                ANIMATION_TYPE_APP_TRANSITION);
         final ArgumentCaptor<OnAnimationFinishedCallback> callbackCaptor = ArgumentCaptor.forClass(
                 OnAnimationFinishedCallback.class);
         assertAnimating(mAnimatable);
         verify(mTransaction).reparent(eq(mAnimatable.mSurface), eq(mAnimatable.mLeash));
-        verify(mSpec).startAnimation(any(), any(), eq(ANIMATION_TYPE_RECENTS),
+        verify(mSpec).startAnimation(any(), any(), eq(ANIMATION_TYPE_APP_TRANSITION),
                 callbackCaptor.capture());
 
-        callbackCaptor.getValue().onAnimationFinished(ANIMATION_TYPE_RECENTS, mSpec);
+        callbackCaptor.getValue().onAnimationFinished(ANIMATION_TYPE_APP_TRANSITION, mSpec);
         assertNotAnimating(mAnimatable);
         assertTrue(mAnimatable.mFinishedCallbackCalled);
-        assertEquals(ANIMATION_TYPE_RECENTS, mAnimatable.mFinishedAnimationType);
+        assertEquals(ANIMATION_TYPE_APP_TRANSITION, mAnimatable.mFinishedAnimationType);
         verify(mTransaction).remove(eq(mAnimatable.mLeash));
         // TODO: Verify reparenting once we use mPendingTransaction to reparent it back
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
index d537bd7..88ce3a6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
@@ -40,14 +40,12 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
 import static com.android.server.wm.WindowStateAnimator.PRESERVED_SURFACE_LAYER;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
 
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
@@ -405,30 +403,6 @@
     }
 
     @Test
-    public void testAssignWindowLayers_ForImeOnAppWithRecentsAnimating() {
-        final WindowState imeAppTarget = createWindow(null, TYPE_APPLICATION,
-                mAppWindow.mActivityRecord, "imeAppTarget");
-        mDisplayContent.setImeInputTarget(imeAppTarget);
-        mDisplayContent.setImeLayeringTarget(imeAppTarget);
-        mDisplayContent.setImeControlTarget(imeAppTarget);
-        mDisplayContent.updateImeParent();
-
-        // Simulate the ime layering target task is animating with recents animation.
-        final Task imeAppTargetTask = imeAppTarget.getTask();
-        final SurfaceAnimator imeTargetTaskAnimator = imeAppTargetTask.mSurfaceAnimator;
-        spyOn(imeTargetTaskAnimator);
-        doReturn(ANIMATION_TYPE_RECENTS).when(imeTargetTaskAnimator).getAnimationType();
-        doReturn(true).when(imeTargetTaskAnimator).isAnimating();
-
-        mDisplayContent.assignChildLayers(mTransaction);
-
-        // Ime should on top of the application window when in recents animation and keep
-        // attached on app.
-        assertTrue(mDisplayContent.shouldImeAttachedToApp());
-        assertWindowHigher(mImeWindow, imeAppTarget);
-    }
-
-    @Test
     public void testAssignWindowLayers_ForImeOnPopupImeLayeringTarget() {
         final WindowState imeAppTarget = createWindow(null, TYPE_APPLICATION,
                 mAppWindow.mActivityRecord, "imeAppTarget");