Merge "Support force-stop IME package for background users" into main
diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java
index 1d950dc..6343313 100644
--- a/core/java/android/view/ImeInsetsSourceConsumer.java
+++ b/core/java/android/view/ImeInsetsSourceConsumer.java
@@ -119,9 +119,11 @@
 
     @Override
     public boolean applyLocalVisibilityOverride() {
-        ImeTracing.getInstance().triggerClientDump(
-                "ImeInsetsSourceConsumer#applyLocalVisibilityOverride",
-                mController.getHost().getInputMethodManager(), null /* icProto */);
+        if (!Flags.refactorInsetsController()) {
+            ImeTracing.getInstance().triggerClientDump(
+                    "ImeInsetsSourceConsumer#applyLocalVisibilityOverride",
+                    mController.getHost().getInputMethodManager(), null /* icProto */);
+        }
         return super.applyLocalVisibilityOverride();
     }
 
@@ -205,9 +207,13 @@
 
     @Override
     public void removeSurface() {
-        final IBinder window = mController.getHost().getWindowToken();
-        if (window != null) {
-            getImm().removeImeSurface(window);
+        if (Flags.refactorInsetsController()) {
+            super.removeSurface();
+        } else {
+            final IBinder window = mController.getHost().getWindowToken();
+            if (window != null) {
+                getImm().removeImeSurface(window);
+            }
         }
     }
 
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index df2af73..f166b89 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -765,7 +765,7 @@
 
     public InsetsController(Host host) {
         this(host, (controller, id, type) -> {
-            if (type == ime()) {
+            if (!Flags.refactorInsetsController() &&  type == ime()) {
                 return new ImeInsetsSourceConsumer(id, controller.mState,
                         Transaction::new, controller);
             } else {
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index c73cbc6..477e35b 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -43,6 +43,7 @@
 import android.view.inputmethod.ImeTracker;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.inputmethod.ImeTracing;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -296,6 +297,13 @@
 
     @VisibleForTesting(visibility = PACKAGE)
     public boolean applyLocalVisibilityOverride() {
+        if (Flags.refactorInsetsController()) {
+            if (mType == WindowInsets.Type.ime()) {
+                ImeTracing.getInstance().triggerClientDump(
+                        "ImeInsetsSourceConsumer#applyLocalVisibilityOverride",
+                        mController.getHost().getInputMethodManager(), null /* icProto */);
+            }
+        }
         final InsetsSource source = mState.peekSource(mId);
         if (source == null) {
             return false;
@@ -396,6 +404,14 @@
      */
     public void removeSurface() {
         // no-op for types that always return ShowResult#SHOW_IMMEDIATELY.
+        if (Flags.refactorInsetsController()) {
+            if (mType == WindowInsets.Type.ime()) {
+                final IBinder window = mController.getHost().getWindowToken();
+                if (window != null) {
+                    mController.getHost().getInputMethodManager().removeImeSurface(window);
+                }
+            }
+        }
     }
 
     @VisibleForTesting(visibility = PACKAGE)
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index c9d2eec..fed8eea 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -1352,12 +1352,16 @@
                 case MSG_SET_VISIBILITY:
                     final boolean visible = msg.arg1 != 0;
                     synchronized (mH) {
-                        if (visible) {
-                            showSoftInput(mServedView, /* flags */ 0);
-                        } else {
-                            if (mCurRootView != null
-                                    && mCurRootView.getInsetsController() != null) {
-                                mCurRootView.getInsetsController().hide(WindowInsets.Type.ime());
+                        if (mCurRootView != null) {
+                            final var insetsController = mCurRootView.getInsetsController();
+                            if (insetsController != null) {
+                                if (visible) {
+                                    insetsController.show(WindowInsets.Type.ime(),
+                                            false /* fromIme */, null /* statsToken */);
+                                } else {
+                                    insetsController.hide(WindowInsets.Type.ime(),
+                                            false /* fromIme */, null /* statsToken */);
+                                }
                             }
                         }
                     }
@@ -2334,16 +2338,18 @@
             ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_VIEW_SERVED);
 
             if (Flags.refactorInsetsController()) {
+                final var viewRootImpl = view.getViewRootImpl();
                 // In case of a running show IME animation, it should not be requested visible,
                 // otherwise the animation would jump and not be controlled by the user anymore
-                if ((mCurRootView.getInsetsController().computeUserAnimatingTypes()
-                        & WindowInsets.Type.ime()) == 0) {
+                if (viewRootImpl != null
+                        && (viewRootImpl.getInsetsController().computeUserAnimatingTypes()
+                                & WindowInsets.Type.ime()) == 0) {
                     // TODO(b/322992891) handle case of SHOW_IMPLICIT
-                    view.getWindowInsetsController().show(WindowInsets.Type.ime());
+                    viewRootImpl.getInsetsController().show(WindowInsets.Type.ime(),
+                            false /* fromIme */, statsToken);
                     return true;
-                } else {
-                    return false;
                 }
+                return false;
             } else {
                 // Makes sure to call ImeInsetsSourceConsumer#onShowRequested on the UI thread.
                 // TODO(b/229426865): call WindowInsetsController#show instead.
@@ -2497,7 +2503,10 @@
 
             if (Flags.refactorInsetsController()) {
                 // TODO(b/322992891) handle case of HIDE_IMPLICIT_ONLY
-                servedView.getWindowInsetsController().hide(WindowInsets.Type.ime());
+                final var viewRootImpl = servedView.getViewRootImpl();
+                if (viewRootImpl != null) {
+                    viewRootImpl.getInsetsController().hide(WindowInsets.Type.ime());
+                }
                 return true;
             } else {
                 return IInputMethodManagerGlobalInvoker.hideSoftInput(mClient, windowToken,
diff --git a/core/java/android/webkit/WebViewProviderInfo.java b/core/java/android/webkit/WebViewProviderInfo.java
index 6629fdc4..16727c3 100644
--- a/core/java/android/webkit/WebViewProviderInfo.java
+++ b/core/java/android/webkit/WebViewProviderInfo.java
@@ -23,6 +23,9 @@
 import android.os.Parcelable;
 import android.util.Base64;
 
+import java.util.Arrays;
+import java.util.Objects;
+
 /**
  * @hide
  */
@@ -80,6 +83,35 @@
         out.writeTypedArray(signatures, 0);
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o instanceof WebViewProviderInfo that) {
+            return this.packageName.equals(that.packageName)
+                    && this.description.equals(that.description)
+                    && this.availableByDefault == that.availableByDefault
+                    && this.isFallback == that.isFallback
+                    && Arrays.equals(this.signatures, that.signatures);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(packageName, description, availableByDefault,
+                isFallback, Arrays.hashCode(signatures));
+    }
+
+    @Override
+    public String toString() {
+        return "WebViewProviderInfo; packageName=" + packageName
+                + " description=\"" + description
+                + "\" availableByDefault=" + availableByDefault
+                + " isFallback=" + isFallback
+                + " signatures=" + Arrays.toString(signatures);
+    }
+
     // fields read from framework resource
     public final String packageName;
     public final String description;
diff --git a/core/tests/batterystatstests/BatteryUsageStatsProtoTests/Android.bp b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/Android.bp
new file mode 100644
index 0000000..1fb5f2c
--- /dev/null
+++ b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/Android.bp
@@ -0,0 +1,30 @@
+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_test {
+    name: "BatteryUsageStatsProtoTests",
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "androidx.test.rules",
+        "junit",
+        "mockito-target-minus-junit4",
+        "platform-test-annotations",
+        "platformprotosnano",
+        "statsdprotolite",
+        "truth",
+    ],
+
+    libs: ["android.test.runner"],
+
+    platform_apis: true,
+    certificate: "platform",
+
+    test_suites: ["device-tests"],
+}
diff --git a/core/tests/batterystatstests/BatteryUsageStatsProtoTests/AndroidManifest.xml b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/AndroidManifest.xml
new file mode 100644
index 0000000..9128dca
--- /dev/null
+++ b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2021 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.frameworks.core.batteryusagestatsprototests">
+
+    <uses-permission android:name="android.permission.BATTERY_STATS"/>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.frameworks.core.batteryusagestatsprototests"
+        android:label="BatteryUsageStats Proto Tests" />
+
+</manifest>
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/src/com/android/internal/os/BatteryUsageStatsPulledTest.java
similarity index 73%
rename from services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java
rename to core/tests/batterystatstests/BatteryUsageStatsProtoTests/src/com/android/internal/os/BatteryUsageStatsPulledTest.java
index 62efbc3..ac1f7d0 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java
+++ b/core/tests/batterystatstests/BatteryUsageStatsProtoTests/src/com/android/internal/os/BatteryUsageStatsPulledTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.server.power.stats;
+package com.android.internal.os;
 
 import static android.os.BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE;
 
@@ -23,262 +23,39 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
 
 import android.os.AggregateBatteryConsumer;
 import android.os.BatteryConsumer;
 import android.os.BatteryUsageStats;
-import android.os.Process;
 import android.os.UidBatteryConsumer;
 import android.os.nano.BatteryUsageStatsAtomsProto;
 import android.os.nano.BatteryUsageStatsAtomsProto.BatteryConsumerData.PowerComponentUsage;
-import android.platform.test.ravenwood.RavenwoodRule;
-import android.util.StatsEvent;
 
 import androidx.test.filters.SmallTest;
 
-import com.android.server.am.BatteryStatsService;
-
 import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
 
-import org.junit.Rule;
 import org.junit.Test;
 
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
-@SmallTest
-public class BatteryUsageStatsAtomTest {
 
-    @Rule
-    public final RavenwoodRule mRavenwood = new RavenwoodRule();
+@SmallTest
+public class BatteryUsageStatsPulledTest {
 
     private static final int UID_0 = 1000;
     private static final int UID_1 = 2000;
     private static final int UID_2 = 3000;
     private static final int UID_3 = 4000;
+    private static final int[] UID_USAGE_TIME_PROCESS_STATES = {
+            BatteryConsumer.PROCESS_STATE_FOREGROUND,
+            BatteryConsumer.PROCESS_STATE_BACKGROUND,
+            BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE
+    };
 
     @Test
-    public void testAtom_BatteryUsageStatsPerUid() {
-        final BatteryUsageStats bus = buildBatteryUsageStats();
-        BatteryStatsService.FrameworkStatsLogger statsLogger =
-                mock(BatteryStatsService.FrameworkStatsLogger.class);
-
-        List<StatsEvent> actual = new ArrayList<>();
-        new BatteryStatsService.StatsPerUidLogger(statsLogger).logStats(bus, actual);
-
-        // Device-wide totals
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                Process.INVALID_UID,
-                BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
-                0L,
-                "cpu",
-                30000.0f,
-                20100.0f,
-                20300L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                Process.INVALID_UID,
-                BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
-                0L,
-                "camera",
-                30000.0f,
-                20150.0f,
-                0L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                Process.INVALID_UID,
-                BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
-                0L,
-                "CustomConsumer1",
-                30000.0f,
-                20200.0f,
-                20400L
-        );
-
-        // Per-proc state estimates for UID_0
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
-                0L,
-                "screen",
-                1650.0f,
-                300.0f,
-                0L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
-                0L,
-                "cpu",
-                1650.0f,
-                400.0f,
-                600L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_FOREGROUND,
-                1000L,
-                "cpu",
-                1650.0f,
-                9100.0f,
-                8100L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_BACKGROUND,
-                2000L,
-                "cpu",
-                1650.0f,
-                9200.0f,
-                8200L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE,
-                0L,
-                "cpu",
-                1650.0f,
-                9300.0f,
-                8400L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_CACHED,
-                0L,
-                "cpu",
-                1650.0f,
-                9400.0f,
-                0L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_FOREGROUND,
-                1000L,
-                "CustomConsumer1",
-                1650.0f,
-                450.0f,
-                0L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_BACKGROUND,
-                2000L,
-                "CustomConsumer1",
-                1650.0f,
-                450.0f,
-                0L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_FOREGROUND,
-                1000L,
-                "CustomConsumer2",
-                1650.0f,
-                500.0f,
-                800L
-        );
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_0,
-                BatteryConsumer.PROCESS_STATE_BACKGROUND,
-                2000L,
-                "CustomConsumer2",
-                1650.0f,
-                500.0f,
-                800L
-        );
-
-        // Nothing for UID_1, because its power consumption is 0
-
-        // Only "screen" is populated for UID_2
-        verify(statsLogger).buildStatsEvent(
-                1000L,
-                20000L,
-                10000L,
-                20,
-                1234L,
-                UID_2,
-                BatteryConsumer.PROCESS_STATE_UNSPECIFIED,
-                0L,
-                "screen",
-                766.0f,
-                766.0f,
-                0L
-        );
-
-        verifyNoMoreInteractions(statsLogger);
-    }
-
-    @Test
-    public void testAtom_BatteryUsageStatsAtomsProto() {
+    public void testGetStatsProto() {
         final BatteryUsageStats bus = buildBatteryUsageStats();
         final byte[] bytes = bus.getStatsProto();
         BatteryUsageStatsAtomsProto proto;
@@ -291,7 +68,9 @@
 
         assertEquals(bus.getStatsStartTimestamp(), proto.sessionStartMillis);
         assertEquals(bus.getStatsEndTimestamp(), proto.sessionEndMillis);
-        assertEquals(10000, proto.sessionDurationMillis);
+        assertEquals(
+                bus.getStatsEndTimestamp() - bus.getStatsStartTimestamp(),
+                proto.sessionDurationMillis);
         assertEquals(bus.getDischargePercentage(), proto.sessionDischargePercentage);
         assertEquals(bus.getDischargeDurationMs(), proto.dischargeDurationMillis);
 
@@ -311,8 +90,8 @@
         final List<android.os.UidBatteryConsumer> uidConsumers = bus.getUidBatteryConsumers();
         uidConsumers.sort((a, b) -> a.getUid() - b.getUid());
 
-        final BatteryUsageStatsAtomsProto.UidBatteryConsumer[] uidConsumersProto =
-                proto.uidBatteryConsumers;
+        final BatteryUsageStatsAtomsProto.UidBatteryConsumer[] uidConsumersProto
+                = proto.uidBatteryConsumers;
         Arrays.sort(uidConsumersProto, (a, b) -> a.uid - b.uid);
 
         // UID_0 - After sorting, UID_0 should be in position 0 for both data structures
@@ -407,12 +186,6 @@
         }
     }
 
-    private static final int[] UID_USAGE_TIME_PROCESS_STATES = {
-            BatteryConsumer.PROCESS_STATE_FOREGROUND,
-            BatteryConsumer.PROCESS_STATE_BACKGROUND,
-            BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE
-    };
-
     private void assertSameUidBatteryConsumer(
             android.os.UidBatteryConsumer uidConsumer,
             BatteryUsageStatsAtomsProto.UidBatteryConsumer uidConsumerProto,
@@ -422,10 +195,10 @@
         assertEquals("Uid consumers had mismatched uids", uid, uidConsumer.getUid());
 
         assertEquals("For uid " + uid,
-                uidConsumer.getTimeInProcessStateMs(BatteryConsumer.PROCESS_STATE_FOREGROUND),
+                uidConsumer.getTimeInStateMs(android.os.UidBatteryConsumer.STATE_FOREGROUND),
                 uidConsumerProto.timeInForegroundMillis);
         assertEquals("For uid " + uid,
-                uidConsumer.getTimeInProcessStateMs(BatteryConsumer.PROCESS_STATE_BACKGROUND),
+                uidConsumer.getTimeInStateMs(android.os.UidBatteryConsumer.STATE_BACKGROUND),
                 uidConsumerProto.timeInBackgroundMillis);
         for (int processState : UID_USAGE_TIME_PROCESS_STATES) {
             final long timeInStateMillis = uidConsumer.getTimeInProcessStateMs(processState);
@@ -492,9 +265,7 @@
                         .setDischargePercentage(20)
                         .setDischargedPowerRange(1000, 2000)
                         .setDischargeDurationMs(1234)
-                        .setStatsStartTimestamp(1000)
-                        .setStatsEndTimestamp(20000)
-                        .setStatsDuration(10000);
+                        .setStatsStartTimestamp(1000);
         final UidBatteryConsumer.Builder uidBuilder = builder
                 .getOrCreateUidBatteryConsumerBuilder(UID_0)
                 .setPackageWithHighestDrain("myPackage0")
diff --git a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
index 47b28899..248db65 100644
--- a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java
@@ -37,8 +37,12 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.view.WindowManager.BadTokenException;
 import android.view.WindowManager.LayoutParams;
+import android.view.inputmethod.Flags;
 import android.view.inputmethod.ImeTracker;
 import android.widget.TextView;
 
@@ -46,6 +50,7 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mockito;
@@ -61,6 +66,9 @@
 @RunWith(AndroidJUnit4.class)
 public class ImeInsetsSourceConsumerTest {
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
     InsetsSourceConsumer mImeConsumer;
     @Spy InsetsController mController;
@@ -112,6 +120,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testImeRequestedVisibleAwaitingControl() {
         // Set null control and then request show.
         mController.onControlsChanged(new InsetsSourceControl[] { null });
@@ -141,6 +150,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testImeRequestedVisibleAwaitingLeash() {
         // Set null control, then request show.
         mController.onControlsChanged(new InsetsSourceControl[] { null });
@@ -185,6 +195,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testImeGetAndClearSkipAnimationOnce_expectSkip() {
         // Expect IME animation will skipped when the IME is visible at first place.
         verifyImeGetAndClearSkipAnimationOnce(true /* hasWindowFocus */, true /* hasViewFocus */,
@@ -192,6 +203,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void testImeGetAndClearSkipAnimationOnce_expectNoSkip() {
         // Expect IME animation will not skipped if previously no view focused when gained the
         // window focus and requesting the IME visible next time.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 9bcd9b0..99bd96e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -3739,8 +3739,9 @@
                 mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType,
                         EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW);
                 Log.w(TAG, splitFailureMessage("onNoLongerSupportMultiWindow",
-                        "app package " + taskInfo.baseActivity.getPackageName()
-                        + " does not support splitscreen, or is a controlled activity type"));
+                        "app package " + taskInfo.baseIntent.getComponent()
+                                + " does not support splitscreen, or is a controlled activity"
+                                + " type"));
                 if (splitScreenVisible) {
                     handleUnsupportedSplitStart();
                 }
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
index db962e7..2406bde 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
@@ -48,7 +48,10 @@
 
     @Before
     fun setup() {
-        tapl.workspace.switchToOverview().dismissAllTasks()
+        val overview = tapl.workspace.switchToOverview()
+        if (overview.hasTasks()) {
+            overview.dismissAllTasks()
+        }
 
         tapl.setEnableRotation(true)
         tapl.setExpectedRotation(rotation.value)
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
index 7886e85..49b974f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
@@ -20,6 +20,7 @@
 import android.provider.Settings
 import com.android.settingslib.notification.modes.TestModeBuilder
 import com.android.settingslib.notification.modes.ZenMode
+import java.time.Duration
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -35,8 +36,7 @@
     override val globalZenMode: StateFlow<Int>
         get() = mutableZenMode.asStateFlow()
 
-    private val mutableModesFlow: MutableStateFlow<List<ZenMode>> =
-        MutableStateFlow(listOf(TestModeBuilder.EXAMPLE))
+    private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = MutableStateFlow(listOf())
     override val modes: Flow<List<ZenMode>>
         get() = mutableModesFlow.asStateFlow()
 
@@ -52,6 +52,10 @@
         mutableZenMode.value = zenMode
     }
 
+    fun addModes(zenModes: List<ZenMode>) {
+        mutableModesFlow.value += zenModes
+    }
+
     fun addMode(id: String, active: Boolean = false) {
         mutableModesFlow.value += newMode(id, active)
     }
@@ -60,6 +64,20 @@
         mutableModesFlow.value = mutableModesFlow.value.filter { it.id != id }
     }
 
+    override fun activateMode(zenMode: ZenMode, duration: Duration?) {
+        activateMode(zenMode.id)
+    }
+
+    override fun deactivateMode(zenMode: ZenMode) {
+        deactivateMode(zenMode.id)
+    }
+
+    fun activateMode(id: String) {
+        val oldMode = mutableModesFlow.value.find { it.id == id } ?: return
+        removeMode(id)
+        mutableModesFlow.value += TestModeBuilder(oldMode).setActive(true).build()
+    }
+
     fun deactivateMode(id: String) {
         val oldMode = mutableModesFlow.value.find { it.id == id } ?: return
         removeMode(id)
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
index b2fcb5f..0ff7f84 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt
@@ -30,6 +30,7 @@
 import com.android.settingslib.flags.Flags
 import com.android.settingslib.notification.modes.ZenMode
 import com.android.settingslib.notification.modes.ZenModesBackend
+import java.time.Duration
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
@@ -57,6 +58,10 @@
 
     /** A list of all existing priority modes. */
     val modes: Flow<List<ZenMode>>
+
+    fun activateMode(zenMode: ZenMode, duration: Duration? = null)
+
+    fun deactivateMode(zenMode: ZenMode)
 }
 
 @SuppressLint("SharedFlowCreation")
@@ -178,4 +183,12 @@
             flowOf(emptyList())
         }
     }
+
+    override fun activateMode(zenMode: ZenMode, duration: Duration?) {
+        backend.activateMode(zenMode, duration)
+    }
+
+    override fun deactivateMode(zenMode: ZenMode) {
+        backend.deactivateMode(zenMode)
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
index 7b994d5..2f7cdd6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
@@ -37,6 +37,13 @@
     private ZenModeConfig.ZenRule mConfigZenRule;
 
     public static final ZenMode EXAMPLE = new TestModeBuilder().build();
+    public static final ZenMode MANUAL_DND = ZenMode.manualDndMode(
+            new AutomaticZenRule.Builder("Manual DND", Uri.parse("rule://dnd"))
+                    .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
+                    .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build())
+                    .build(),
+            true /* isActive */
+    );
 
     public TestModeBuilder() {
         // Reasonable defaults
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index a17076b..4e01a71 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -539,6 +539,7 @@
         "androidx.preference_preference",
         "androidx.appcompat_appcompat",
         "androidx.concurrent_concurrent-futures",
+        "androidx.concurrent_concurrent-futures-ktx",
         "androidx.mediarouter_mediarouter",
         "androidx.palette_palette",
         "androidx.legacy_legacy-preference-v14",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 97a45fb..035e2fb 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -870,6 +870,13 @@
 }
 
 flag {
+    name: "qs_ui_refactor_compose_fragment"
+    namespace: "systemui"
+    description: "Uses a different QS fragment in NPVC that uses the new compose UI and recommended architecture. This flag depends on qs_ui_refactor flag."
+    bug: "325099249"
+}
+
+flag {
   name: "remove_dream_overlay_hide_on_touch"
   namespace: "systemui"
   description: "Removes logic to hide the dream overlay on user interaction, as it conflicts with various transitions"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
index 5e87f46..61873ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModelTest.kt
@@ -18,6 +18,7 @@
 
 package com.android.systemui.statusbar.notification.row.ui.viewmodel
 
+import android.app.Notification
 import android.app.PendingIntent
 import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -90,7 +91,8 @@
         name: String = "example",
         timeRemaining: Duration = Duration.ofMinutes(3),
         resumeIntent: PendingIntent? = null,
-        resetIntent: PendingIntent? = null
+        addMinuteAction: Notification.Action? = null,
+        resetAction: Notification.Action? = null
     ) =
         TimerContentModel(
             icon = icon,
@@ -99,7 +101,8 @@
                 Paused(
                     timeRemaining = timeRemaining,
                     resumeIntent = resumeIntent,
-                    resetIntent = resetIntent,
+                    addMinuteAction = addMinuteAction,
+                    resetAction = resetAction,
                 )
         )
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
new file mode 100644
index 0000000..fdfc7f1
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt
@@ -0,0 +1,164 @@
+/*
+ * 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.policy.ui.dialog.viewmodel
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ModesDialogViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    val repository = kosmos.fakeZenModeRepository
+    val interactor = kosmos.zenModeInteractor
+
+    val underTest = ModesDialogViewModel(context, interactor, kosmos.testDispatcher)
+
+    @Test
+    fun tiles_filtersOutDisabledModes() =
+        testScope.runTest {
+            val tiles by collectLastValue(underTest.tiles)
+
+            repository.addModes(
+                listOf(
+                    TestModeBuilder().setName("Disabled").setEnabled(false).build(),
+                    TestModeBuilder.MANUAL_DND,
+                    TestModeBuilder()
+                        .setName("Enabled")
+                        .setEnabled(true)
+                        .setManualInvocationAllowed(true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Disabled with manual")
+                        .setEnabled(false)
+                        .setManualInvocationAllowed(true)
+                        .build(),
+                ))
+            runCurrent()
+
+            assertThat(tiles?.size).isEqualTo(2)
+            with(tiles?.elementAt(0)!!) {
+                assertThat(this.text).isEqualTo("Manual DND")
+                assertThat(this.subtext).isEqualTo("On")
+                assertThat(this.enabled).isEqualTo(true)
+            }
+            with(tiles?.elementAt(1)!!) {
+                assertThat(this.text).isEqualTo("Enabled")
+                assertThat(this.subtext).isEqualTo("Off")
+                assertThat(this.enabled).isEqualTo(false)
+            }
+        }
+
+    @Test
+    fun tiles_filtersOutInactiveModesWithoutManualInvocation() =
+        testScope.runTest {
+            val tiles by collectLastValue(underTest.tiles)
+
+            repository.addModes(
+                listOf(
+                    TestModeBuilder()
+                        .setName("Active without manual")
+                        .setActive(true)
+                        .setManualInvocationAllowed(false)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Active with manual")
+                        .setTriggerDescription("trigger description")
+                        .setActive(true)
+                        .setManualInvocationAllowed(true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Inactive with manual")
+                        .setActive(false)
+                        .setManualInvocationAllowed(true)
+                        .build(),
+                    TestModeBuilder()
+                        .setName("Inactive without manual")
+                        .setActive(false)
+                        .setManualInvocationAllowed(false)
+                        .build(),
+                ))
+            runCurrent()
+
+            assertThat(tiles?.size).isEqualTo(3)
+            with(tiles?.elementAt(0)!!) {
+                assertThat(this.text).isEqualTo("Active without manual")
+                assertThat(this.subtext).isEqualTo("On")
+                assertThat(this.enabled).isEqualTo(true)
+            }
+            with(tiles?.elementAt(1)!!) {
+                assertThat(this.text).isEqualTo("Active with manual")
+                assertThat(this.subtext).isEqualTo("trigger description")
+                assertThat(this.enabled).isEqualTo(true)
+            }
+            with(tiles?.elementAt(2)!!) {
+                assertThat(this.text).isEqualTo("Inactive with manual")
+                assertThat(this.subtext).isEqualTo("Off")
+                assertThat(this.enabled).isEqualTo(false)
+            }
+        }
+
+    @Test
+    fun onClick_togglesTileState() =
+        testScope.runTest {
+            val tiles by collectLastValue(underTest.tiles)
+
+            val modeId = "id"
+            repository.addModes(
+                listOf(
+                    TestModeBuilder()
+                        .setId(modeId)
+                        .setName("Test")
+                        .setManualInvocationAllowed(true)
+                        .build()
+                )
+            )
+            runCurrent()
+
+            assertThat(tiles?.size).isEqualTo(1)
+            assertThat(tiles?.elementAt(0)?.enabled).isFalse()
+
+            // Trigger onClick
+            tiles?.first()?.onClick?.let { it() }
+            runCurrent()
+
+            assertThat(tiles?.first()?.enabled).isTrue()
+
+            // Trigger onClick
+            tiles?.first()?.onClick?.let { it() }
+            runCurrent()
+
+            assertThat(tiles?.first()?.enabled).isFalse()
+        }
+}
diff --git a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
index f2bfbe5c9..3a679e3 100644
--- a/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
+++ b/packages/SystemUI/res/layout/rich_ongoing_timer_notification.xml
@@ -33,7 +33,6 @@
         android:id="@+id/icon"
         android:layout_width="24dp"
         android:layout_height="24dp"
-        android:src="@drawable/ic_close"
         app:tint="@android:color/white"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toStartOf="@id/label"
@@ -88,11 +87,10 @@
         />
 
     <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+        style="@*android:style/NotificationEmphasizedAction"
         android:id="@+id/mainButton"
         android:layout_width="124dp"
         android:layout_height="wrap_content"
-        tools:text="Reset"
-        tools:drawableStart="@android:drawable/ic_menu_add"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toStartOf="@id/altButton"
         app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
@@ -101,15 +99,23 @@
         />
 
     <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+        style="@*android:style/NotificationEmphasizedAction"
         android:id="@+id/altButton"
-        tools:text="Reset"
-        tools:drawableStart="@android:drawable/ic_menu_add"
-        android:drawablePadding="2dp"
-        android:drawableTint="@android:color/white"
         android:layout_width="124dp"
         android:layout_height="wrap_content"
         app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
         app:layout_constraintStart_toEndOf="@id/mainButton"
+        app:layout_constraintEnd_toEndOf="@id/resetButton"
+        android:paddingEnd="4dp"
+        />
+
+    <com.android.systemui.statusbar.notification.row.ui.view.TimerButtonView
+        style="@*android:style/NotificationEmphasizedAction"
+        android:id="@+id/resetButton"
+        android:layout_width="124dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toBottomOf="@id/bottomOfTop"
+        app:layout_constraintStart_toEndOf="@id/altButton"
         app:layout_constraintEnd_toEndOf="parent"
         android:paddingEnd="4dp"
         />
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index be28504..8322b6c 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1093,6 +1093,12 @@
     <!-- Priority modes dialog settings shortcut button [CHAR LIMIT=15] -->
     <string name="zen_modes_dialog_settings">Settings</string>
 
+    <!-- Priority modes: label for an active mode [CHAR LIMIT=35] -->
+    <string name="zen_mode_on">On</string>
+
+    <!-- Priority modes: label for an inactive mode [CHAR LIMIT=35] -->
+    <string name="zen_mode_off">Off</string>
+
     <!-- Zen mode: Priority only introduction message on first use -->
     <string name="zen_priority_introduction">You won\'t be disturbed by sounds and vibrations, except from alarms, reminders, events, and callers you specify. You\'ll still hear anything you choose to play including music, videos, and games.</string>
 
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index af7ecf6..1ba274f 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -28,6 +28,8 @@
 import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.ComposeLockscreen
+import com.android.systemui.qs.flags.NewQsUI
+import com.android.systemui.qs.flags.QSComposeFragment
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.shared.flag.DualShade
 import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
@@ -66,14 +68,20 @@
 
         // DualShade dependencies
         DualShade.token dependsOn SceneContainerFlag.getMainAconfigFlag()
+
+        // QS Fragment using Compose dependencies
+        QSComposeFragment.token dependsOn NewQsUI.token
     }
 
     private inline val politeNotifications
         get() = FlagToken(FLAG_POLITE_NOTIFICATIONS, politeNotifications())
+
     private inline val crossAppPoliteNotifications
         get() = FlagToken(FLAG_CROSS_APP_POLITE_NOTIFICATIONS, crossAppPoliteNotifications())
+
     private inline val vibrateWhileUnlockedToken: FlagToken
         get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked())
+
     private inline val communalHub
         get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub())
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt
index 8af5665..ee709c4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/flags/NewQsUI.kt
@@ -20,7 +20,7 @@
 import com.android.systemui.flags.FlagToken
 import com.android.systemui.flags.RefactorFlagUtils
 
-/** Helper for reading or using the notification avalanche suppression flag state. */
+/** Helper for reading or using the new QS UI flag state. */
 @Suppress("NOTHING_TO_INLINE")
 object NewQsUI {
     /** The aconfig flag name */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt
new file mode 100644
index 0000000..664d496
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/flags/QSComposeFragment.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.qs.flags
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the new QS UI in NPVC flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object QSComposeFragment {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.qsUiRefactorComposeFragment() && NewQsUI.isEnabled
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index 4f6a64f..bd08685 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -1265,20 +1265,20 @@
             mTranslationForFullShadeTransition = qsTranslation;
             updateQsFrameTranslation();
             float currentTranslation = mQsFrame.getTranslationY();
-            int clipTop = mEnableClipping
-                    ? (int) (top - currentTranslation - mQsFrame.getTop()) : 0;
-            int clipBottom = mEnableClipping
-                    ? (int) (bottom - currentTranslation - mQsFrame.getTop()) : 0;
+            int clipTop = (int) (top - currentTranslation - mQsFrame.getTop());
+            int clipBottom = (int) (bottom - currentTranslation - mQsFrame.getTop());
             mVisible = qsVisible;
             mQs.setQsVisible(qsVisible);
-            mQs.setFancyClipping(
-                    mDisplayLeftInset,
-                    clipTop,
-                    mDisplayRightInset,
-                    clipBottom,
-                    radius,
-                    qsVisible && !mSplitShadeEnabled,
-                    mIsFullWidth);
+            if (mEnableClipping) {
+                mQs.setFancyClipping(
+                        mDisplayLeftInset,
+                        clipTop,
+                        mDisplayRightInset,
+                        clipBottom,
+                        radius,
+                        qsVisible && !mSplitShadeEnabled,
+                        mIsFullWidth);
+            }
 
         }
 
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 b8af369..fe86375 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
@@ -122,12 +122,15 @@
                 val timeRemaining = parseTimeDelta(remaining)
                 TimerContentModel(
                     icon = icon,
-                    name = total,
+                    // TODO: b/352142761 - define and use a string resource rather than " Timer".
+                    // (The UX isn't final so using " Timer" for now).
+                    name = total.replace("Σ", "") + " Timer",
                     state =
                         TimerContentModel.TimerState.Paused(
                             timeRemaining = timeRemaining,
-                            resumeIntent = notification.findActionWithName("Resume"),
-                            resetIntent = notification.findActionWithName("Reset"),
+                            resumeIntent = notification.findStartIntent(),
+                            addMinuteAction = notification.findAddMinuteAction(),
+                            resetAction = notification.findResetAction(),
                         )
                 )
             }
@@ -136,12 +139,15 @@
                 val finishTime = parseCurrentTime(current) + parseTimeDelta(remaining).toMillis()
                 TimerContentModel(
                     icon = icon,
-                    name = total,
+                    // TODO: b/352142761 - define and use a string resource rather than " Timer".
+                    // (The UX isn't final so using " Timer" for now).
+                    name = total.replace("Σ", "") + " Timer",
                     state =
                         TimerContentModel.TimerState.Running(
                             finishTime = finishTime,
-                            pauseIntent = notification.findActionWithName("Pause"),
-                            addOneMinuteIntent = notification.findActionWithName("Add 1 min"),
+                            pauseIntent = notification.findPauseIntent(),
+                            addMinuteAction = notification.findAddMinuteAction(),
+                            resetAction = notification.findResetAction(),
                         )
                 )
             }
@@ -149,8 +155,34 @@
         }
     }
 
-    private fun Notification.findActionWithName(name: String): PendingIntent? {
-        return actions.firstOrNull { name == it.title?.toString() }?.actionIntent
+    private fun Notification.findPauseIntent(): PendingIntent? {
+        return actions
+            .firstOrNull { it.actionIntent.intent?.action?.endsWith(".PAUSE_TIMER") == true }
+            ?.actionIntent
+    }
+
+    private fun Notification.findStartIntent(): PendingIntent? {
+        return actions
+            .firstOrNull { it.actionIntent.intent?.action?.endsWith(".START_TIMER") == true }
+            ?.actionIntent
+    }
+
+    // TODO: b/352142761 - switch to system attributes for label and icon.
+    //   - We probably want a consistent look for the Reset button. (Double check with UX.)
+    //   - Using the custom assets now since I couldn't an existing "Reset" icon.
+    private fun Notification.findResetAction(): Notification.Action? {
+        return actions.firstOrNull {
+            it.actionIntent.intent?.action?.endsWith(".RESET_TIMER") == true
+        }
+    }
+
+    // TODO: b/352142761 - check with UX on whether this should be required.
+    //   - Alternative is to allow for optional actions in addition to main and reset.
+    //   - For optional actions, we should take the custom label and icon.
+    private fun Notification.findAddMinuteAction(): Notification.Action? {
+        return actions.firstOrNull {
+            it.actionIntent.intent?.action?.endsWith(".ADD_MINUTE_TIMER") == true
+        }
     }
 
     private fun parseCurrentTime(current: String): Long {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
index 5584701..33b2564 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/RichOngoingClock.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.notification.row.shared
 
+import android.app.Notification
 import android.app.PendingIntent
 import java.time.Duration
 
@@ -32,6 +33,9 @@
 ) : RichOngoingContentModel {
     /** The state (paused or running) of the timer, and relevant time */
     sealed interface TimerState {
+        val addMinuteAction: Notification.Action?
+        val resetAction: Notification.Action?
+
         /**
          * Indicates a running timer
          *
@@ -41,7 +45,8 @@
         data class Running(
             val finishTime: Long,
             val pauseIntent: PendingIntent?,
-            val addOneMinuteIntent: PendingIntent?,
+            override val addMinuteAction: Notification.Action?,
+            override val resetAction: Notification.Action?,
         ) : TimerState
 
         /**
@@ -53,7 +58,8 @@
         data class Paused(
             val timeRemaining: Duration,
             val resumeIntent: PendingIntent?,
-            val resetIntent: PendingIntent?,
+            override val addMinuteAction: Notification.Action?,
+            override val resetAction: Notification.Action?,
         ) : TimerState
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
index 0d83ace..8c95187 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerButtonView.kt
@@ -18,8 +18,9 @@
 
 import android.annotation.DrawableRes
 import android.content.Context
+import android.graphics.BlendMode
 import android.util.AttributeSet
-import android.widget.Button
+import com.android.internal.widget.EmphasizedNotificationButton
 
 class TimerButtonView
 @JvmOverloads
@@ -28,14 +29,19 @@
     attrs: AttributeSet? = null,
     defStyleAttr: Int = 0,
     defStyleRes: Int = 0,
-) : Button(context, attrs, defStyleAttr, defStyleRes) {
+) : EmphasizedNotificationButton(context, attrs, defStyleAttr, defStyleRes) {
 
     private val Int.dp: Int
         get() = (this * context.resources.displayMetrics.density).toInt()
 
     fun setIcon(@DrawableRes icon: Int) {
         val drawable = context.getDrawable(icon)
+
+        drawable?.mutate()
+        drawable?.setTintList(textColors)
+        drawable?.setTintBlendMode(BlendMode.SRC_IN)
         drawable?.setBounds(0, 0, 24.dp, 24.dp)
+
         setCompoundDrawablesRelative(drawable, null, null, null)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
index 2e164d6..d481b50 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/TimerView.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.notification.row.ui.view
 
 import android.content.Context
-import android.graphics.drawable.Drawable
+import android.graphics.drawable.Icon
 import android.os.SystemClock
 import android.util.AttributeSet
 import android.widget.Chronometer
@@ -48,6 +48,9 @@
     lateinit var altButton: TimerButtonView
         private set
 
+    lateinit var resetButton: TimerButtonView
+        private set
+
     override fun onFinishInflate() {
         super.onFinishInflate()
         icon = requireViewById(R.id.icon)
@@ -56,13 +59,14 @@
         pausedTimeRemaining = requireViewById(R.id.pausedTimeRemaining)
         mainButton = requireViewById(R.id.mainButton)
         altButton = requireViewById(R.id.altButton)
+        resetButton = requireViewById(R.id.resetButton)
     }
 
     /** the resources configuration has changed such that the view needs to be reinflated */
     fun isReinflateNeeded(): Boolean = configTracker.hasUnhandledConfigChange()
 
-    fun setIcon(iconDrawable: Drawable?) {
-        this.icon.setImageDrawable(iconDrawable)
+    fun setIcon(icon: Icon?) {
+        this.icon.setImageIcon(icon)
     }
 
     fun setLabel(label: String) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
index c9ff589..042d1bc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/TimerViewBinder.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification.row.ui.viewbinder
 
+import android.content.res.ColorStateList
+import android.graphics.drawable.Icon
 import android.view.View
 import androidx.core.view.isGone
 import androidx.lifecycle.lifecycleScope
@@ -46,12 +48,43 @@
         launch { viewModel.countdownTime.collect { view.setCountdownTime(it) } }
         launch { viewModel.mainButtonModel.collect { bind(view.mainButton, it) } }
         launch { viewModel.altButtonModel.collect { bind(view.altButton, it) } }
+        launch { viewModel.resetButtonModel.collect { bind(view.resetButton, it) } }
     }
 
     fun bind(buttonView: TimerButtonView, model: TimerViewModel.ButtonViewModel?) {
         if (model != null) {
-            buttonView.setIcon(model.iconRes)
-            buttonView.setText(model.labelRes)
+            buttonView.setButtonBackground(
+                ColorStateList.valueOf(
+                    buttonView.context.getColor(com.android.internal.R.color.system_accent2_100)
+                )
+            )
+            buttonView.setTextColor(
+                buttonView.context.getColor(
+                    com.android.internal.R.color.notification_primary_text_color_light
+                )
+            )
+
+            when (model) {
+                is TimerViewModel.ButtonViewModel.WithSystemAttrs -> {
+                    buttonView.setIcon(model.iconRes)
+                    buttonView.setText(model.labelRes)
+                }
+                is TimerViewModel.ButtonViewModel.WithCustomAttrs -> {
+                    // TODO: b/352142761 - is there a better way to deal with TYPE_RESOURCE icons
+                    // with empty resPackage? RemoteViews handles this by using a  different
+                    // `contextForResources` for inflation.
+                    val icon =
+                        if (model.icon.type == Icon.TYPE_RESOURCE && model.icon.resPackage == "")
+                            Icon.createWithResource(
+                                "com.google.android.deskclock",
+                                model.icon.resId
+                            )
+                        else model.icon
+                    buttonView.setImageIcon(icon)
+                    buttonView.text = model.label
+                }
+            }
+
             buttonView.setOnClickListener(
                 model.pendingIntent?.let { pendingIntent ->
                     View.OnClickListener { pendingIntent.send() }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
index a85c87f..768a093 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/TimerViewModel.kt
@@ -19,7 +19,7 @@
 import android.annotation.DrawableRes
 import android.annotation.StringRes
 import android.app.PendingIntent
-import android.graphics.drawable.Drawable
+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
@@ -44,7 +44,7 @@
 
     private val state: Flow<TimerState> = rowInteractor.timerContentModel.mapNotNull { it.state }
 
-    val icon: Flow<Drawable?> = rowInteractor.timerContentModel.mapNotNull { it.icon.drawable }
+    val icon: Flow<Icon?> = rowInteractor.timerContentModel.mapNotNull { it.icon.icon }
 
     val label: Flow<String> = rowInteractor.timerContentModel.mapNotNull { it.name }
 
@@ -57,13 +57,13 @@
         state.map {
             when (it) {
                 is TimerState.Paused ->
-                    ButtonViewModel(
+                    ButtonViewModel.WithSystemAttrs(
                         it.resumeIntent,
                         com.android.systemui.res.R.string.controls_media_resume, // "Resume",
                         com.android.systemui.res.R.drawable.ic_media_play
                     )
                 is TimerState.Running ->
-                    ButtonViewModel(
+                    ButtonViewModel.WithSystemAttrs(
                         it.pauseIntent,
                         com.android.systemui.res.R.string.controls_media_button_pause, // "Pause",
                         com.android.systemui.res.R.drawable.ic_media_pause
@@ -73,31 +73,41 @@
 
     val altButtonModel: Flow<ButtonViewModel?> =
         state.map {
-            when (it) {
-                is TimerState.Paused ->
-                    it.resetIntent?.let { resetIntent ->
-                        ButtonViewModel(
-                            resetIntent,
-                            com.android.systemui.res.R.string.reset, // "Reset",
-                            com.android.systemui.res.R.drawable.ic_close_white_rounded
-                        )
-                    }
-                is TimerState.Running ->
-                    it.addOneMinuteIntent?.let { addOneMinuteIntent ->
-                        ButtonViewModel(
-                            addOneMinuteIntent,
-                            com.android.systemui.res.R.string.add, // "Add 1 minute",
-                            com.android.systemui.res.R.drawable.ic_add
-                        )
-                    }
+            it.addMinuteAction?.let { action ->
+                ButtonViewModel.WithCustomAttrs(
+                    action.actionIntent,
+                    action.title, // "1:00",
+                    action.getIcon()
+                )
             }
         }
 
-    data class ButtonViewModel(
-        val pendingIntent: PendingIntent?,
-        @StringRes val labelRes: Int,
-        @DrawableRes val iconRes: Int,
-    )
+    val resetButtonModel: Flow<ButtonViewModel?> =
+        state.map {
+            it.resetAction?.let { action ->
+                ButtonViewModel.WithCustomAttrs(
+                    action.actionIntent,
+                    action.title, // "Reset",
+                    action.getIcon()
+                )
+            }
+        }
+
+    sealed interface ButtonViewModel {
+        val pendingIntent: PendingIntent?
+
+        data class WithSystemAttrs(
+            override val pendingIntent: PendingIntent?,
+            @StringRes val labelRes: Int,
+            @DrawableRes val iconRes: Int,
+        ) : ButtonViewModel
+
+        data class WithCustomAttrs(
+            override val pendingIntent: PendingIntent?,
+            val label: CharSequence,
+            val icon: Icon,
+        ) : ButtonViewModel
+    }
 }
 
 private fun Duration.format(): String {
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 e4d0668..7a521a6 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
@@ -16,8 +16,14 @@
 
 package com.android.systemui.statusbar.policy.domain.interactor
 
+import android.content.Context
 import android.provider.Settings
+import androidx.concurrent.futures.await
 import com.android.settingslib.notification.data.repository.ZenModeRepository
+import com.android.settingslib.notification.modes.ZenIconLoader
+import com.android.settingslib.notification.modes.ZenMode
+import com.android.systemui.common.shared.model.Icon
+import java.time.Duration
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
@@ -28,7 +34,9 @@
  * An interactor that performs business logic related to the status and configuration of Zen Mode
  * (or Do Not Disturb/DND Mode).
  */
-class ZenModeInteractor @Inject constructor(repository: ZenModeRepository) {
+class ZenModeInteractor @Inject constructor(private val repository: ZenModeRepository) {
+    private val iconLoader: ZenIconLoader = ZenIconLoader.getInstance()
+
     val isZenModeEnabled: Flow<Boolean> =
         repository.globalZenMode
             .map {
@@ -52,4 +60,18 @@
                 }
             }
             .distinctUntilChanged()
+
+    val modes: Flow<List<ZenMode>> = repository.modes
+
+    suspend fun getModeIcon(mode: ZenMode, context: Context): Icon {
+        return Icon.Loaded(mode.getIcon(context, iconLoader).await(), contentDescription = null)
+    }
+
+    fun activateMode(zenMode: ZenMode, duration: Duration? = null) {
+        repository.activateMode(zenMode, duration)
+    }
+
+    fun deactivateMode(zenMode: ZenMode) {
+        repository.deactivateMode(zenMode)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
index 6db1eac..2b094d6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
@@ -29,6 +29,8 @@
 import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.statusbar.phone.SystemUIDialogFactory
 import com.android.systemui.statusbar.phone.create
+import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid
+import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel
 import javax.inject.Inject
 
 class ModesDialogDelegate
@@ -37,12 +39,13 @@
     private val sysuiDialogFactory: SystemUIDialogFactory,
     private val dialogTransitionAnimator: DialogTransitionAnimator,
     private val activityStarter: ActivityStarter,
+    private val viewModel: ModesDialogViewModel,
 ) : SystemUIDialog.Delegate {
     override fun createDialog(): SystemUIDialog {
         return sysuiDialogFactory.create { dialog ->
             AlertDialogContent(
                 title = { Text(stringResource(R.string.zen_modes_dialog_title)) },
-                content = { Text("Under construction") },
+                content = { ModeTileGrid(viewModel) },
                 neutralButton = {
                     PlatformOutlinedButton(
                         onClick = {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
new file mode 100644
index 0000000..91bfdff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.policy.ui.dialog.composable
+
+import androidx.compose.foundation.basicMarquee
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModeTileViewModel
+
+@Composable
+fun ModeTile(viewModel: ModeTileViewModel) {
+    val tileColor =
+        if (viewModel.enabled) MaterialTheme.colorScheme.primary
+        else MaterialTheme.colorScheme.surfaceVariant
+    val contentColor =
+        if (viewModel.enabled) MaterialTheme.colorScheme.onPrimary
+        else MaterialTheme.colorScheme.onSurfaceVariant
+
+    CompositionLocalProvider(LocalContentColor provides contentColor) {
+        Surface(
+            color = tileColor,
+            shape = RoundedCornerShape(16.dp),
+            modifier =
+                Modifier.combinedClickable(
+                    onClick = viewModel.onClick,
+                    onLongClick = viewModel.onLongClick
+                ),
+        ) {
+            Row(
+                modifier = Modifier.padding(20.dp),
+                verticalAlignment = Alignment.CenterVertically,
+                horizontalArrangement =
+                    Arrangement.spacedBy(
+                        space = 10.dp,
+                        alignment = Alignment.Start,
+                    ),
+            ) {
+                Icon(icon = viewModel.icon, modifier = Modifier.size(24.dp))
+                Column {
+                    Text(
+                        viewModel.text,
+                        fontWeight = FontWeight.W500,
+                        modifier = Modifier.tileMarquee()
+                    )
+                    Text(
+                        viewModel.subtext,
+                        fontWeight = FontWeight.W400,
+                        modifier = Modifier.tileMarquee()
+                    )
+                }
+            }
+        }
+    }
+}
+
+private fun Modifier.tileMarquee(): Modifier {
+    return this.basicMarquee(
+        iterations = 1,
+        initialDelayMillis = 200,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt
new file mode 100644
index 0000000..73d361f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.policy.ui.dialog.composable
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel
+
+@Composable
+fun ModeTileGrid(viewModel: ModesDialogViewModel) {
+    val tiles by viewModel.tiles.collectAsStateWithLifecycle(initialValue = emptyList())
+
+    // TODO(b/346519570): Handle what happens when we have more than a few modes.
+    LazyVerticalGrid(
+        columns = GridCells.Fixed(2),
+        modifier = Modifier.padding(8.dp).fillMaxWidth().heightIn(max = 300.dp),
+        verticalArrangement = Arrangement.spacedBy(8.dp),
+        horizontalArrangement = Arrangement.spacedBy(8.dp),
+    ) {
+        items(
+            tiles.size,
+            key = { index -> tiles[index].id },
+        ) { index ->
+            ModeTile(viewModel = tiles[index])
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt
new file mode 100644
index 0000000..5bd26cc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.policy.ui.dialog.viewmodel
+
+import com.android.systemui.common.shared.model.Icon
+
+/**
+ * Viewmodel for a tile representing a single priority ("zen") mode, for use within the modes
+ * dialog. Not to be confused with ModesTile, which is the Quick Settings tile that opens the
+ * dialog.
+ */
+data class ModeTileViewModel(
+    val id: String,
+    val icon: Icon,
+    val text: String,
+    val subtext: String,
+    val enabled: Boolean,
+    val contentDescription: String,
+    val onClick: () -> Unit,
+    val onLongClick: () -> Unit,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
new file mode 100644
index 0000000..e84c8b6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.policy.ui.dialog.viewmodel
+
+import android.content.Context
+import com.android.settingslib.notification.modes.ZenMode
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+/**
+ * Viewmodel for the priority ("zen") modes dialog that can be opened from quick settings. It allows
+ * the user to quickly toggle modes.
+ */
+@SysUISingleton
+class ModesDialogViewModel
+@Inject
+constructor(
+    val context: Context,
+    zenModeInteractor: ZenModeInteractor,
+    @Background val bgDispatcher: CoroutineDispatcher,
+) {
+    // Modes that should be displayed in the dialog
+    // TODO(b/346519570): Include modes that have not been set up yet.
+    private val visibleModes: Flow<List<ZenMode>> =
+        zenModeInteractor.modes.map {
+            it.filter { mode ->
+                mode.rule.isEnabled && (mode.isActive || mode.rule.isManualInvocationAllowed)
+            }
+        }
+
+    val tiles: Flow<List<ModeTileViewModel>> =
+        visibleModes
+            .map { modesList ->
+                modesList.map { mode ->
+                    ModeTileViewModel(
+                        id = mode.id,
+                        icon = zenModeInteractor.getModeIcon(mode, context),
+                        text = mode.rule.name,
+                        subtext = getTileSubtext(mode),
+                        enabled = mode.isActive,
+                        // TODO(b/346519570): This should be some combination of the above, e.g.
+                        //  "ON: Do Not Disturb, Until Mon 08:09"; see DndTile.
+                        contentDescription = "",
+                        onClick = {
+                            if (mode.isActive) {
+                                zenModeInteractor.deactivateMode(mode)
+                            } else {
+                                // TODO(b/346519570): Handle duration for DND mode.
+                                zenModeInteractor.activateMode(mode)
+                            }
+                        },
+                        onLongClick = {
+                            // TODO(b/346519570): Open settings page for mode.
+                        }
+                    )
+                }
+            }
+            .flowOn(bgDispatcher)
+
+    private fun getTileSubtext(mode: ZenMode): String {
+        // TODO(b/346519570): Use ZenModeConfig.getDescription for manual DND
+        val on = context.resources.getString(R.string.zen_mode_on)
+        val off = context.resources.getString(R.string.zen_mode_off)
+        return mode.rule.triggerDescription ?: if (mode.isActive) on else off
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 49e3f04..31f93b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -1059,6 +1059,7 @@
 
     @Test
     @DisableSceneContainer
+    @DisableFlags(Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART)
     public void testShowBouncerOrKeyguard_needsFullScreen() {
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
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
new file mode 100644
index 0000000..db95fad
--- /dev/null
+++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java
@@ -0,0 +1,51 @@
+/*
+ * 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
new file mode 100644
index 0000000..ddc66c7
--- /dev/null
+++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java
@@ -0,0 +1,51 @@
+/*
+ * 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
new file mode 100644
index 0000000..ec8e907
--- /dev/null
+++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java
@@ -0,0 +1,51 @@
+/*
+ * 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
new file mode 100644
index 0000000..d952d07
--- /dev/null
+++ b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index 49e793f..4357f2b 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -33,14 +33,17 @@
 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.PrintStream;
+import java.lang.annotation.Annotation;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
@@ -230,6 +233,18 @@
         }
     }
 
+    /**
+     * @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();
@@ -239,13 +254,14 @@
         boolean hasErrors = false;
         for (Method m : collectMethods(testClass)) {
             if (Modifier.isPublic(m.getModifiers()) && m.getName().startsWith("test")) {
-                if (m.getAnnotation(Test.class) == null) {
+                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 (m.getAnnotation(Before.class) == null) {
+                if (!hasAnyAnnotations(m, Before.class)) {
                     message.append("\nMethod " + m.getName() + "() doesn't have @Before");
                     hasErrors = true;
                 }
@@ -255,7 +271,7 @@
                 }
             }
             if ("tearDown".equals(m.getName())) {
-                if (m.getAnnotation(After.class) == null) {
+                if (!hasAnyAnnotations(m, After.class)) {
                     message.append("\nMethod " + m.getName() + "() doesn't have @After");
                     hasErrors = true;
                 }
diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
index d7da2f0..a5ec2ba 100644
--- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
@@ -804,7 +804,15 @@
                     + event.mSuggestionPresentedLastTimestampMs
                     + " event.mFocusedVirtualAutofillId=" + event.mFocusedVirtualAutofillId
                     + " event.mFieldFirstLength=" + event.mFieldFirstLength
-                    + " event.mFieldLastLength=" + event.mFieldLastLength);
+                    + " event.mFieldLastLength=" + event.mFieldLastLength
+                    + " event.mViewFailedPriorToRefillCount=" + event.mViewFailedPriorToRefillCount
+                    + " event.mViewFilledSuccessfullyOnRefillCount="
+                    + event.mViewFilledSuccessfullyOnRefillCount
+                    + " event.mViewFailedOnRefillCount=" + event.mViewFailedOnRefillCount
+                    + " event.notExpiringResponseDuringAuthCount="
+                    + event.mFixExpireResponseDuringAuthCount
+                    + " event.notifyViewEnteredIgnoredDuringAuthCount="
+                    + event.mNotifyViewEnteredIgnoredDuringAuthCount);
         }
 
         // TODO(b/234185326): Distinguish empty responses from other no presentation reasons.
@@ -859,7 +867,12 @@
                 event.mSuggestionPresentedLastTimestampMs,
                 event.mFocusedVirtualAutofillId,
                 event.mFieldFirstLength,
-                event.mFieldLastLength);
+                event.mFieldLastLength,
+                event.mViewFailedPriorToRefillCount,
+                event.mViewFilledSuccessfullyOnRefillCount,
+                event.mViewFailedOnRefillCount,
+                event.mFixExpireResponseDuringAuthCount,
+                event.mNotifyViewEnteredIgnoredDuringAuthCount);
         mEventInternal = Optional.empty();
     }
 
@@ -912,6 +925,12 @@
         // uninitialized doesn't help much, as this would be non-zero only if callback is received.
         int mViewFillSuccessCount = 0;
         int mViewFilledButUnexpectedCount = 0;
+        int mViewFailedPriorToRefillCount = 0;
+        int mViewFailedOnRefillCount = 0;
+        int mViewFilledSuccessfullyOnRefillCount = 0;
+
+        int mFixExpireResponseDuringAuthCount = 0;
+        int mNotifyViewEnteredIgnoredDuringAuthCount = 0;
 
         ArraySet<AutofillId> mAutofillIdsAttemptedAutofill;
         ArraySet<AutofillId> mAlreadyFilledAutofillIds = new ArraySet<>();
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 092ee16..67985ef 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -102,7 +102,6 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IBatteryStats;
 import com.android.internal.os.BinderCallsStats;
 import com.android.internal.os.Clock;
@@ -1192,7 +1191,7 @@
                                     .setMinConsumedPowerThreshold(minConsumedPowerThreshold)
                                     .build();
                     bus = getBatteryUsageStats(List.of(query)).get(0);
-                    return new StatsPerUidLogger(new FrameworkStatsLogger()).logStats(bus, data);
+                    return StatsPerUidLogger.logStats(bus, data);
                 }
                 default:
                     throw new UnsupportedOperationException("Unknown tagId=" + atomTag);
@@ -1205,35 +1204,7 @@
         }
     }
 
-    public static class FrameworkStatsLogger {
-        /**
-         * Wrapper for the FrameworkStatsLog.buildStatsEvent method that makes it easier
-         * for mocking.
-         */
-        @VisibleForTesting
-        public StatsEvent buildStatsEvent(long sessionStartTs, long sessionEndTs,
-                long sessionDuration, int sessionDischargePercentage, long sessionDischargeDuration,
-                int uid, @BatteryConsumer.ProcessState int processState, long timeInStateMillis,
-                String powerComponentName, float totalConsumedPowerMah, float powerComponentMah,
-                long powerComponentDurationMillis) {
-            return FrameworkStatsLog.buildStatsEvent(
-                    FrameworkStatsLog.BATTERY_USAGE_STATS_PER_UID,
-                    sessionStartTs,
-                    sessionEndTs,
-                    sessionDuration,
-                    sessionDischargePercentage,
-                    sessionDischargeDuration,
-                    uid,
-                    processState,
-                    timeInStateMillis,
-                    powerComponentName,
-                    totalConsumedPowerMah,
-                    powerComponentMah,
-                    powerComponentDurationMillis);
-        }
-    }
-
-    public static class StatsPerUidLogger {
+    private static class StatsPerUidLogger {
 
         private static final int STATSD_METRIC_MAX_DIMENSIONS_COUNT = 3000;
 
@@ -1253,18 +1224,7 @@
                 long dischargeDuration) {}
         ;
 
-        private final FrameworkStatsLogger mFrameworkStatsLogger;
-
-        public StatsPerUidLogger(FrameworkStatsLogger frameworkStatsLogger) {
-            mFrameworkStatsLogger = frameworkStatsLogger;
-        }
-
-        /**
-         * Generates StatsEvents for the supplied battery usage stats and adds them to
-         * the supplied list.
-         */
-        @VisibleForTesting
-        public int logStats(BatteryUsageStats bus, List<StatsEvent> data) {
+        static int logStats(BatteryUsageStats bus, List<StatsEvent> data) {
             final SessionInfo sessionInfo =
                     new SessionInfo(
                             bus.getStatsStartTimestamp(),
@@ -1380,7 +1340,7 @@
             return StatsManager.PULL_SUCCESS;
         }
 
-        private boolean addStatsForPredefinedComponent(
+        private static boolean addStatsForPredefinedComponent(
                 List<StatsEvent> data,
                 SessionInfo sessionInfo,
                 int uid,
@@ -1420,7 +1380,7 @@
                     powerComponentDurationMillis);
         }
 
-        private boolean addStatsForCustomComponent(
+        private static boolean addStatsForCustomComponent(
                 List<StatsEvent> data,
                 SessionInfo sessionInfo,
                 int uid,
@@ -1462,7 +1422,7 @@
          * Returns true on success and false if reached max atoms capacity and no more atoms should
          * be added
          */
-        private boolean addStatsAtom(
+        private static boolean addStatsAtom(
                 List<StatsEvent> data,
                 SessionInfo sessionInfo,
                 int uid,
@@ -1472,7 +1432,9 @@
                 float totalConsumedPowerMah,
                 float powerComponentMah,
                 long powerComponentDurationMillis) {
-            data.add(mFrameworkStatsLogger.buildStatsEvent(
+            data.add(
+                    FrameworkStatsLog.buildStatsEvent(
+                            FrameworkStatsLog.BATTERY_USAGE_STATS_PER_UID,
                             sessionInfo.startTs(),
                             sessionInfo.endTs(),
                             sessionInfo.duration(),
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index b346767..ecbbd46 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1441,9 +1441,9 @@
                 (windowToken, imeVisible) -> {
                     if (Flags.refactorInsetsController()) {
                         if (imeVisible) {
-                            showSoftInputInternal(windowToken);
+                            showCurrentInputInternal(windowToken);
                         } else {
-                            hideSoftInputInternal(windowToken);
+                            hideCurrentInputInternal(windowToken);
                         }
                     }
                 });
@@ -1919,7 +1919,7 @@
         if (Flags.refactorInsetsController()) {
             if (isShowRequestedForCurrentWindow(userId) && userData.mImeBindingState != null
                     && userData.mImeBindingState.mFocusedWindow != null) {
-                showSoftInputInternal(userData.mImeBindingState.mFocusedWindow);
+                showCurrentInputInternal(userData.mImeBindingState.mFocusedWindow);
             }
         } else {
             if (isShowRequestedForCurrentWindow(userId)) {
@@ -3151,8 +3151,8 @@
         }
     }
 
-    boolean showSoftInputInternal(IBinder windowToken) {
-        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showSoftInputInternal");
+    boolean showCurrentInputInternal(IBinder windowToken) {
+        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showCurrentInputInternal");
         ImeTracing.getInstance().triggerManagerServiceDump(
                 "InputMethodManagerService#showSoftInput", mDumper);
         synchronized (ImfLock.class) {
@@ -3171,8 +3171,8 @@
         }
     }
 
-    boolean hideSoftInputInternal(IBinder windowToken) {
-        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideSoftInputInternal");
+    boolean hideCurrentInputInternal(IBinder windowToken) {
+        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideCurrentInputInternal");
         ImeTracing.getInstance().triggerManagerServiceDump(
                 "InputMethodManagerService#hideSoftInput", mDumper);
         synchronized (ImfLock.class) {
diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java
index 1f341147..e0c0c2c 100644
--- a/services/core/java/com/android/server/wm/DesktopModeHelper.java
+++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java
@@ -22,7 +22,7 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.window.flags.Flags;
+import com.android.server.wm.utils.DesktopModeFlagsUtil;
 
 /**
  * Constants for desktop mode feature
@@ -35,8 +35,8 @@
             "persist.wm.debug.desktop_mode_enforce_device_restrictions", true);
 
     /** Whether desktop mode is enabled. */
-    static boolean isDesktopModeEnabled() {
-        return Flags.enableDesktopWindowingMode();
+    static boolean isDesktopModeEnabled(@NonNull Context context) {
+        return DesktopModeFlagsUtil.DESKTOP_WINDOWING_MODE.isEnabled(context);
     }
 
     /**
@@ -60,7 +60,7 @@
      * Return {@code true} if desktop mode can be entered on the current device.
      */
     static boolean canEnterDesktopMode(@NonNull Context context) {
-        return isDesktopModeEnabled()
+        return isDesktopModeEnabled(context)
                 && (!shouldEnforceDeviceRestrictions() || isDesktopModeSupported(context));
     }
 }
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 3483842..dcadb0f 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -397,9 +397,11 @@
                 onRequestedVisibleTypesChanged(newControlTargets.valueAt(i));
             }
             newControlTargets.clear();
-            // Check for and try to run the scheduled show IME request (if it exists), as we
-            // now applied the surface transaction and notified the target of the new control.
-            getImeSourceProvider().checkAndStartShowImePostLayout();
+            if (!android.view.inputmethod.Flags.refactorInsetsController()) {
+                // Check for and try to run the scheduled show IME request (if it exists), as we
+                // now applied the surface transaction and notified the target of the new control.
+                getImeSourceProvider().checkAndStartShowImePostLayout();
+            }
         });
     }
 
diff --git a/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java
new file mode 100644
index 0000000..4211764
--- /dev/null
+++ b/services/core/java/com/android/server/wm/utils/DesktopModeFlagsUtil.java
@@ -0,0 +1,173 @@
+/*
+ * 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.server.wm.utils;
+
+import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.window.flags.Flags;
+
+import java.util.function.Supplier;
+
+/**
+ * Util to check desktop mode flags state.
+ *
+ * This utility is used to allow developer option toggles to override flags related to desktop
+ * windowing.
+ *
+ * Computes whether Desktop Windowing related flags should be enabled by using the aconfig flag
+ * value and the developer option override state (if applicable).
+ *
+ * This is a partial copy of {@link com.android.wm.shell.shared.desktopmode.DesktopModeFlags} which
+ * is to be used in WM core.
+ */
+public enum DesktopModeFlagsUtil {
+    // All desktop mode related flags to be overridden by developer option toggle will be added here
+    DESKTOP_WINDOWING_MODE(
+            Flags::enableDesktopWindowingMode, /* shouldOverrideByDevOption= */ true),
+    WALLPAPER_ACTIVITY(
+            Flags::enableDesktopWindowingWallpaperActivity, /* shouldOverrideByDevOption= */ true);
+
+    private static final String TAG = "DesktopModeFlagsUtil";
+    private static final String SYSTEM_PROPERTY_OVERRIDE_KEY =
+            "sys.wmshell.desktopmode.dev_toggle_override";
+
+    // Function called to obtain aconfig flag value.
+    private final Supplier<Boolean> mFlagFunction;
+    // Whether the flag state should be affected by developer option.
+    private final boolean mShouldOverrideByDevOption;
+
+    // Local cache for toggle override, which is initialized once on its first access. It needs to
+    // be refreshed only on reboots as overridden state takes effect on reboots.
+    private static ToggleOverride sCachedToggleOverride;
+
+    DesktopModeFlagsUtil(Supplier<Boolean> flagFunction, boolean shouldOverrideByDevOption) {
+        this.mFlagFunction = flagFunction;
+        this.mShouldOverrideByDevOption = shouldOverrideByDevOption;
+    }
+
+    /**
+     * Determines state of flag based on the actual flag and desktop mode developer option
+     * overrides.
+     *
+     * Note: this method makes sure that a constant developer toggle overrides is read until
+     * reboot.
+     */
+    public boolean isEnabled(Context context) {
+        if (!Flags.showDesktopWindowingDevOption()
+                || !mShouldOverrideByDevOption
+                || context.getContentResolver() == null) {
+            return mFlagFunction.get();
+        } else {
+            boolean shouldToggleBeEnabledByDefault = Flags.enableDesktopWindowingMode();
+            return switch (getToggleOverride(context)) {
+                case OVERRIDE_UNSET -> mFlagFunction.get();
+                // When toggle override matches its default state, don't override flags. This
+                // helps users reset their feature overrides.
+                case OVERRIDE_OFF -> !shouldToggleBeEnabledByDefault && mFlagFunction.get();
+                case OVERRIDE_ON -> shouldToggleBeEnabledByDefault ? mFlagFunction.get() : true;
+            };
+        }
+    }
+
+    private ToggleOverride getToggleOverride(Context context) {
+        // If cached, return it
+        if (sCachedToggleOverride != null) {
+            return sCachedToggleOverride;
+        }
+
+        // Otherwise, fetch and cache it
+        ToggleOverride override = getToggleOverrideFromSystem(context);
+        sCachedToggleOverride = override;
+        Log.d(TAG, "Toggle override initialized to: " + override);
+        return override;
+    }
+
+    /**
+     *  Returns {@link ToggleOverride} from a non-persistent system property if present. Otherwise
+     *  initializes the system property by reading Settings.Global.
+     */
+    private ToggleOverride getToggleOverrideFromSystem(Context context) {
+        // A non-persistent System Property is used to store override to ensure it remains
+        // constant till reboot.
+        String overrideProperty = System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, null);
+        ToggleOverride overrideFromSystemProperties = convertToToggleOverride(overrideProperty);
+
+        // If valid system property, return it
+        if (overrideFromSystemProperties != null) {
+            return overrideFromSystemProperties;
+        }
+
+        // Fallback when System Property is not present (just after reboot) or not valid (user
+        // manually changed the value): Read from Settings.Global
+        int settingValue = Settings.Global.getInt(
+                context.getContentResolver(),
+                Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES,
+                OVERRIDE_UNSET.getSetting()
+        );
+        ToggleOverride overrideFromSettingsGlobal =
+                ToggleOverride.fromSetting(settingValue, OVERRIDE_UNSET);
+        // Initialize System Property
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(settingValue));
+        return overrideFromSettingsGlobal;
+    }
+
+    /**
+     * Converts {@code intString} into {@link ToggleOverride}. Return {@code null} if
+     * {@code intString} does not correspond to a {@link ToggleOverride}.
+     */
+    private static @Nullable ToggleOverride convertToToggleOverride(
+            @Nullable String intString
+    ) {
+        if (intString == null) return null;
+        try {
+            int intValue = Integer.parseInt(intString);
+            return ToggleOverride.fromSetting(intValue, null);
+        } catch (NumberFormatException e) {
+            Log.w(TAG, "Unknown toggleOverride int " + intString);
+            return null;
+        }
+    }
+
+    /** Override state of desktop mode developer option toggle. */
+    enum ToggleOverride {
+        OVERRIDE_UNSET,
+        OVERRIDE_OFF,
+        OVERRIDE_ON;
+
+        int getSetting() {
+            return switch (this) {
+                case OVERRIDE_ON -> 1;
+                case OVERRIDE_OFF -> 0;
+                case OVERRIDE_UNSET -> -1;
+            };
+        }
+
+        static ToggleOverride fromSetting(int setting, @Nullable ToggleOverride fallback) {
+            return switch (setting) {
+                case 1 -> OVERRIDE_ON;
+                case 0 -> OVERRIDE_OFF;
+                case -1 -> OVERRIDE_UNSET;
+                default -> fallback;
+            };
+        }
+    }
+}
diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp
index b2a5b02..f2b4136 100644
--- a/services/tests/powerstatstests/Android.bp
+++ b/services/tests/powerstatstests/Android.bp
@@ -59,7 +59,6 @@
     name: "PowerStatsTestsRavenwood",
     static_libs: [
         "services.core",
-        "platformprotosnano",
         "coretests-aidl",
         "ravenwood-junit",
         "truth",
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 41f1ac7..ea2abf7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -141,8 +141,8 @@
 import java.util.List;
 
 /** Common base class for window manager unit test classes. */
-class WindowTestsBase extends SystemServiceTestsBase {
-    final Context mContext = getInstrumentation().getTargetContext();
+public class WindowTestsBase extends SystemServiceTestsBase {
+    protected final Context mContext = getInstrumentation().getTargetContext();
 
     // Default package name
     static final String DEFAULT_COMPONENT_PACKAGE_NAME = "com.foo";
diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java
new file mode 100644
index 0000000..e5f2f89
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/utils/DesktopModeFlagsUtilTest.java
@@ -0,0 +1,459 @@
+/*
+ * 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.server.wm.utils;
+
+import static com.android.server.wm.utils.DesktopModeFlagsUtil.DESKTOP_WINDOWING_MODE;
+import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_OFF;
+import static com.android.server.wm.utils.DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_ON;
+import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE;
+import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY;
+import static com.android.window.flags.Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ContentResolver;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.wm.WindowTestRunner;
+import com.android.server.wm.WindowTestsBase;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Field;
+
+/**
+ * Test class for [DesktopModeFlagsUtil]
+ *
+ * Build/Install/Run:
+ * atest WmTests:DesktopModeFlagsUtilTest
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class DesktopModeFlagsUtilTest extends WindowTestsBase {
+
+    @Rule
+    public SetFlagsRule setFlagsRule = new SetFlagsRule();
+
+    @Before
+    public void setUp() throws Exception {
+        resetCache();
+    }
+
+    private static final String SYSTEM_PROPERTY_OVERRIDE_KEY =
+            "sys.wmshell.desktopmode.dev_toggle_override";
+
+    @Test
+    @DisableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_devOptionFlagDisabled_overrideOff_featureFlagOn_returnsTrue() {
+        setOverride(OVERRIDE_OFF.getSetting());
+        // In absence of dev options, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+
+    @Test
+    @DisableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_devOptionFlagDisabled_overrideOn_featureFlagOff_returnsFalse() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_overrideUnset_featureFlagOn_returnsTrue() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        // For overridableFlag, for unset overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_overrideUnset_featureFlagOff_returnsFalse() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        // For overridableFlag, for unset overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_noOverride_featureFlagOn_returnsTrue() {
+        setOverride(null);
+
+        // For overridableFlag, in absence of overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_noOverride_featureFlagOff_returnsFalse() {
+        setOverride(null);
+
+        // For overridableFlag, in absence of overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_unrecognizableOverride_featureFlagOn_returnsTrue() {
+        setOverride(-2);
+
+        // For overridableFlag, for unrecognized overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_unrecognizableOverride_featureFlagOff_returnsFalse() {
+        setOverride(-2);
+
+        // For overridableFlag, for unrecognizable overrides, follow flag
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_overrideOff_featureFlagOn_returnsFalse() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // For overridableFlag, follow override if they exist
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_overrideOn_featureFlagOff_returnsTrue() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // For overridableFlag, follow override if they exist
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_overrideOffThenOn_featureFlagOn_returnsFalseAndFalse() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // For overridableFlag, follow override if they exist
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // Keep overrides constant through the process
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_overrideOnThenOff_featureFlagOff_returnsTrueAndTrue() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // For overridableFlag, follow override if they exist
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // Keep overrides constant through the process
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_noProperty_overrideOn_featureFlagOff_returnsTrueAndPropertyOn() {
+        System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY);
+        setOverride(OVERRIDE_ON.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        // Store System Property if not present
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(OVERRIDE_ON.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_noProperty_overrideUnset_featureFlagOn_returnsTrueAndPropertyUnset() {
+        System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY);
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        // Store System Property if not present
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(
+                        DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()));
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_noProperty_overrideUnset_featureFlagOff_returnsFalseAndPropertyUnset() {
+        System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY);
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        // Store System Property if not present
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(
+                        DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_propertyNotInt_overrideOff_featureFlagOn_returnsFalseAndPropertyOff() {
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "abc");
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        // Store System Property if currently invalid
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_propertyInvalid_overrideOff_featureFlagOn_returnsFalseAndPropertyOff() {
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, "-2");
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        // Store System Property if currently invalid
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_propertyOff_overrideOn_featureFlagOn_returnsFalseAndnoPropertyUpdate() {
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(
+                OVERRIDE_OFF.getSetting()));
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // Have a consistent override until reboot
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isFalse();
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(OVERRIDE_OFF.getSetting()));
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_propertyOn_overrideOff_featureFlagOff_returnsTrueAndnoPropertyUpdate() {
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY, String.valueOf(OVERRIDE_ON.getSetting()));
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // Have a consistent override until reboot
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(OVERRIDE_ON.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    public void isEnabled_propertyUnset_overrideOff_featureFlagOn_returnsTrueAndnoPropertyUpdate() {
+        System.setProperty(SYSTEM_PROPERTY_OVERRIDE_KEY,
+                String.valueOf(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()));
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // Have a consistent override until reboot
+        assertThat(DESKTOP_WINDOWING_MODE.isEnabled(mContext)).isTrue();
+        assertThat(System.getProperty(SYSTEM_PROPERTY_OVERRIDE_KEY))
+                .isEqualTo(String.valueOf(
+                        DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting()));
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY})
+    public void isEnabled_dwFlagOn_overrideUnset_featureFlagOn_returnsTrue() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        // For unset overrides, follow flag
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    public void isEnabled_dwFlagOn_overrideUnset_featureFlagOff_returnsFalse() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+        // For unset overrides, follow flag
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({
+            FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
+            FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    public void isEnabled_dwFlagOn_overrideOn_featureFlagOn_returnsTrue() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // When toggle override matches its default state (dw flag), don't override flags
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    public void isEnabled_dwFlagOn_overrideOn_featureFlagOff_returnsFalse() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // When toggle override matches its default state (dw flag), don't override flags
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({
+            FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
+            FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    public void isEnabled_dwFlagOn_overrideOff_featureFlagOn_returnsFalse() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // Follow override if they exist, and is not equal to default toggle state (dw flag)
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION, FLAG_ENABLE_DESKTOP_WINDOWING_MODE})
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+    public void isEnabled_dwFlagOn_overrideOff_featureFlagOff_returnsFalse() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // Follow override if they exist, and is not equal to default toggle state (dw flag)
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({
+            FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_dwFlagOff_overrideUnset_featureFlagOn_returnsTrue() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        // For unset overrides, follow flag
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags({
+            FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    public void isEnabled_dwFlagOff_overrideUnset_featureFlagOff_returnsFalse() {
+        setOverride(DesktopModeFlagsUtil.ToggleOverride.OVERRIDE_UNSET.getSetting());
+
+        // For unset overrides, follow flag
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    @Test
+    @EnableFlags({
+            FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_dwFlagOff_overrideOn_featureFlagOn_returnsTrue() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // Follow override if they exist, and is not equal to default toggle state (dw flag)
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags({
+            FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    public void isEnabled_dwFlagOff_overrideOn_featureFlagOff_returnTrue() {
+        setOverride(OVERRIDE_ON.getSetting());
+
+        // Follow override if they exist, and is not equal to default toggle state (dw flag)
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags({
+            FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    public void isEnabled_dwFlagOff_overrideOff_featureFlagOn_returnsTrue() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // When toggle override matches its default state (dw flag), don't override flags
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
+    @DisableFlags({
+            FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
+    })
+    public void isEnabled_dwFlagOff_overrideOff_featureFlagOff_returnsFalse() {
+        setOverride(OVERRIDE_OFF.getSetting());
+
+        // When toggle override matches its default state (dw flag), don't override flags
+        assertThat(DesktopModeFlagsUtil.WALLPAPER_ACTIVITY.isEnabled(mContext)).isFalse();
+    }
+
+    private void setOverride(Integer setting) {
+        ContentResolver contentResolver = mContext.getContentResolver();
+        String key = Settings.Global.DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES;
+
+        if (setting == null) {
+            Settings.Global.putString(contentResolver, key, null);
+        } else {
+            Settings.Global.putInt(contentResolver, key, setting);
+        }
+    }
+
+    private void resetCache() throws Exception {
+        Field cachedToggleOverride = DesktopModeFlagsUtil.class.getDeclaredField(
+                "sCachedToggleOverride");
+        cachedToggleOverride.setAccessible(true);
+        cachedToggleOverride.set(null, null);
+
+        // Clear override cache stored in System property
+        System.clearProperty(SYSTEM_PROPERTY_OVERRIDE_KEY);
+    }
+}
diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp
index e839fc1..7739171 100644
--- a/tools/aapt2/cmd/Util.cpp
+++ b/tools/aapt2/cmd/Util.cpp
@@ -137,22 +137,25 @@
       diag->Error(android::DiagMessage() << "No name given for one or more flags in: " << arg);
       return false;
     }
+
     std::vector<std::string> name_parts = util::Split(flag_name, ':');
     if (name_parts.size() > 2) {
       diag->Error(android::DiagMessage()
                   << "Invalid feature flag and optional value '" << flag_and_value
-                  << "'. Must be in the format 'flag_name[:ro][=true|false]");
+                  << "'. Must be in the format 'flag_name[:READ_ONLY|READ_WRITE][=true|false]");
       return false;
     }
     flag_name = name_parts[0];
     bool read_only = false;
     if (name_parts.size() == 2) {
-      if (name_parts[1] == "ro") {
+      if (name_parts[1] == "ro" || name_parts[1] == "READ_ONLY") {
         read_only = true;
+      } else if (name_parts[1] == "READ_WRITE") {
+        read_only = false;
       } else {
         diag->Error(android::DiagMessage()
                     << "Invalid feature flag and optional value '" << flag_and_value
-                    << "'. Must be in the format 'flag_name[:ro][=true|false]");
+                    << "'. Must be in the format 'flag_name[:READ_ONLY|READ_WRITE][=true|false]");
         return false;
       }
     }
diff --git a/tools/aapt2/cmd/Util_test.cpp b/tools/aapt2/cmd/Util_test.cpp
index 35bc637..7818340 100644
--- a/tools/aapt2/cmd/Util_test.cpp
+++ b/tools/aapt2/cmd/Util_test.cpp
@@ -383,7 +383,7 @@
 TEST(UtilTest, ParseFeatureFlagsParameter_DuplicateFlag) {
   auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
   FeatureFlagValues feature_flag_values;
-  ASSERT_TRUE(ParseFeatureFlagsParameter("foo=true,bar=true,foo:ro=false", diagnostics,
+  ASSERT_TRUE(ParseFeatureFlagsParameter("foo=true,bar:READ_WRITE=true,foo:ro=false", diagnostics,
                                          &feature_flag_values));
   EXPECT_THAT(
       feature_flag_values,
@@ -394,11 +394,11 @@
 TEST(UtilTest, ParseFeatureFlagsParameter_Valid) {
   auto diagnostics = test::ContextBuilder().Build()->GetDiagnostics();
   FeatureFlagValues feature_flag_values;
-  ASSERT_TRUE(ParseFeatureFlagsParameter("foo= true, bar:ro =FALSE,baz=, quux", diagnostics,
-                                         &feature_flag_values));
+  ASSERT_TRUE(ParseFeatureFlagsParameter("foo:READ_ONLY= true, bar:ro =FALSE,baz:READ_WRITE=, quux",
+                                         diagnostics, &feature_flag_values));
   EXPECT_THAT(
       feature_flag_values,
-      UnorderedElementsAre(Pair("foo", FeatureFlagProperties{false, std::optional<bool>(true)}),
+      UnorderedElementsAre(Pair("foo", FeatureFlagProperties{true, std::optional<bool>(true)}),
                            Pair("bar", FeatureFlagProperties{true, std::optional<bool>(false)}),
                            Pair("baz", FeatureFlagProperties{false, std::nullopt}),
                            Pair("quux", FeatureFlagProperties{false, std::nullopt})));