Merge "Ktfmt on MultiPointerDraggable MultiPointerDraggableTest" into main
diff --git a/Android.bp b/Android.bp
index f8907f3..258440f 100644
--- a/Android.bp
+++ b/Android.bp
@@ -98,6 +98,7 @@
         ":android.frameworks.location.altitude-V2-java-source",
         ":android.hardware.biometrics.common-V4-java-source",
         ":android.hardware.biometrics.fingerprint-V5-java-source",
+        ":android.hardware.biometrics.fingerprint.virtualhal-java-source",
         ":android.hardware.biometrics.face-V4-java-source",
         ":android.hardware.gnss-V2-java-source",
         ":android.hardware.graphics.common-V3-java-source",
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index ede4a89..fb425a9 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -18156,9 +18156,17 @@
     field public static final int DISPLAY_IME_POLICY_LOCAL = 0; // 0x0
   }
 
+  @FlaggedApi("android.companion.virtualdevice.flags.status_bar_and_insets") public static class WindowManager.InsetsParams {
+    ctor public WindowManager.InsetsParams(int);
+    method @Nullable public android.graphics.Insets getInsetsSize();
+    method public int getType();
+    method @NonNull public android.view.WindowManager.InsetsParams setInsetsSize(@Nullable android.graphics.Insets);
+  }
+
   public static class WindowManager.LayoutParams extends android.view.ViewGroup.LayoutParams implements android.os.Parcelable {
     method public final long getUserActivityTimeout();
     method public boolean isSystemApplicationOverlay();
+    method @FlaggedApi("android.companion.virtualdevice.flags.status_bar_and_insets") public void setInsetsParams(@NonNull java.util.List<android.view.WindowManager.InsetsParams>);
     method @RequiresPermission(android.Manifest.permission.SYSTEM_APPLICATION_OVERLAY) public void setSystemApplicationOverlay(boolean);
     method public final void setUserActivityTimeout(long);
     field @RequiresPermission(android.Manifest.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS) public static final int SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS = 524288; // 0x80000
diff --git a/core/java/android/app/appfunctions/AppFunctionStaticMetadataHelper.java b/core/java/android/app/appfunctions/AppFunctionStaticMetadataHelper.java
index 926cc9a..085e0a4 100644
--- a/core/java/android/app/appfunctions/AppFunctionStaticMetadataHelper.java
+++ b/core/java/android/app/appfunctions/AppFunctionStaticMetadataHelper.java
@@ -39,6 +39,8 @@
     public static final String STATIC_PROPERTY_ENABLED_BY_DEFAULT = "enabledByDefault";
 
     public static final String APP_FUNCTION_STATIC_NAMESPACE = "app_functions";
+    public static final String PROPERTY_FUNCTION_ID = "functionId";
+    public static final String PROPERTY_PACKAGE_NAME = "packageName";
 
     // These are constants that has to be kept the same with {@code
     // com.android.server.appsearch.appsindexer.appsearchtypes.AppSearchHelper}.
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 6f1327c..e9fa3e1 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -121,7 +121,6 @@
     description: "Make relevant PowerManager APIs display aware by default"
     bug: "365042486"
     is_fixed_read_only: true
-    is_exported: true
 }
 
 flag {
@@ -131,3 +130,10 @@
     bug: "350007866"
     is_exported: true
 }
+
+flag {
+  namespace: "virtual_devices"
+  name: "camera_timestamp_from_surface"
+  description: "Pass the surface timestamp to the capture result"
+  bug: "351341245"
+}
diff --git a/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java
index 43c0da9..48c5887 100644
--- a/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java
+++ b/core/java/android/hardware/fingerprint/FingerprintSensorConfigurations.java
@@ -23,12 +23,14 @@
 import android.content.Context;
 import android.hardware.biometrics.fingerprint.IFingerprint;
 import android.hardware.biometrics.fingerprint.SensorProps;
+import android.hardware.biometrics.fingerprint.virtualhal.IVirtualHal;
 import android.os.Binder;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.util.Log;
+import android.util.Slog;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -162,6 +164,43 @@
         dest.writeMap(mSensorPropsMap);
     }
 
+
+    /**
+     * Remap fqName of VHAL because the `virtual` instance is registered
+     * with IVirtulalHal now (IFingerprint previously)
+     * @param fqName fqName to be translated
+     * @return real fqName
+     */
+    public static String remapFqName(String fqName) {
+        if (!fqName.contains(IFingerprint.DESCRIPTOR + "/virtual")) {
+            return fqName;  //no remap needed for real hardware HAL
+        } else {
+            //new Vhal instance name
+            return fqName.replace("IFingerprint", "virtualhal.IVirtualHal");
+        }
+    }
+
+    /**
+     * @param fqName aidl interface instance name
+     * @return aidl interface
+     */
+    public static IFingerprint getIFingerprint(String fqName) {
+        if (fqName.contains("virtual")) {
+            String fqNameMapped = remapFqName(fqName);
+            Slog.i(TAG, "getIFingerprint fqName is mapped: " + fqName + "->" + fqNameMapped);
+            try {
+                IVirtualHal vhal = IVirtualHal.Stub.asInterface(
+                        Binder.allowBlocking(ServiceManager.waitForService(fqNameMapped)));
+                return vhal.getFingerprintHal();
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Remote exception in vhal.getFingerprintHal() call" + fqNameMapped);
+            }
+        }
+
+        return IFingerprint.Stub.asInterface(
+                Binder.allowBlocking(ServiceManager.waitForDeclaredService(fqName)));
+    }
+
     /**
      * Returns fingerprint sensor props for the HAL {@param instance}.
      */
@@ -176,8 +215,7 @@
 
         try {
             final String fqName = IFingerprint.DESCRIPTOR + "/" + instance;
-            final IFingerprint fp = IFingerprint.Stub.asInterface(Binder.allowBlocking(
-                    ServiceManager.waitForDeclaredService(fqName)));
+            final IFingerprint fp = getIFingerprint(fqName);
             if (fp != null) {
                 props = fp.getSensorProps();
             } else {
diff --git a/core/java/android/os/SystemClock.java b/core/java/android/os/SystemClock.java
index 0ed1ab6..4c9a02c 100644
--- a/core/java/android/os/SystemClock.java
+++ b/core/java/android/os/SystemClock.java
@@ -109,6 +109,7 @@
     private static final String TAG = "SystemClock";
 
     private static volatile IAlarmManager sIAlarmManager;
+    private static volatile ITimeDetectorService sITimeDetectorService;
 
     /**
      * Since {@code nanoTime()} is arbitrary, anchor our Ravenwood clocks against it.
@@ -188,6 +189,14 @@
         return sIAlarmManager;
     }
 
+    private static ITimeDetectorService getITimeDetectorService() {
+        if (sITimeDetectorService == null) {
+            sITimeDetectorService = ITimeDetectorService.Stub
+                    .asInterface(ServiceManager.getService(Context.TIME_DETECTOR_SERVICE));
+        }
+        return sITimeDetectorService;
+    }
+
     /**
      * Returns milliseconds since boot, not counting time spent in deep sleep.
      *
@@ -314,15 +323,6 @@
     }
 
     /**
-     * @see #currentNetworkTimeMillis(ITimeDetectorService)
-     * @hide
-     */
-    public static long currentNetworkTimeMillis() {
-        return currentNetworkTimeMillis(ITimeDetectorService.Stub
-                .asInterface(ServiceManager.getService(Context.TIME_DETECTOR_SERVICE)));
-    }
-
-    /**
      * Returns milliseconds since January 1, 1970 00:00:00.0 UTC, synchronized
      * using a remote network source outside the device.
      * <p>
@@ -346,29 +346,29 @@
      * @throws DateTimeException when no network time can be provided.
      * @hide
      */
-    public static long currentNetworkTimeMillis(
-            ITimeDetectorService timeDetectorService) {
-        if (timeDetectorService != null) {
-            UnixEpochTime time;
-            try {
-                time = timeDetectorService.latestNetworkTime();
-            } catch (ParcelableException e) {
-                e.maybeRethrow(DateTimeException.class);
-                throw new RuntimeException(e);
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            }
-
-            if (time == null) {
-                // This is not expected.
-                throw new DateTimeException("Network based time is not available.");
-            }
-            long currentMillis = elapsedRealtime();
-            long deltaMs = currentMillis - time.getElapsedRealtimeMillis();
-            return time.getUnixEpochTimeMillis() + deltaMs;
-        } else {
+    public static long currentNetworkTimeMillis() {
+        ITimeDetectorService timeDetectorService = getITimeDetectorService();
+        if (timeDetectorService == null) {
             throw new RuntimeException(new DeadSystemException());
         }
+
+        UnixEpochTime time;
+        try {
+            time = timeDetectorService.latestNetworkTime();
+        } catch (ParcelableException e) {
+            e.maybeRethrow(DateTimeException.class);
+            throw new RuntimeException(e);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        if (time == null) {
+            // This is not expected.
+            throw new DateTimeException("Network based time is not available.");
+        }
+
+        long currentMillis = elapsedRealtime();
+        long deltaMs = currentMillis - time.getElapsedRealtimeMillis();
+        return time.getUnixEpochTimeMillis() + deltaMs;
     }
 
    /**
@@ -396,14 +396,9 @@
      */
     public static @NonNull Clock currentNetworkTimeClock() {
         return new SimpleClock(ZoneOffset.UTC) {
-            private ITimeDetectorService mSvc;
             @Override
             public long millis() {
-                if (mSvc == null) {
-                    mSvc = ITimeDetectorService.Stub
-                            .asInterface(ServiceManager.getService(Context.TIME_DETECTOR_SERVICE));
-                }
-                return SystemClock.currentNetworkTimeMillis(mSvc);
+                return SystemClock.currentNetworkTimeMillis();
             }
         };
     }
diff --git a/core/java/android/text/ClientFlags.java b/core/java/android/text/ClientFlags.java
index 901bf56..c2ad508 100644
--- a/core/java/android/text/ClientFlags.java
+++ b/core/java/android/text/ClientFlags.java
@@ -28,13 +28,6 @@
  */
 public class ClientFlags {
     /**
-     * @see Flags#noBreakNoHyphenationSpan()
-     */
-    public static boolean noBreakNoHyphenationSpan() {
-        return TextFlags.isFeatureEnabled(Flags.FLAG_NO_BREAK_NO_HYPHENATION_SPAN);
-    }
-
-    /**
      * @see Flags#fixMisalignedContextMenu()
      */
     public static boolean fixMisalignedContextMenu() {
diff --git a/core/java/android/text/TextFlags.java b/core/java/android/text/TextFlags.java
index a78aa7e..076721f 100644
--- a/core/java/android/text/TextFlags.java
+++ b/core/java/android/text/TextFlags.java
@@ -55,7 +55,6 @@
      * List of text flags to be transferred to the application process.
      */
     public static final String[] TEXT_ACONFIGS_FLAGS = {
-            Flags.FLAG_NO_BREAK_NO_HYPHENATION_SPAN,
             Flags.FLAG_FIX_MISALIGNED_CONTEXT_MENU,
     };
 
@@ -65,7 +64,6 @@
      * The order must be the same to the TEXT_ACONFIG_FLAGS.
      */
     public static final boolean[] TEXT_ACONFIG_DEFAULT_VALUE = {
-            Flags.noBreakNoHyphenationSpan(),
             Flags.fixMisalignedContextMenu(),
     };
 
diff --git a/core/java/android/text/flags/24Q3.aconfig b/core/java/android/text/flags/24Q3.aconfig
index 6524ed3..7035fc8 100644
--- a/core/java/android/text/flags/24Q3.aconfig
+++ b/core/java/android/text/flags/24Q3.aconfig
@@ -34,3 +34,21 @@
   description: "Feature flag that preserve the line height of the TextView and EditText even if the the locale is different from Latin"
   bug: "303326708"
 }
+
+flag {
+  name: "new_fonts_fallback_xml"
+  is_exported: true
+  namespace: "text"
+  description: "Feature flag for deprecating fonts.xml. By setting true for this feature flag, the new font configuration XML, /system/etc/font_fallback.xml is used. The new XML has a new syntax and flexibility of variable font declarations, but it is not compatible with the apps that reads fonts.xml. So, fonts.xml is maintained as a subset of the font_fallback.xml"
+  # Make read only, as it could be used before the Settings provider is initialized.
+  is_fixed_read_only: true
+  bug: "281769620"
+}
+
+flag {
+  name: "no_break_no_hyphenation_span"
+  is_exported: true
+  namespace: "text"
+  description: "A feature flag that adding new spans that prevents line breaking and hyphenation."
+  bug: "283193586"
+}
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index f8a98da..3c61f4f 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -2,24 +2,6 @@
 container: "system"
 
 flag {
-  name: "new_fonts_fallback_xml"
-  is_exported: true
-  namespace: "text"
-  description: "Feature flag for deprecating fonts.xml. By setting true for this feature flag, the new font configuration XML, /system/etc/font_fallback.xml is used. The new XML has a new syntax and flexibility of variable font declarations, but it is not compatible with the apps that reads fonts.xml. So, fonts.xml is maintained as a subset of the font_fallback.xml"
-  # Make read only, as it could be used before the Settings provider is initialized.
-  is_fixed_read_only: true
-  bug: "281769620"
-}
-
-flag {
-  name: "no_break_no_hyphenation_span"
-  is_exported: true
-  namespace: "text"
-  description: "A feature flag that adding new spans that prevents line breaking and hyphenation."
-  bug: "283193586"
-}
-
-flag {
   name: "use_optimized_boottime_font_loading"
   namespace: "text"
   description: "Feature flag ensuring that font is loaded once and asynchronously."
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 67a207e..4fe11e6 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -107,6 +107,7 @@
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
+import android.graphics.Insets;
 import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -2371,7 +2372,7 @@
         public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;
 
         /**
-         * Window type: the drag-and-drop pseudowindow.  There is only one
+         * Window type: the drag-and-drop pseudowindow. There is only one
          * drag layer (at most), and it is placed on top of all other windows.
          * In multiuser systems shows only on the owning user's window.
          * @hide
@@ -2381,7 +2382,7 @@
         /**
          * Window type: panel that slides out from over the status bar
          * In multiuser systems shows on all users' windows. These windows
-         * are displayed on top of the stauts bar and any {@link #TYPE_STATUS_BAR_PANEL}
+         * are displayed on top of the status bar and any {@link #TYPE_STATUS_BAR_PANEL}
          * windows.
          * @hide
          */
@@ -4649,6 +4650,30 @@
         public InsetsFrameProvider[] providedInsets;
 
         /**
+         * Sets the insets to be provided by the window.
+         *
+         * @param insetsParams The parameters for the insets to be provided by the window.
+         *
+         * @hide
+         */
+        @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_STATUS_BAR_AND_INSETS)
+        @SystemApi
+        public void setInsetsParams(@NonNull List<InsetsParams> insetsParams) {
+            if (insetsParams.isEmpty()) {
+                providedInsets = null;
+            } else {
+                providedInsets = new InsetsFrameProvider[insetsParams.size()];
+                for (int i = 0; i < insetsParams.size(); ++i) {
+                    final InsetsParams params = insetsParams.get(i);
+                    providedInsets[i] =
+                            new InsetsFrameProvider(/* owner= */ this, /* index= */ i,
+                                    params.getType())
+                                    .setInsetsSize(params.getInsetsSize());
+                }
+            }
+        }
+
+        /**
          * Specifies which {@link InsetsType}s should be forcibly shown. The types shown by this
          * method won't affect the app's layout. This field only takes effects if the caller has
          * {@link android.Manifest.permission#STATUS_BAR_SERVICE} or the caller has the same uid as
@@ -6117,6 +6142,55 @@
     }
 
     /**
+     * Specifies the parameters of the insets provided by a window.
+     *
+     * @see WindowManager.LayoutParams#setInsetsParams(List)
+     * @see android.graphics.Insets
+     *
+     * @hide
+     */
+    @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_STATUS_BAR_AND_INSETS)
+    @SystemApi
+    public static class InsetsParams {
+
+        private final @InsetsType int mType;
+        private @Nullable Insets mInsets;
+
+        /**
+         * Creates an instance of InsetsParams.
+         *
+         * @param type the type of insets to provide, e.g. {@link WindowInsets.Type#statusBars()}.
+         * @see WindowInsets.Type
+         */
+        public InsetsParams(@InsetsType int type) {
+            mType = type;
+        }
+
+        /**
+         * Sets the size of the provided insets. If {@code null}, then the provided insets will
+         * have the same size as the window frame.
+         */
+        public @NonNull InsetsParams setInsetsSize(@Nullable Insets insets) {
+            mInsets = insets;
+            return this;
+        }
+
+        /**
+         * Returns the type of provided insets.
+         */
+        public @InsetsType int getType() {
+            return mType;
+        }
+
+        /**
+         * Returns the size of the provided insets.
+         */
+        public @Nullable Insets getInsetsSize() {
+            return mInsets;
+        }
+    }
+
+    /**
      * Holds the WM lock for the specified amount of milliseconds.
      * Intended for use by the tests that need to imitate lock contention.
      * The token should be obtained by
diff --git a/core/java/android/webkit/UserPackage.java b/core/java/android/webkit/UserPackage.java
index 1da2af4..b18dbbc 100644
--- a/core/java/android/webkit/UserPackage.java
+++ b/core/java/android/webkit/UserPackage.java
@@ -15,12 +15,14 @@
  */
 package android.webkit;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.UserInfo;
+import android.content.pm.PackageManager;
 import android.os.Build;
+import android.os.UserHandle;
 import android.os.UserManager;
 
 import java.util.ArrayList;
@@ -31,30 +33,31 @@
  * @hide
  */
 public class UserPackage {
-    private final UserInfo mUserInfo;
+    private final UserHandle mUser;
     private final PackageInfo mPackageInfo;
 
     public static final int MINIMUM_SUPPORTED_SDK = Build.VERSION_CODES.TIRAMISU;
 
-    public UserPackage(UserInfo user, PackageInfo packageInfo) {
-        this.mUserInfo = user;
-        this.mPackageInfo = packageInfo;
+    public UserPackage(@NonNull UserHandle user, @Nullable PackageInfo packageInfo) {
+        mUser = user;
+        mPackageInfo = packageInfo;
     }
 
     /**
      * Returns a list of (User,PackageInfo) pairs corresponding to the PackageInfos for all
      * device users for the package named {@param packageName}.
      */
-    public static List<UserPackage> getPackageInfosAllUsers(Context context,
-            String packageName, int packageFlags) {
-        List<UserInfo> users = getAllUsers(context);
+    public static @NonNull List<UserPackage> getPackageInfosAllUsers(@NonNull Context context,
+            @NonNull String packageName, int packageFlags) {
+        UserManager userManager = context.getSystemService(UserManager.class);
+        List<UserHandle> users = userManager.getUserHandles(false);
         List<UserPackage> userPackages = new ArrayList<UserPackage>(users.size());
-        for (UserInfo user : users) {
+        for (UserHandle user : users) {
+            PackageManager pm = context.createContextAsUser(user, 0).getPackageManager();
             PackageInfo packageInfo = null;
             try {
-                packageInfo = context.getPackageManager().getPackageInfoAsUser(
-                        packageName, packageFlags, user.id);
-            } catch (NameNotFoundException e) {
+                packageInfo = pm.getPackageInfo(packageName, packageFlags);
+            } catch (PackageManager.NameNotFoundException e) {
             }
             userPackages.add(new UserPackage(user, packageInfo));
         }
@@ -88,18 +91,11 @@
         return packageInfo.applicationInfo.targetSdkVersion >= MINIMUM_SUPPORTED_SDK;
     }
 
-    public UserInfo getUserInfo() {
-        return mUserInfo;
+    public UserHandle getUser() {
+        return mUser;
     }
 
     public PackageInfo getPackageInfo() {
         return mPackageInfo;
     }
-
-
-    private static List<UserInfo> getAllUsers(Context context) {
-        UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
-        return userManager.getUsers();
-    }
-
 }
diff --git a/core/java/com/android/internal/jank/flags.aconfig b/core/java/com/android/internal/jank/flags.aconfig
index b6b8bc5..82f50ae 100644
--- a/core/java/com/android/internal/jank/flags.aconfig
+++ b/core/java/com/android/internal/jank/flags.aconfig
@@ -3,7 +3,7 @@
 
 flag {
   name: "use_sf_frame_duration"
-  namespace: "android_platform_window_surfaces"
+  namespace: "window_surfaces"
   description: "Whether to get the frame duration from SurfaceFlinger, or HWUI"
   bug: "354763298"
   is_fixed_read_only: true
diff --git a/graphics/java/android/graphics/OWNERS b/graphics/java/android/graphics/OWNERS
index 9fa8f1b..ef8d26c 100644
--- a/graphics/java/android/graphics/OWNERS
+++ b/graphics/java/android/graphics/OWNERS
@@ -11,3 +11,4 @@
 per-file FontFamily.java = file:fonts/OWNERS
 per-file FontListParser.java = file:fonts/OWNERS
 per-file Typeface.java = file:fonts/OWNERS
+per-file Paint.java = file:fonts/OWNERS
diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java
index f727f5b..0e25c34 100644
--- a/graphics/java/android/graphics/fonts/SystemFonts.java
+++ b/graphics/java/android/graphics/fonts/SystemFonts.java
@@ -306,13 +306,7 @@
             long lastModifiedDate,
             int configVersion
     ) {
-        final String fontsXml;
-        if (com.android.text.flags.Flags.newFontsFallbackXml()) {
-            fontsXml = FONTS_XML;
-        } else {
-            fontsXml = LEGACY_FONTS_XML;
-        }
-        return getSystemFontConfigInternal(fontsXml, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR,
+        return getSystemFontConfigInternal(FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR,
                 updatableFontMap, lastModifiedDate, configVersion);
     }
 
@@ -337,13 +331,7 @@
      * @hide
      */
     public static @NonNull FontConfig getSystemPreinstalledFontConfig() {
-        final String fontsXml;
-        if (com.android.text.flags.Flags.newFontsFallbackXml()) {
-            fontsXml = FONTS_XML;
-        } else {
-            fontsXml = LEGACY_FONTS_XML;
-        }
-        return getSystemFontConfigInternal(fontsXml, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, null,
+        return getSystemFontConfigInternal(FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, null,
                 0, 0);
     }
 
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
index 424d4bf..b5d63bd 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlags.kt
@@ -49,6 +49,7 @@
     SIZE_CONSTRAINTS(Flags::enableDesktopWindowingSizeConstraints, true),
     DISABLE_SNAP_RESIZE(Flags::disableNonResizableAppSnapResizing, true),
     DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, false),
+    SCALED_RESIZING(Flags::enableWindowingScaledResizing, false),
     ENABLE_DESKTOP_WINDOWING_TASK_LIMIT(Flags::enableDesktopWindowingTaskLimit, true),
     BACK_NAVIGATION(Flags::enableDesktopWindowingBackNavigation, true),
     EDGE_DRAG_RESIZE(Flags::enableWindowingEdgeDragResize, true),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
index 1e6fa28..2033902 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
@@ -24,6 +24,7 @@
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__CHILD_TASK_ENTER_PIP;
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED;
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_REQUEST;
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT;
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DESKTOP_MODE;
 import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RECREATE_SPLIT;
@@ -45,6 +46,7 @@
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DESKTOP_MODE;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_REQUEST;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RECREATE_SPLIT;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME;
@@ -211,6 +213,8 @@
                 return SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT;
             case EXIT_REASON_DESKTOP_MODE:
                 return SPLITSCREEN_UICHANGED__EXIT_REASON__DESKTOP_MODE;
+            case EXIT_REASON_FULLSCREEN_REQUEST:
+                return SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_REQUEST;
             case EXIT_REASON_UNKNOWN:
                 // Fall through
             default:
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 dad0d4e..1b143eb 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
@@ -1175,6 +1175,7 @@
         mSplitTransitions.startDismissTransition(wct, this, mLastActiveStage, reason);
         setSplitsVisible(false);
         mBreakOnNextWake = false;
+        logExit(reason);
     }
 
     void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
@@ -1265,6 +1266,7 @@
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         prepareExitSplitScreen(stage, wct);
         mSplitTransitions.startDismissTransition(wct, this, stage, exitReason);
+        logExit(exitReason);
     }
 
     /**
@@ -1361,6 +1363,7 @@
             mMainStage.doForAllChildTasks(taskId -> recentTasks.removeSplitPair(taskId));
             mSideStage.doForAllChildTasks(taskId -> recentTasks.removeSplitPair(taskId));
         });
+        logExit(exitReason);
     }
 
     /**
@@ -1579,7 +1582,7 @@
         if (stage == STAGE_TYPE_MAIN) {
             mLogger.logMainStageAppChange(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
                     mSplitLayout.isLeftRightSplit());
-        } else {
+        } else if (stage == STAGE_TYPE_SIDE) {
             mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(),
                     mSplitLayout.isLeftRightSplit());
         }
@@ -2275,6 +2278,7 @@
         if (isOpening && inFullscreen) {
             // One task is opening into fullscreen mode, remove the corresponding split record.
             mRecentTasks.ifPresent(recentTasks -> recentTasks.removeSplitPair(triggerTask.taskId));
+            logExit(EXIT_REASON_FULLSCREEN_REQUEST);
         }
 
         if (isSplitActive()) {
@@ -2402,6 +2406,7 @@
             if (triggerTask != null) {
                 mRecentTasks.ifPresent(
                         recentTasks -> recentTasks.removeSplitPair(triggerTask.taskId));
+                logExit(EXIT_REASON_CHILD_TASK_ENTER_PIP);
             }
             @StageType int topStage = STAGE_TYPE_UNDEFINED;
             if (isSplitScreenVisible()) {
@@ -2743,7 +2748,10 @@
                 final int dismissTop = mainChild != null ? STAGE_TYPE_MAIN :
                         (sideChild != null ? STAGE_TYPE_SIDE : STAGE_TYPE_UNDEFINED);
                 pendingEnter.cancel(
-                        (cancelWct, cancelT) -> prepareExitSplitScreen(dismissTop, cancelWct));
+                        (cancelWct, cancelT) -> {
+                            prepareExitSplitScreen(dismissTop, cancelWct);
+                            logExit(EXIT_REASON_UNKNOWN);
+                        });
                 Log.w(TAG, splitFailureMessage("startPendingEnterAnimation",
                         "launched 2 tasks in split, but didn't receive "
                         + "2 tasks in transition. Possibly one of them failed to launch"));
diff --git a/libs/androidfw/AssetManager.cpp b/libs/androidfw/AssetManager.cpp
index 5955915..e618245 100644
--- a/libs/androidfw/AssetManager.cpp
+++ b/libs/androidfw/AssetManager.cpp
@@ -1420,20 +1420,18 @@
 Mutex AssetManager::SharedZip::gLock;
 DefaultKeyedVector<String8, wp<AssetManager::SharedZip> > AssetManager::SharedZip::gOpen;
 
-AssetManager::SharedZip::SharedZip(const String8& path, ModDate modWhen)
-    : mPath(path),
-      mZipFile(NULL),
-      mModWhen(modWhen),
-      mResourceTableAsset(NULL),
-      mResourceTable(NULL) {
-  if (kIsDebug) {
-    ALOGI("Creating SharedZip %p %s\n", this, mPath.c_str());
-  }
-  ALOGV("+++ opening zip '%s'\n", mPath.c_str());
-  mZipFile = ZipFileRO::open(mPath.c_str());
-  if (mZipFile == NULL) {
-    ALOGD("failed to open Zip archive '%s'\n", mPath.c_str());
-  }
+AssetManager::SharedZip::SharedZip(const String8& path, time_t modWhen)
+    : mPath(path), mZipFile(NULL), mModWhen(modWhen),
+      mResourceTableAsset(NULL), mResourceTable(NULL)
+{
+    if (kIsDebug) {
+        ALOGI("Creating SharedZip %p %s\n", this, mPath.c_str());
+    }
+    ALOGV("+++ opening zip '%s'\n", mPath.c_str());
+    mZipFile = ZipFileRO::open(mPath.c_str());
+    if (mZipFile == NULL) {
+        ALOGD("failed to open Zip archive '%s'\n", mPath.c_str());
+    }
 }
 
 AssetManager::SharedZip::SharedZip(int fd, const String8& path)
@@ -1455,7 +1453,7 @@
         bool createIfNotPresent)
 {
     AutoMutex _l(gLock);
-    auto modWhen = getFileModDate(path.c_str());
+    time_t modWhen = getFileModDate(path.c_str());
     sp<SharedZip> zip = gOpen.valueFor(path).promote();
     if (zip != NULL && zip->mModWhen == modWhen) {
         return zip;
@@ -1522,8 +1520,8 @@
 
 bool AssetManager::SharedZip::isUpToDate()
 {
-  auto modWhen = getFileModDate(mPath.c_str());
-  return mModWhen == modWhen;
+    time_t modWhen = getFileModDate(mPath.c_str());
+    return mModWhen == modWhen;
 }
 
 void AssetManager::SharedZip::addOverlay(const asset_path& ap)
diff --git a/libs/androidfw/include/androidfw/AssetManager.h b/libs/androidfw/include/androidfw/AssetManager.h
index 376c881..ce0985b 100644
--- a/libs/androidfw/include/androidfw/AssetManager.h
+++ b/libs/androidfw/include/androidfw/AssetManager.h
@@ -280,21 +280,21 @@
         ~SharedZip();
 
     private:
-     SharedZip(const String8& path, ModDate modWhen);
-     SharedZip(int fd, const String8& path);
-     SharedZip();  // <-- not implemented
+        SharedZip(const String8& path, time_t modWhen);
+        SharedZip(int fd, const String8& path);
+        SharedZip(); // <-- not implemented
 
-     String8 mPath;
-     ZipFileRO* mZipFile;
-     ModDate mModWhen;
+        String8 mPath;
+        ZipFileRO* mZipFile;
+        time_t mModWhen;
 
-     Asset* mResourceTableAsset;
-     ResTable* mResourceTable;
+        Asset* mResourceTableAsset;
+        ResTable* mResourceTable;
 
-     Vector<asset_path> mOverlays;
+        Vector<asset_path> mOverlays;
 
-     static Mutex gLock;
-     static DefaultKeyedVector<String8, wp<SharedZip> > gOpen;
+        static Mutex gLock;
+        static DefaultKeyedVector<String8, wp<SharedZip> > gOpen;
     };
 
     /*
diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h
index 98f1aa8..64b1f0c 100644
--- a/libs/androidfw/include/androidfw/Idmap.h
+++ b/libs/androidfw/include/androidfw/Idmap.h
@@ -25,9 +25,8 @@
 #include "android-base/macros.h"
 #include "android-base/unique_fd.h"
 #include "androidfw/ConfigDescription.h"
-#include "androidfw/ResourceTypes.h"
 #include "androidfw/StringPiece.h"
-#include "androidfw/misc.h"
+#include "androidfw/ResourceTypes.h"
 #include "utils/ByteOrder.h"
 
 namespace android {
@@ -203,7 +202,7 @@
   android::base::unique_fd idmap_fd_;
   std::string_view overlay_apk_path_;
   std::string_view target_apk_path_;
-  ModDate idmap_last_mod_time_;
+  time_t idmap_last_mod_time_;
 
  private:
   DISALLOW_COPY_AND_ASSIGN(LoadedIdmap);
diff --git a/libs/androidfw/include/androidfw/misc.h b/libs/androidfw/include/androidfw/misc.h
index 09ae40c..077609d 100644
--- a/libs/androidfw/include/androidfw/misc.h
+++ b/libs/androidfw/include/androidfw/misc.h
@@ -13,13 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-#pragma once
 
-#include <time.h>
+#include <sys/types.h>
 
 //
 // Handy utility functions and portability code.
 //
+#ifndef _LIBS_ANDROID_FW_MISC_H
+#define _LIBS_ANDROID_FW_MISC_H
 
 namespace android {
 
@@ -40,35 +41,15 @@
 } FileType;
 /* get the file's type; follows symlinks */
 FileType getFileType(const char* fileName);
-
-// MinGW doesn't support nanosecond resolution in stat() modification time, and given
-// that it only matters on the device it's ok to keep it at the second level there.
-#ifdef _WIN32
-using ModDate = time_t;
-inline constexpr ModDate kInvalidModDate = ModDate(-1);
-inline constexpr unsigned long long kModDateResolutionNs = 1ull * 1000 * 1000 * 1000;
-inline time_t toTimeT(ModDate m) {
-  return m;
-}
-#else
-using ModDate = timespec;
-inline constexpr ModDate kInvalidModDate = {-1, -1};
-inline constexpr unsigned long long kModDateResolutionNs = 1;
-inline time_t toTimeT(ModDate m) {
-  return m.tv_sec;
-}
-#endif
-
-/* get the file's modification date; returns kInvalidModDate w/errno set on failure */
-ModDate getFileModDate(const char* fileName);
+/* get the file's modification date; returns -1 w/errno set on failure */
+time_t getFileModDate(const char* fileName);
 /* same, but also returns -1 if the file has already been deleted */
-ModDate getFileModDate(int fd);
+time_t getFileModDate(int fd);
 
 // Check if |path| or |fd| resides on a readonly filesystem.
 bool isReadonlyFilesystem(const char* path);
 bool isReadonlyFilesystem(int fd);
 
-}  // namespace android
+}; // namespace android
 
-// Whoever uses getFileModDate() will need this as well
-bool operator==(const timespec& l, const timespec& r);
+#endif // _LIBS_ANDROID_FW_MISC_H
diff --git a/libs/androidfw/misc.cpp b/libs/androidfw/misc.cpp
index 9bdaf18a..93dcaf5 100644
--- a/libs/androidfw/misc.cpp
+++ b/libs/androidfw/misc.cpp
@@ -28,13 +28,11 @@
 #include <sys/vfs.h>
 #endif  // __linux__
 
+#include <cstring>
+#include <cstdio>
 #include <errno.h>
 #include <sys/stat.h>
 
-#include <cstdio>
-#include <cstring>
-#include <tuple>
-
 namespace android {
 
 /*
@@ -75,32 +73,27 @@
     }
 }
 
-static ModDate getModDate(const struct stat& st) {
-#ifdef _WIN32
-  return st.st_mtime;
-#else
-  return st.st_mtim;
-#endif
+/*
+ * Get a file's modification date.
+ */
+time_t getFileModDate(const char* fileName) {
+    struct stat sb;
+    if (stat(fileName, &sb) < 0) {
+        return (time_t)-1;
+    }
+    return sb.st_mtime;
 }
 
-ModDate getFileModDate(const char* fileName) {
-  struct stat sb;
-  if (stat(fileName, &sb) < 0) {
-    return kInvalidModDate;
-  }
-  return getModDate(sb);
-}
-
-ModDate getFileModDate(int fd) {
-  struct stat sb;
-  if (fstat(fd, &sb) < 0) {
-    return kInvalidModDate;
-  }
-  if (sb.st_nlink <= 0) {
-    errno = ENOENT;
-    return kInvalidModDate;
-  }
-  return getModDate(sb);
+time_t getFileModDate(int fd) {
+    struct stat sb;
+    if (fstat(fd, &sb) < 0) {
+        return (time_t)-1;
+    }
+    if (sb.st_nlink <= 0) {
+        errno = ENOENT;
+        return (time_t)-1;
+    }
+    return sb.st_mtime;
 }
 
 #ifndef __linux__
@@ -131,8 +124,4 @@
 }
 #endif  // __linux__
 
-}  // namespace android
-
-bool operator==(const timespec& l, const timespec& r) {
-  return std::tie(l.tv_sec, l.tv_nsec) == std::tie(r.tv_sec, l.tv_nsec);
-}
+}; // namespace android
diff --git a/libs/androidfw/tests/Idmap_test.cpp b/libs/androidfw/tests/Idmap_test.cpp
index cb2e56f..60aa7d8 100644
--- a/libs/androidfw/tests/Idmap_test.cpp
+++ b/libs/androidfw/tests/Idmap_test.cpp
@@ -14,9 +14,6 @@
  * limitations under the License.
  */
 
-#include <chrono>
-#include <thread>
-
 #include "android-base/file.h"
 #include "androidfw/ApkAssets.h"
 #include "androidfw/AssetManager2.h"
@@ -30,7 +27,6 @@
 #include "data/overlayable/R.h"
 #include "data/system/R.h"
 
-using namespace std::chrono_literals;
 using ::testing::NotNull;
 
 namespace overlay = com::android::overlay;
@@ -222,13 +218,10 @@
 
   unlink(temp_file.path);
   ASSERT_FALSE(apk_assets->IsUpToDate());
-
-  const auto sleep_duration =
-      std::chrono::nanoseconds(std::max(kModDateResolutionNs, 1'000'000ull));
-  std::this_thread::sleep_for(sleep_duration);
+  sleep(2);
 
   base::WriteStringToFile("hello", temp_file.path);
-  std::this_thread::sleep_for(sleep_duration);
+  sleep(2);
 
   ASSERT_FALSE(apk_assets->IsUpToDate());
 }
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index 3409c29..defbc11 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -16,8 +16,6 @@
 
 package com.android.externalstorage;
 
-import static java.util.regex.Pattern.CASE_INSENSITIVE;
-
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.usage.StorageStatsManager;
@@ -61,12 +59,15 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 import java.util.Objects;
 import java.util.UUID;
-import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 /**
  * Presents content of the shared (a.k.a. "external") storage.
@@ -89,12 +90,9 @@
     private static final Uri BASE_URI =
             new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build();
 
-    /**
-     * Regex for detecting {@code /Android/data/}, {@code /Android/obb/} and
-     * {@code /Android/sandbox/} along with all their subdirectories and content.
-     */
-    private static final Pattern PATTERN_RESTRICTED_ANDROID_SUBTREES =
-            Pattern.compile("^Android/(?:data|obb|sandbox)(?:/.+)?", CASE_INSENSITIVE);
+    private static final String PRIMARY_EMULATED_STORAGE_PATH = "/storage/emulated/";
+
+    private static final String STORAGE_PATH = "/storage/";
 
     private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
             Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
@@ -309,11 +307,70 @@
             return false;
         }
 
-        final String path = getPathFromDocId(documentId);
-        return PATTERN_RESTRICTED_ANDROID_SUBTREES.matcher(path).matches();
+        try {
+            final RootInfo root = getRootFromDocId(documentId);
+            final String canonicalPath = getPathFromDocId(documentId);
+            return isRestrictedPath(root.rootId, canonicalPath);
+        } catch (Exception e) {
+            return true;
+        }
     }
 
     /**
+     * Based on the given root id and path, we restrict path access if file is Android/data or
+     * Android/obb or Android/sandbox or one of their subdirectories.
+     *
+     * @param canonicalPath of the file
+     * @return true if path is restricted
+     */
+    private boolean isRestrictedPath(String rootId, String canonicalPath) {
+        if (rootId == null || canonicalPath == null) {
+            return true;
+        }
+
+        final String rootPath;
+        if (rootId.equalsIgnoreCase(ROOT_ID_PRIMARY_EMULATED)) {
+            // Creates "/storage/emulated/<user-id>"
+            rootPath = PRIMARY_EMULATED_STORAGE_PATH + UserHandle.myUserId();
+        } else {
+            // Creates "/storage/<volume-uuid>"
+            rootPath = STORAGE_PATH + rootId;
+        }
+        List<java.nio.file.Path> restrictedPathList = Arrays.asList(
+                Paths.get(rootPath, "Android", "data"),
+                Paths.get(rootPath, "Android", "obb"),
+                Paths.get(rootPath, "Android", "sandbox"));
+        // We need to identify restricted parent paths which actually exist on the device
+        List<java.nio.file.Path> validRestrictedPathsToCheck = restrictedPathList.stream().filter(
+                Files::exists).collect(Collectors.toList());
+
+        boolean isRestricted = false;
+        java.nio.file.Path filePathToCheck = Paths.get(rootPath, canonicalPath);
+        try {
+            while (filePathToCheck != null) {
+                for (java.nio.file.Path restrictedPath : validRestrictedPathsToCheck) {
+                    if (Files.isSameFile(restrictedPath, filePathToCheck)) {
+                        isRestricted = true;
+                        Log.v(TAG, "Restricting access for path: " + filePathToCheck);
+                        break;
+                    }
+                }
+                if (isRestricted) {
+                    break;
+                }
+
+                filePathToCheck = filePathToCheck.getParent();
+            }
+        } catch (Exception e) {
+            Log.w(TAG, "Error in checking file equality check.", e);
+            isRestricted = true;
+        }
+
+        return isRestricted;
+    }
+
+
+    /**
      * Check that the directory is the root of storage or blocked file from tree.
      * <p>
      * Note, that this is different from hidden documents: blocked documents <b>WILL</b> appear
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
index 3e2d832..d3c345d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
@@ -98,7 +98,7 @@
      */
     suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean): Boolean
 
-    suspend fun setRingerMode(audioStream: AudioStream, mode: RingerMode)
+    suspend fun setRingerModeInternal(audioStream: AudioStream, mode: RingerMode)
 
     /** Gets audio device category. */
     @AudioDeviceCategory suspend fun getBluetoothAudioDeviceCategory(bluetoothAddress: String): Int
@@ -248,8 +248,8 @@
         }
     }
 
-    override suspend fun setRingerMode(audioStream: AudioStream, mode: RingerMode) {
-        withContext(backgroundCoroutineContext) { audioManager.ringerMode = mode.value }
+    override suspend fun setRingerModeInternal(audioStream: AudioStream, mode: RingerMode) {
+        withContext(backgroundCoroutineContext) { audioManager.ringerModeInternal = mode.value }
     }
 
     @AudioDeviceCategory
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt
index 08863b5..dca890d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt
@@ -68,7 +68,7 @@
         if (audioStream.value == AudioManager.STREAM_RING) {
             val mode =
                 if (isMuted) AudioManager.RINGER_MODE_VIBRATE else AudioManager.RINGER_MODE_NORMAL
-            audioRepository.setRingerMode(audioStream, RingerMode(mode))
+            audioRepository.setRingerModeInternal(audioStream, RingerMode(mode))
         }
         val mutedChanged = audioRepository.setMuted(audioStream, isMuted)
         if (mutedChanged && !isMuted) {
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
index 52e6391..8a3b1dfb 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
@@ -74,6 +74,8 @@
 
     private lateinit var underTest: AudioRepository
 
+    private var ringerModeInternal: RingerMode = RingerMode(AudioManager.RINGER_MODE_NORMAL)
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
@@ -82,7 +84,7 @@
         `when`(audioManager.communicationDevice).thenReturn(communicationDevice)
         `when`(audioManager.getStreamMinVolume(anyInt())).thenReturn(MIN_VOLUME)
         `when`(audioManager.getStreamMaxVolume(anyInt())).thenReturn(MAX_VOLUME)
-        `when`(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL)
+        `when`(audioManager.ringerModeInternal).then { ringerModeInternal.value }
         `when`(audioManager.setStreamVolume(anyInt(), anyInt(), anyInt())).then {
             val streamType = it.arguments[0] as Int
             volumeByStream[it.arguments[0] as Int] = it.arguments[1] as Int
@@ -103,6 +105,10 @@
         `when`(audioManager.isStreamMute(anyInt())).thenAnswer {
             isMuteByStream.getOrDefault(it.arguments[0] as Int, false)
         }
+        `when`(audioManager.setRingerModeInternal(anyInt())).then {
+            ringerModeInternal = RingerMode(it.arguments[0] as Int)
+            Unit
+        }
 
         underTest =
             AudioRepositoryImpl(
@@ -137,7 +143,7 @@
             underTest.ringerMode.onEach { modes.add(it) }.launchIn(backgroundScope)
             runCurrent()
 
-            `when`(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_SILENT)
+            ringerModeInternal = RingerMode(AudioManager.RINGER_MODE_SILENT)
             triggerEvent(AudioManagerEvent.InternalRingerModeChanged)
             runCurrent()
 
@@ -150,6 +156,19 @@
     }
 
     @Test
+    fun changingRingerMode_changesRingerModeInternal() {
+        testScope.runTest {
+            underTest.setRingerModeInternal(
+                AudioStream(AudioManager.STREAM_SYSTEM),
+                RingerMode(AudioManager.RINGER_MODE_SILENT),
+            )
+            runCurrent()
+
+            assertThat(ringerModeInternal).isEqualTo(RingerMode(AudioManager.RINGER_MODE_SILENT))
+        }
+    }
+
+    @Test
     fun communicationDeviceChanges_repositoryEmits() {
         testScope.runTest {
             var device: AudioDeviceInfo? = null
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
index ba885f7..1f98cd8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt
@@ -65,7 +65,6 @@
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Dialog
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.PlatformOutlinedButton
 import com.android.compose.animation.Easings
@@ -355,7 +354,10 @@
     fun Content(modifier: Modifier) {
 
         // Wrap PIN entry in a Box so it is visible to accessibility (even if empty).
-        Box(modifier = modifier.fillMaxWidth().wrapContentHeight()) {
+        Box(
+            modifier = modifier.fillMaxWidth().wrapContentHeight(),
+            contentAlignment = Alignment.Center,
+        ) {
             Row(
                 modifier
                     .heightIn(min = shapeAnimations.shapeSize)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/TextExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/TextExt.kt
index e1f73e3..4e8121f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/TextExt.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/TextExt.kt
@@ -17,9 +17,12 @@
 
 package com.android.systemui.common.ui.compose
 
+import android.content.Context
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
 import com.android.systemui.common.shared.model.Text
+import com.android.systemui.common.shared.model.Text.Companion.loadText
 
 /** Returns the loaded [String] or `null` if there isn't one. */
 @Composable
@@ -29,3 +32,7 @@
         is Text.Resource -> stringResource(res)
     }
 }
+
+fun Text.toAnnotatedString(context: Context): AnnotatedString? {
+    return loadText(context)?.let { AnnotatedString(it) }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt
index cd0c58f..c4fc132 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadStatsInteractorTest.kt
@@ -19,12 +19,15 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.contextualeducation.GestureType.ALL_APPS
 import com.android.systemui.contextualeducation.GestureType.BACK
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.education.data.repository.contextualEducationRepository
 import com.android.systemui.education.data.repository.fakeEduClock
+import com.android.systemui.keyboard.data.repository.keyboardRepository
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.testKosmos
+import com.android.systemui.touchpad.data.repository.touchpadRepository
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
@@ -36,22 +39,62 @@
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val underTest = kosmos.keyboardTouchpadEduStatsInteractor
+    private val keyboardRepository = kosmos.keyboardRepository
+    private val touchpadRepository = kosmos.touchpadRepository
+    private val repository = kosmos.contextualEducationRepository
 
     @Test
-    fun dataUpdatedOnIncrementSignalCount() =
+    fun dataUpdatedOnIncrementSignalCountWhenTouchpadConnected() =
         testScope.runTest {
-            val model by
-                collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK))
+            touchpadRepository.setIsAnyTouchpadConnected(true)
+
+            val model by collectLastValue(repository.readGestureEduModelFlow(BACK))
             val originalValue = model!!.signalCount
             underTest.incrementSignalCount(BACK)
+
             assertThat(model?.signalCount).isEqualTo(originalValue + 1)
         }
 
     @Test
+    fun dataUnchangedOnIncrementSignalCountWhenTouchpadDisconnected() =
+        testScope.runTest {
+            touchpadRepository.setIsAnyTouchpadConnected(false)
+
+            val model by collectLastValue(repository.readGestureEduModelFlow(BACK))
+            val originalValue = model!!.signalCount
+            underTest.incrementSignalCount(BACK)
+
+            assertThat(model?.signalCount).isEqualTo(originalValue)
+        }
+
+    @Test
+    fun dataUpdatedOnIncrementSignalCountWhenKeyboardConnected() =
+        testScope.runTest {
+            keyboardRepository.setIsAnyKeyboardConnected(true)
+
+            val model by collectLastValue(repository.readGestureEduModelFlow(ALL_APPS))
+            val originalValue = model!!.signalCount
+            underTest.incrementSignalCount(ALL_APPS)
+
+            assertThat(model?.signalCount).isEqualTo(originalValue + 1)
+        }
+
+    @Test
+    fun dataUnchangedOnIncrementSignalCountWhenKeyboardDisconnected() =
+        testScope.runTest {
+            keyboardRepository.setIsAnyKeyboardConnected(false)
+
+            val model by collectLastValue(repository.readGestureEduModelFlow(ALL_APPS))
+            val originalValue = model!!.signalCount
+            underTest.incrementSignalCount(ALL_APPS)
+
+            assertThat(model?.signalCount).isEqualTo(originalValue)
+        }
+
+    @Test
     fun dataAddedOnUpdateShortcutTriggerTime() =
         testScope.runTest {
-            val model by
-                collectLastValue(kosmos.contextualEducationRepository.readGestureEduModelFlow(BACK))
+            val model by collectLastValue(repository.readGestureEduModelFlow(BACK))
             assertThat(model?.lastShortcutTriggeredTime).isNull()
             underTest.updateShortcutTriggerTime(BACK)
             assertThat(model?.lastShortcutTriggeredTime).isEqualTo(kosmos.fakeEduClock.instant())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt
index 1e5599b..93ba265 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt
@@ -19,7 +19,6 @@
 import android.content.ComponentName
 import android.content.packageManager
 import android.content.pm.PackageManager
-import android.content.pm.ServiceInfo
 import android.content.pm.UserInfo
 import android.graphics.drawable.TestStubDrawable
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -35,6 +34,7 @@
 import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository
 import com.android.systemui.qs.pipeline.data.repository.installedTilesRepository
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.settings.fakeUserTracker
 import com.android.systemui.testKosmos
@@ -100,6 +100,7 @@
                         Icon.Loaded(drawable1, ContentDescription.Loaded(tileService1)),
                         Text.Loaded(tileService1),
                         Text.Loaded(appName1),
+                        TileCategory.PROVIDED_BY_APP,
                     )
                 val expectedData2 =
                     EditTileData(
@@ -107,6 +108,7 @@
                         Icon.Loaded(drawable2, ContentDescription.Loaded(tileService2)),
                         Text.Loaded(tileService2),
                         Text.Loaded(appName2),
+                        TileCategory.PROVIDED_BY_APP,
                     )
 
                 assertThat(editTileDataList).containsExactly(expectedData1, expectedData2)
@@ -144,6 +146,7 @@
                         Icon.Loaded(drawable1, ContentDescription.Loaded(tileService1)),
                         Text.Loaded(tileService1),
                         Text.Loaded(appName1),
+                        TileCategory.PROVIDED_BY_APP,
                     )
 
                 val editTileDataList = underTest.getCustomTileData()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.kt
index deefbf5..053a59a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository
 import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 import com.android.systemui.qs.tiles.impl.battery.qsBatterySaverTileConfig
 import com.android.systemui.qs.tiles.impl.flashlight.qsFlashlightTileConfig
 import com.android.systemui.qs.tiles.impl.internet.qsInternetTileConfig
@@ -132,6 +133,7 @@
                         icon = Icon.Loaded(icon, ContentDescription.Loaded(tileName)),
                         label = Text.Loaded(tileName),
                         appName = Text.Loaded(appName),
+                        category = TileCategory.PROVIDED_BY_APP,
                     )
 
                 assertThat(editTiles.customTiles).hasSize(1)
@@ -181,7 +183,8 @@
                 tileSpec = this,
                 icon = Icon.Resource(android.R.drawable.star_on, ContentDescription.Loaded(spec)),
                 label = Text.Loaded(spec),
-                appName = null
+                appName = null,
+                category = TileCategory.UNKNOWN,
             )
         }
 
@@ -192,6 +195,7 @@
                     Icon.Resource(uiConfig.iconRes, ContentDescription.Resource(uiConfig.labelRes)),
                 label = Text.Resource(uiConfig.labelRes),
                 appName = null,
+                category = category,
             )
         }
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
index 7f01fad..484a8ff 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/EditTileListStateTest.kt
@@ -16,11 +16,11 @@
 
 package com.android.systemui.qs.panels.ui.compose
 
+import androidx.compose.ui.text.AnnotatedString
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.common.shared.model.Text
 import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.shared.model.SizedTileImpl
 import com.android.systemui.qs.panels.ui.model.GridCell
@@ -28,6 +28,7 @@
 import com.android.systemui.qs.panels.ui.model.TileGridCell
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -141,10 +142,11 @@
                 EditTileViewModel(
                     tileSpec = TileSpec.create(tileSpec),
                     icon = Icon.Resource(0, null),
-                    label = Text.Loaded("unused"),
+                    label = AnnotatedString("unused"),
                     appName = null,
                     isCurrent = true,
                     availableEditActions = emptySet(),
+                    category = TileCategory.UNKNOWN,
                 ),
                 width,
             )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt
index 601779f..583db72 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
+import com.android.systemui.common.ui.compose.toAnnotatedString
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
@@ -42,6 +43,7 @@
 import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.qsTileFactory
+import com.android.systemui.qs.shared.model.TileCategory
 import com.android.systemui.qs.tiles.impl.alarm.qsAlarmTileConfig
 import com.android.systemui.qs.tiles.impl.battery.qsBatterySaverTileConfig
 import com.android.systemui.qs.tiles.impl.flashlight.qsFlashlightTileConfig
@@ -190,7 +192,7 @@
                     .forEach {
                         val data = getEditTileData(it.tileSpec)
 
-                        assertThat(it.label).isEqualTo(data.label)
+                        assertThat(it.label).isEqualTo(data.label.toAnnotatedString(context))
                         assertThat(it.icon).isEqualTo(data.icon)
                         assertThat(it.appName).isNull()
                     }
@@ -224,15 +226,19 @@
 
                 // service1
                 val model1 = tiles!!.first { it.tileSpec == TileSpec.create(component1) }
-                assertThat(model1.label).isEqualTo(Text.Loaded(tileService1))
-                assertThat(model1.appName).isEqualTo(Text.Loaded(appName1))
+                assertThat(model1.label)
+                    .isEqualTo(Text.Loaded(tileService1).toAnnotatedString(context))
+                assertThat(model1.appName)
+                    .isEqualTo(Text.Loaded(appName1).toAnnotatedString(context))
                 assertThat(model1.icon)
                     .isEqualTo(Icon.Loaded(drawable1, ContentDescription.Loaded(tileService1)))
 
                 // service2
                 val model2 = tiles!!.first { it.tileSpec == TileSpec.create(component2) }
-                assertThat(model2.label).isEqualTo(Text.Loaded(tileService2))
-                assertThat(model2.appName).isEqualTo(Text.Loaded(appName2))
+                assertThat(model2.label)
+                    .isEqualTo(Text.Loaded(tileService2).toAnnotatedString(context))
+                assertThat(model2.appName)
+                    .isEqualTo(Text.Loaded(appName2).toAnnotatedString(context))
                 assertThat(model2.icon)
                     .isEqualTo(Icon.Loaded(drawable2, ContentDescription.Loaded(tileService2)))
             }
@@ -559,7 +565,8 @@
                 tileSpec = this,
                 icon = Icon.Resource(R.drawable.star_on, ContentDescription.Loaded(spec)),
                 label = Text.Loaded(spec),
-                appName = null
+                appName = null,
+                category = TileCategory.UNKNOWN,
             )
         }
 
@@ -570,6 +577,7 @@
                     Icon.Resource(uiConfig.iconRes, ContentDescription.Resource(uiConfig.labelRes)),
                 label = Text.Resource(uiConfig.labelRes),
                 appName = null,
+                category = category,
             )
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/shared/model/GroupAndSortCategoryAndNameTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/shared/model/GroupAndSortCategoryAndNameTest.kt
new file mode 100644
index 0000000..7f90e3b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/shared/model/GroupAndSortCategoryAndNameTest.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.qs.shared.model
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class GroupAndSortCategoryAndNameTest : SysuiTestCase() {
+
+    private val elements =
+        listOf(
+            CategoryAndName(TileCategory.DISPLAY, "B"),
+            CategoryAndName(TileCategory.PRIVACY, "A"),
+            CategoryAndName(TileCategory.DISPLAY, "C"),
+            CategoryAndName(TileCategory.UTILITIES, "B"),
+            CategoryAndName(TileCategory.CONNECTIVITY, "A"),
+            CategoryAndName(TileCategory.PROVIDED_BY_APP, "B"),
+            CategoryAndName(TileCategory.CONNECTIVITY, "C"),
+            CategoryAndName(TileCategory.ACCESSIBILITY, "A")
+        )
+
+    @Test
+    fun allElementsInResult() {
+        val grouped = groupAndSort(elements)
+        val allValues = grouped.values.reduce { acc, el -> acc + el }
+        assertThat(allValues).containsExactlyElementsIn(elements)
+    }
+
+    @Test
+    fun groupedByCategory() {
+        val grouped = groupAndSort(elements)
+        grouped.forEach { tileCategory, categoryAndNames ->
+            categoryAndNames.forEach { element ->
+                assertThat(element.category).isEqualTo(tileCategory)
+            }
+        }
+    }
+
+    @Test
+    fun sortedAlphabeticallyInEachCategory() {
+        val grouped = groupAndSort(elements)
+        grouped.values.forEach { elements ->
+            assertThat(elements.map(CategoryAndName::name)).isInOrder()
+        }
+    }
+
+    @Test
+    fun categoriesSortedInNaturalOrder() {
+        val grouped = groupAndSort(elements)
+        assertThat(grouped.keys).isInOrder()
+    }
+
+    @Test
+    fun missingCategoriesAreNotInResult() {
+        val grouped = groupAndSort(elements.filterNot { it.category == TileCategory.CONNECTIVITY })
+        assertThat(grouped.keys).doesNotContain(TileCategory.CONNECTIVITY)
+    }
+
+    companion object {
+        private fun CategoryAndName(category: TileCategory, name: String): CategoryAndName {
+            return object : CategoryAndName {
+                override val category = category
+                override val name = name
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
index aaad0fc..5a45060 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
@@ -184,7 +184,7 @@
                 )
 
             val networkModel =
-                WifiNetworkModel.Active(
+                WifiNetworkModel.Active.of(
                     level = 4,
                     ssid = "test ssid",
                 )
@@ -219,7 +219,7 @@
                 )
 
             val networkModel =
-                WifiNetworkModel.Active(
+                WifiNetworkModel.Active.of(
                     level = 4,
                     ssid = "test ssid",
                     hotspotDeviceType = WifiNetworkModel.HotspotDeviceType.NONE,
@@ -398,7 +398,7 @@
                 collectLastValue(
                     underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
                 )
-            val networkModel = WifiNetworkModel.Inactive
+            val networkModel = WifiNetworkModel.Inactive()
 
             connectivityRepository.setWifiConnected(validated = false)
             wifiRepository.setIsWifiDefault(true)
@@ -416,7 +416,7 @@
                     underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
                 )
 
-            val networkModel = WifiNetworkModel.Inactive
+            val networkModel = WifiNetworkModel.Inactive()
 
             connectivityRepository.setWifiConnected(validated = false)
             wifiRepository.setIsWifiDefault(true)
@@ -543,7 +543,7 @@
 
     private fun setWifiNetworkWithHotspot(hotspot: WifiNetworkModel.HotspotDeviceType) {
         val networkModel =
-            WifiNetworkModel.Active(
+            WifiNetworkModel.Active.of(
                 level = 4,
                 ssid = "test ssid",
                 hotspotDeviceType = hotspot,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapperTest.kt
index 2444229..fa6d8bf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapperTest.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.kosmos.testCase
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.qs.qsEventLogger
+import com.android.systemui.qs.shared.model.TileCategory
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileState
 import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
@@ -43,7 +44,8 @@
         QSTileConfig(
             TileSpec.create(RecordIssueModule.TILE_SPEC),
             uiConfig,
-            kosmos.qsEventLogger.getNewInstanceId()
+            kosmos.qsEventLogger.getNewInstanceId(),
+            TileCategory.UTILITIES,
         )
     private val resources = kosmos.mainResources
     private val theme = resources.newTheme()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
index b9ca8fc..c0a1592 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt
@@ -78,7 +78,7 @@
     @Test
     fun ssid_inactiveNetwork_outputsNull() =
         testScope.runTest {
-            wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive())
 
             var latest: String? = "default"
             val job = underTest.ssid.onEach { latest = it }.launchIn(this)
@@ -93,7 +93,7 @@
     fun ssid_carrierMergedNetwork_outputsNull() =
         testScope.runTest {
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.CarrierMerged(subscriptionId = 2, level = 1)
+                WifiNetworkModel.CarrierMerged.of(subscriptionId = 2, level = 1)
             )
 
             var latest: String? = "default"
@@ -109,7 +109,7 @@
     fun ssid_unknownSsid_outputsNull() =
         testScope.runTest {
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.Active(
+                WifiNetworkModel.Active.of(
                     level = 1,
                     ssid = WifiManager.UNKNOWN_SSID,
                 )
@@ -128,7 +128,7 @@
     fun ssid_validSsid_outputsSsid() =
         testScope.runTest {
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.Active(
+                WifiNetworkModel.Active.of(
                     level = 1,
                     ssid = "MyAwesomeWifiNetwork",
                 )
@@ -189,7 +189,7 @@
     fun wifiNetwork_matchesRepoWifiNetwork() =
         testScope.runTest {
             val wifiNetwork =
-                WifiNetworkModel.Active(
+                WifiNetworkModel.Active.of(
                     isValidated = true,
                     level = 3,
                     ssid = "AB",
@@ -263,7 +263,7 @@
             val latest by collectLastValue(underTest.areNetworksAvailable)
 
             wifiRepository.wifiScanResults.value = emptyList()
-            wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive())
 
             assertThat(latest).isFalse()
         }
@@ -280,7 +280,7 @@
                     WifiScanEntry(ssid = "ssid 3"),
                 )
 
-            wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive())
 
             assertThat(latest).isTrue()
         }
@@ -298,7 +298,7 @@
                 )
 
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.Active(
+                WifiNetworkModel.Active.of(
                     ssid = "ssid 2",
                     level = 2,
                 )
@@ -318,7 +318,7 @@
                 )
 
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.Active(
+                WifiNetworkModel.Active.of(
                     ssid = "ssid 2",
                     level = 2,
                 )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index 72a45b9..141e304 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -115,7 +115,7 @@
             val latestKeyguard by collectLastValue(keyguard.wifiIcon)
             val latestQs by collectLastValue(qs.wifiIcon)
 
-            wifiRepository.setWifiNetwork(WifiNetworkModel.Active(isValidated = true, level = 1))
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Active.of(isValidated = true, level = 1))
 
             assertThat(latestHome).isInstanceOf(WifiIcon.Visible::class.java)
             assertThat(latestHome).isEqualTo(latestKeyguard)
@@ -129,7 +129,7 @@
 
             // Even WHEN the network has a valid hotspot type
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.Active(
+                WifiNetworkModel.Active.of(
                     isValidated = true,
                     level = 1,
                     hotspotDeviceType = WifiNetworkModel.HotspotDeviceType.LAPTOP,
@@ -191,7 +191,7 @@
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
 
-            wifiRepository.setWifiNetwork(WifiNetworkModel.Active(ssid = null, level = 1))
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Active.of(ssid = null, level = 1))
 
             val activityIn by collectLastValue(underTest.isActivityInViewVisible)
             val activityOut by collectLastValue(underTest.isActivityOutViewVisible)
@@ -214,7 +214,7 @@
             whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true)
             createAndSetViewModel()
 
-            wifiRepository.setWifiNetwork(WifiNetworkModel.Active(ssid = null, level = 1))
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Active.of(ssid = null, level = 1))
 
             val activityIn by collectLastValue(underTest.isActivityInViewVisible)
             val activityOut by collectLastValue(underTest.isActivityOutViewVisible)
@@ -463,6 +463,6 @@
     }
 
     companion object {
-        private val ACTIVE_VALID_WIFI_NETWORK = WifiNetworkModel.Active(ssid = "AB", level = 1)
+        private val ACTIVE_VALID_WIFI_NETWORK = WifiNetworkModel.Active.of(ssid = "AB", level = 1)
     }
 }
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 1fb1dad..589d9dd 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3812,4 +3812,33 @@
     <!-- Toast message for notifying users to use regular brightness bar to lower the brightness. [CHAR LIMIT=NONE] -->
     <string name="accessibility_deprecate_extra_dim_dialog_toast">
         Extra dim shortcut removed. To lower your brightness, use the regular brightness bar.</string>
+
+    <!-- Label for category in QS Edit mode list of tiles, for tiles that are related to connectivity, e.g. Internet. [CHAR LIMIT=NONE] -->
+    <string name="qs_edit_mode_category_connectivity">
+        Connectivity
+    </string>
+    <!-- Label for category in QS Edit mode list of tiles, for tiles that are related to accessibility functions, e.g. Hearing devices. [CHAR LIMIT=NONE] -->
+    <string name="qs_edit_mode_category_accessibility">
+        Accessibility
+    </string>
+    <!-- Label for category in QS Edit mode list of tiles, for tiles that are related to general utilities, e.g. Flashlight. [CHAR LIMIT=NONE] -->
+    <string name="qs_edit_mode_category_utilities">
+        Utilities
+    </string>
+    <!-- Label for category in QS Edit mode list of tiles, for tiles that are related to privacy, e.g. Mic access. [CHAR LIMIT=NONE] -->
+    <string name="qs_edit_mode_category_privacy">
+        Privacy
+    </string>
+    <!-- Label for category in QS Edit mode list of tiles, for tiles that are provided by an app, e.g. Calculator. [CHAR LIMIT=NONE] -->
+    <string name="qs_edit_mode_category_providedByApps">
+        Provided by apps
+    </string>
+    <!-- Label for category in QS Edit mode list of tiles, for tiles that are related to the display, e.g. Dark theme. [CHAR LIMIT=NONE] -->
+    <string name="qs_edit_mode_category_display">
+        Display
+    </string>
+    <!-- Label for category in QS Edit mode list of tiles, for tiles with an unknown category. [CHAR LIMIT=NONE] -->
+    <string name="qs_edit_mode_category_unknown">
+        Unknown
+    </string>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
index bb80396..cd9efaf 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.Flags
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.qs.tiles.ColorCorrectionTile
 import com.android.systemui.qs.tiles.ColorInversionTile
@@ -179,6 +180,7 @@
                         labelRes = R.string.quick_settings_color_correction_label,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.ACCESSIBILITY,
             )
 
         /** Inject ColorCorrectionTile into tileViewModelMap in QSModule */
@@ -210,6 +212,7 @@
                         labelRes = R.string.quick_settings_inversion_label,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.ACCESSIBILITY,
             )
 
         /** Inject ColorInversionTile into tileViewModelMap in QSModule */
@@ -241,6 +244,7 @@
                         labelRes = R.string.quick_settings_font_scaling_label,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.DISPLAY,
             )
 
         /** Inject FontScaling Tile into tileViewModelMap in QSModule */
@@ -272,6 +276,7 @@
                         labelRes = com.android.internal.R.string.reduce_bright_colors_feature_name,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.DISPLAY,
             )
 
         @Provides
@@ -286,6 +291,7 @@
                         labelRes = R.string.quick_settings_hearing_devices_label,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.ACCESSIBILITY,
             )
 
         /**
@@ -322,6 +328,7 @@
                         labelRes = R.string.quick_settings_onehanded_label,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.ACCESSIBILITY,
             )
 
         /** Inject One Handed Mode Tile into tileViewModelMap in QSModule. */
@@ -355,6 +362,7 @@
                         labelRes = R.string.quick_settings_night_display_label,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.DISPLAY,
             )
 
         /**
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatterySaverModule.kt b/packages/SystemUI/src/com/android/systemui/battery/BatterySaverModule.kt
index 8a9a322..831bc1d 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatterySaverModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatterySaverModule.kt
@@ -2,6 +2,7 @@
 
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.qs.tiles.BatterySaverTile
 import com.android.systemui.qs.tiles.base.interactor.QSTileAvailabilityInteractor
@@ -33,7 +34,7 @@
     @IntoMap
     @StringKey(BATTERY_SAVER_TILE_SPEC)
     fun provideBatterySaverAvailabilityInteractor(
-            impl: BatterySaverTileDataInteractor
+        impl: BatterySaverTileDataInteractor
     ): QSTileAvailabilityInteractor
 
     companion object {
@@ -51,6 +52,7 @@
                         labelRes = R.string.battery_detail_switch_title,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.UTILITIES,
             )
 
         /** Inject BatterySaverTile into tileViewModelMap in QSModule */
diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
index db7ffc1..037b6fa 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt
@@ -48,6 +48,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.qs.tiles.DeviceControlsTile
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
@@ -86,15 +87,16 @@
         @IntoMap
         @StringKey(DEVICE_CONTROLS_SPEC)
         fun provideDeviceControlsTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
-                QSTileConfig(
-                        tileSpec = TileSpec.create(DEVICE_CONTROLS_SPEC),
-                        uiConfig =
-                        QSTileUIConfig.Resource(
-                                iconRes = com.android.systemui.res.R.drawable.controls_icon,
-                                labelRes = com.android.systemui.res.R.string.quick_controls_title
-                        ),
-                        instanceId = uiEventLogger.getNewInstanceId(),
-                )
+            QSTileConfig(
+                tileSpec = TileSpec.create(DEVICE_CONTROLS_SPEC),
+                uiConfig =
+                    QSTileUIConfig.Resource(
+                        iconRes = com.android.systemui.res.R.drawable.controls_icon,
+                        labelRes = com.android.systemui.res.R.string.quick_controls_title
+                    ),
+                instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.UTILITIES,
+            )
     }
 
     @Binds
@@ -115,12 +117,12 @@
 
     @Binds
     abstract fun provideSettingsManager(
-            manager: ControlsSettingsRepositoryImpl
+        manager: ControlsSettingsRepositoryImpl
     ): ControlsSettingsRepository
 
     @Binds
     abstract fun provideDialogManager(
-            manager: ControlsSettingsDialogManagerImpl
+        manager: ControlsSettingsDialogManagerImpl
     ): ControlsSettingsDialogManager
 
     @Binds
@@ -141,8 +143,7 @@
         repository: SelectedComponentRepositoryImpl
     ): SelectedComponentRepository
 
-    @BindsOptionalOf
-    abstract fun optionalPersistenceWrapper(): ControlsFavoritePersistenceWrapper
+    @BindsOptionalOf abstract fun optionalPersistenceWrapper(): ControlsFavoritePersistenceWrapper
 
     @BindsOptionalOf
     abstract fun provideControlsTileResourceConfiguration(): ControlsTileResourceConfiguration
@@ -157,23 +158,17 @@
     @Binds
     @IntoMap
     @ClassKey(ControlsFavoritingActivity::class)
-    abstract fun provideControlsFavoritingActivity(
-        activity: ControlsFavoritingActivity
-    ): Activity
+    abstract fun provideControlsFavoritingActivity(activity: ControlsFavoritingActivity): Activity
 
     @Binds
     @IntoMap
     @ClassKey(ControlsEditingActivity::class)
-    abstract fun provideControlsEditingActivity(
-        activity: ControlsEditingActivity
-    ): Activity
+    abstract fun provideControlsEditingActivity(activity: ControlsEditingActivity): Activity
 
     @Binds
     @IntoMap
     @ClassKey(ControlsRequestDialog::class)
-    abstract fun provideControlsRequestDialog(
-        activity: ControlsRequestDialog
-    ): Activity
+    abstract fun provideControlsRequestDialog(activity: ControlsRequestDialog): Activity
 
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index f6ac7a5..a45ad15 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -38,6 +38,7 @@
 import com.android.systemui.dreams.homecontrols.HomeControlsDreamService;
 import com.android.systemui.qs.QsEventLogger;
 import com.android.systemui.qs.pipeline.shared.TileSpec;
+import com.android.systemui.qs.shared.model.TileCategory;
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig;
 import com.android.systemui.qs.tiles.viewmodel.QSTilePolicy;
 import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig;
@@ -196,6 +197,7 @@
                         R.drawable.ic_qs_screen_saver,
                         R.string.quick_settings_screensaver_label),
                 uiEventLogger.getNewInstanceId(),
+                TileCategory.UTILITIES,
                 tileSpec.getSpec(),
                 QSTilePolicy.NoRestrictions.INSTANCE
                 );
diff --git a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt
index 3223433..eb4eee2 100644
--- a/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduStatsInteractor.kt
@@ -16,11 +16,17 @@
 
 package com.android.systemui.education.domain.interactor
 
+import com.android.systemui.contextualeducation.GestureType
+import com.android.systemui.contextualeducation.GestureType.ALL_APPS
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.contextualeducation.GestureType
+import com.android.systemui.inputdevice.data.repository.UserInputDeviceRepository
+import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType
+import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.KEYBOARD
+import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.TOUCHPAD
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
 
 /**
@@ -39,12 +45,17 @@
 @Inject
 constructor(
     @Background private val backgroundScope: CoroutineScope,
-    private val contextualEducationInteractor: ContextualEducationInteractor
+    private val contextualEducationInteractor: ContextualEducationInteractor,
+    private val inputDeviceRepository: UserInputDeviceRepository,
 ) : KeyboardTouchpadEduStatsInteractor {
 
     override fun incrementSignalCount(gestureType: GestureType) {
-        // Todo: check if keyboard/touchpad is connected before update
-        backgroundScope.launch { contextualEducationInteractor.incrementSignalCount(gestureType) }
+        backgroundScope.launch {
+            val targetDevice = getTargetDevice(gestureType)
+            if (isTargetDeviceConnected(targetDevice)) {
+                contextualEducationInteractor.incrementSignalCount(gestureType)
+            }
+        }
     }
 
     override fun updateShortcutTriggerTime(gestureType: GestureType) {
@@ -52,4 +63,24 @@
             contextualEducationInteractor.updateShortcutTriggerTime(gestureType)
         }
     }
+
+    private suspend fun isTargetDeviceConnected(deviceType: DeviceType): Boolean {
+        if (deviceType == KEYBOARD) {
+            return inputDeviceRepository.isAnyKeyboardConnectedForUser.first().isConnected
+        } else if (deviceType == TOUCHPAD) {
+            return inputDeviceRepository.isAnyTouchpadConnectedForUser.first().isConnected
+        }
+        return false
+    }
+
+    /**
+     * Keyboard shortcut education would be provided for All Apps. Touchpad gesture education would
+     * be provided for the rest of the gesture types (i.e. Home, Overview, Back). This method maps
+     * gesture to its target education device.
+     */
+    private fun getTargetDevice(gestureType: GestureType) =
+        when (gestureType) {
+            ALL_APPS -> KEYBOARD
+            else -> TOUCHPAD
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartable.kt
index 7ecacdc..092a25a 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartable.kt
@@ -16,9 +16,17 @@
 
 package com.android.systemui.inputdevice.tutorial
 
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.UserHandle
 import com.android.systemui.CoreStartable
+import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.inputdevice.tutorial.ui.TutorialNotificationCoordinator
+import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity
 import com.android.systemui.shared.Flags.newTouchpadGesturesTutorial
 import dagger.Lazy
 import javax.inject.Inject
@@ -27,11 +35,35 @@
 @SysUISingleton
 class KeyboardTouchpadTutorialCoreStartable
 @Inject
-constructor(private val tutorialNotificationCoordinator: Lazy<TutorialNotificationCoordinator>) :
-    CoreStartable {
+constructor(
+    private val tutorialNotificationCoordinator: Lazy<TutorialNotificationCoordinator>,
+    private val broadcastDispatcher: BroadcastDispatcher,
+    @Application private val applicationContext: Context,
+) : CoreStartable {
     override fun start() {
         if (newTouchpadGesturesTutorial()) {
             tutorialNotificationCoordinator.get().start()
+            registerTutorialBroadcastReceiver()
         }
     }
+
+    private fun registerTutorialBroadcastReceiver() {
+        broadcastDispatcher.registerReceiver(
+            receiver =
+                object : BroadcastReceiver() {
+                    override fun onReceive(context: Context, intent: Intent) {
+                        applicationContext.startActivityAsUser(
+                            Intent(
+                                applicationContext,
+                                KeyboardTouchpadTutorialActivity::class.java
+                            ),
+                            UserHandle.SYSTEM
+                        )
+                    }
+                },
+            filter = IntentFilter("com.android.systemui.action.KEYBOARD_TOUCHPAD_TUTORIAL"),
+            flags = Context.RECEIVER_EXPORTED,
+            user = UserHandle.ALL,
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
index 63f3d52..dcca12f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -82,6 +82,7 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.focus.FocusDirection
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.geometry.CornerRadius
@@ -92,8 +93,12 @@
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onKeyEvent
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
@@ -824,9 +829,18 @@
     // from the ViewModel.
     var queryInternal by remember { mutableStateOf("") }
     val focusRequester = remember { FocusRequester() }
+    val focusManager = LocalFocusManager.current
     LaunchedEffect(Unit) { focusRequester.requestFocus() }
     SearchBar(
-        modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),
+        modifier =
+            Modifier.fillMaxWidth().focusRequester(focusRequester).onKeyEvent {
+                if (it.key == Key.DirectionDown) {
+                    focusManager.moveFocus(FocusDirection.Down)
+                    return@onKeyEvent true
+                } else {
+                    return@onKeyEvent false
+                }
+            },
         colors = SearchBarDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceBright),
         query = queryInternal,
         active = false,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 362e016c..df0f10a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -71,6 +71,7 @@
 import com.android.systemui.statusbar.KeyguardIndicationController
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
 import com.google.android.msdl.domain.MSDLPlayer
 import dagger.Lazy
@@ -112,6 +113,7 @@
     private val clockInteractor: KeyguardClockInteractor,
     private val keyguardViewMediator: KeyguardViewMediator,
     private val deviceEntryUnlockTrackerViewBinder: Optional<DeviceEntryUnlockTrackerViewBinder>,
+    private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
     @Main private val mainDispatcher: CoroutineDispatcher,
     private val msdlPlayer: MSDLPlayer,
 ) : CoreStartable {
@@ -220,6 +222,7 @@
                 vibratorHelper,
                 falsingManager,
                 keyguardViewMediator,
+                statusBarKeyguardViewManager,
                 mainDispatcher,
                 msdlPlayer,
             )
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt
index f2d39da..ecfabc3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceOnTouchListener.kt
@@ -18,13 +18,12 @@
 
 import android.annotation.SuppressLint
 import android.graphics.PointF
+import android.view.InputDevice
 import android.view.MotionEvent
 import android.view.View
 import android.view.ViewConfiguration
 import android.view.ViewPropertyAnimator
-import androidx.core.animation.CycleInterpolator
-import androidx.core.animation.ObjectAnimator
-import com.android.systemui.res.R
+import com.android.systemui.Flags
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.ui.view.rawDistanceFrom
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
@@ -71,11 +70,10 @@
                     // Moving too far while performing a long-press gesture cancels that
                     // gesture.
                     if (
-                        event
-                            .rawDistanceFrom(
-                                downDisplayCoords.x,
-                                downDisplayCoords.y,
-                            ) > ViewConfiguration.getTouchSlop()
+                        event.rawDistanceFrom(
+                            downDisplayCoords.x,
+                            downDisplayCoords.y,
+                        ) > ViewConfiguration.getTouchSlop()
                     ) {
                         cancel()
                     }
@@ -151,10 +149,14 @@
             event: MotionEvent,
             pointerIndex: Int = 0,
         ): Boolean {
-            return when (event.getToolType(pointerIndex)) {
-                MotionEvent.TOOL_TYPE_STYLUS -> true
-                MotionEvent.TOOL_TYPE_MOUSE -> true
-                else -> false
+            return if (Flags.nonTouchscreenDevicesBypassFalsing()) {
+                event.device?.supportsSource(InputDevice.SOURCE_TOUCHSCREEN) == false
+            } else {
+                when (event.getToolType(pointerIndex)) {
+                    MotionEvent.TOOL_TYPE_STYLUS -> true
+                    MotionEvent.TOOL_TYPE_MOUSE -> true
+                    else -> false
+                }
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 5bb7b64..ed82159 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -24,6 +24,8 @@
 import android.graphics.Rect
 import android.util.Log
 import android.view.HapticFeedbackConstants
+import android.view.InputDevice
+import android.view.MotionEvent
 import android.view.View
 import android.view.View.OnLayoutChangeListener
 import android.view.View.VISIBLE
@@ -41,6 +43,7 @@
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
 import com.android.keyguard.AuthInteractionProperties
+import com.android.systemui.Flags
 import com.android.systemui.Flags.msdlFeedback
 import com.android.systemui.Flags.newAodTransition
 import com.android.systemui.common.shared.model.Icon
@@ -73,6 +76,7 @@
 import com.android.systemui.statusbar.CrossFadeHelper
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
 import com.android.systemui.temporarydisplay.ViewPriority
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
 import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
@@ -115,6 +119,7 @@
         vibratorHelper: VibratorHelper?,
         falsingManager: FalsingManager?,
         keyguardViewMediator: KeyguardViewMediator?,
+        statusBarKeyguardViewManager: StatusBarKeyguardViewManager?,
         mainImmediateDispatcher: CoroutineDispatcher,
         msdlPlayer: MSDLPlayer?,
     ): DisposableHandle {
@@ -124,12 +129,30 @@
         if (KeyguardBottomAreaRefactor.isEnabled) {
             disposables +=
                 view.onTouchListener { _, event ->
+                    var consumed = false
                     if (falsingManager?.isFalseTap(FalsingManager.LOW_PENALTY) == false) {
+                        // signifies a primary button click down has reached keyguardrootview
+                        // we need to return true here otherwise an ACTION_UP will never arrive
+                        if (Flags.nonTouchscreenDevicesBypassFalsing()) {
+                            if (
+                                event.action == MotionEvent.ACTION_DOWN &&
+                                    event.buttonState == MotionEvent.BUTTON_PRIMARY &&
+                                    !event.isTouchscreenSource()
+                            ) {
+                                consumed = true
+                            } else if (
+                                event.action == MotionEvent.ACTION_UP &&
+                                    !event.isTouchscreenSource()
+                            ) {
+                                statusBarKeyguardViewManager?.showBouncer(true)
+                                consumed = true
+                            }
+                        }
                         viewModel.setRootViewLastTapPosition(
                             Point(event.x.toInt(), event.y.toInt())
                         )
                     }
-                    false
+                    consumed
                 }
         }
 
@@ -637,6 +660,10 @@
         }
     }
 
+    private fun MotionEvent.isTouchscreenSource(): Boolean {
+        return device?.supportsSource(InputDevice.SOURCE_TOUCHSCREEN) == true
+    }
+
     private fun ViewPropertyAnimator.animateInIconTranslation(): ViewPropertyAnimator =
         setInterpolator(Interpolators.DECELERATE_QUINT).translationY(0f)
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index f581a2e..0b8f741 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -417,6 +417,7 @@
                     null, // device entry haptics not required for preview mode
                     null, // falsing manager not required for preview mode
                     null, // keyguard view mediator is not required for preview mode
+                    null, // primary bouncer interactor is not required for preview mode
                     mainDispatcher,
                     null,
                 )
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
index 4ad437c..84aae65 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -70,6 +70,7 @@
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification
 import com.android.systemui.media.controls.domain.resume.MediaResumeListener
 import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
+import com.android.systemui.media.controls.shared.MediaLogger
 import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
 import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC
 import com.android.systemui.media.controls.shared.model.MediaAction
@@ -84,7 +85,7 @@
 import com.android.systemui.media.controls.util.MediaDataUtils
 import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.media.controls.util.MediaUiEventLogger
-import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.media.controls.util.SmallHash
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
@@ -186,7 +187,6 @@
     private val mediaDeviceManager: MediaDeviceManager,
     mediaDataCombineLatest: MediaDataCombineLatest,
     private val mediaDataFilter: LegacyMediaDataFilterImpl,
-    private val activityStarter: ActivityStarter,
     private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
     private var useMediaResumption: Boolean,
     private val useQsMediaPlayer: Boolean,
@@ -197,6 +197,7 @@
     private val smartspaceManager: SmartspaceManager?,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     private val mediaDataLoader: dagger.Lazy<MediaDataLoader>,
+    private val mediaLogger: MediaLogger,
 ) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener, MediaDataManager {
 
     companion object {
@@ -273,7 +274,6 @@
         mediaDeviceManager: MediaDeviceManager,
         mediaDataCombineLatest: MediaDataCombineLatest,
         mediaDataFilter: LegacyMediaDataFilterImpl,
-        activityStarter: ActivityStarter,
         smartspaceMediaDataProvider: SmartspaceMediaDataProvider,
         clock: SystemClock,
         tunerService: TunerService,
@@ -282,6 +282,7 @@
         smartspaceManager: SmartspaceManager?,
         keyguardUpdateMonitor: KeyguardUpdateMonitor,
         mediaDataLoader: dagger.Lazy<MediaDataLoader>,
+        mediaLogger: MediaLogger,
     ) : this(
         context,
         // Loading bitmap for UMO background can take longer time, so it cannot run on the default
@@ -301,7 +302,6 @@
         mediaDeviceManager,
         mediaDataCombineLatest,
         mediaDataFilter,
-        activityStarter,
         smartspaceMediaDataProvider,
         Utils.useMediaResumption(context),
         Utils.useQsMediaPlayer(context),
@@ -312,6 +312,7 @@
         smartspaceManager,
         keyguardUpdateMonitor,
         mediaDataLoader,
+        mediaLogger,
     )
 
     private val appChangeReceiver =
@@ -564,6 +565,42 @@
             val resumeAction: Runnable? = currentEntry?.resumeAction
             val hasCheckedForResume = currentEntry?.hasCheckedForResume == true
             val active = currentEntry?.active ?: true
+            val mediaController = mediaControllerFactory.create(result.token!!)
+
+            val mediaData =
+                MediaData(
+                    userId = sbn.normalizedUserId,
+                    initialized = true,
+                    app = result.appName,
+                    appIcon = result.appIcon,
+                    artist = result.artist,
+                    song = result.song,
+                    artwork = result.artworkIcon,
+                    actions = result.actionIcons,
+                    actionsToShowInCompact = result.actionsToShowInCompact,
+                    semanticActions = result.semanticActions,
+                    packageName = sbn.packageName,
+                    token = result.token,
+                    clickIntent = result.clickIntent,
+                    device = result.device,
+                    active = active,
+                    resumeAction = resumeAction,
+                    playbackLocation = result.playbackLocation,
+                    notificationKey = key,
+                    hasCheckedForResume = hasCheckedForResume,
+                    isPlaying = result.isPlaying,
+                    isClearable = !sbn.isOngoing,
+                    lastActive = lastActive,
+                    createdTimestampMillis = createdTimestampMillis,
+                    instanceId = instanceId,
+                    appUid = result.appUid,
+                    isExplicit = result.isExplicit,
+                )
+
+            if (isSameMediaData(context, mediaController, mediaData, currentEntry)) {
+                mediaLogger.logDuplicateMediaNotification(key)
+                return@withContext
+            }
 
             // We need to log the correct media added.
             if (isNewlyActiveEntry) {
@@ -583,40 +620,7 @@
                 )
             }
 
-            withContext(mainDispatcher) {
-                onMediaDataLoaded(
-                    key,
-                    oldKey,
-                    MediaData(
-                        userId = sbn.normalizedUserId,
-                        initialized = true,
-                        app = result.appName,
-                        appIcon = result.appIcon,
-                        artist = result.artist,
-                        song = result.song,
-                        artwork = result.artworkIcon,
-                        actions = result.actionIcons,
-                        actionsToShowInCompact = result.actionsToShowInCompact,
-                        semanticActions = result.semanticActions,
-                        packageName = sbn.packageName,
-                        token = result.token,
-                        clickIntent = result.clickIntent,
-                        device = result.device,
-                        active = active,
-                        resumeAction = resumeAction,
-                        playbackLocation = result.playbackLocation,
-                        notificationKey = key,
-                        hasCheckedForResume = hasCheckedForResume,
-                        isPlaying = result.isPlaying,
-                        isClearable = !sbn.isOngoing,
-                        lastActive = lastActive,
-                        createdTimestampMillis = createdTimestampMillis,
-                        instanceId = instanceId,
-                        appUid = result.appUid,
-                        isExplicit = result.isExplicit,
-                    )
-                )
-            }
+            withContext(mainDispatcher) { onMediaDataLoaded(key, oldKey, mediaData) }
         }
 
     /** Add a listener for changes in this class */
@@ -1100,6 +1104,47 @@
         val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
         val appUid = appInfo?.uid ?: Process.INVALID_UID
 
+        val lastActive = systemClock.elapsedRealtime()
+        val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
+        val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
+        val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
+        val active = mediaEntries[key]?.active ?: true
+        var mediaData =
+            MediaData(
+                sbn.normalizedUserId,
+                true,
+                appName,
+                smallIcon,
+                artist,
+                song,
+                artWorkIcon,
+                actionIcons,
+                actionsToShowCollapsed,
+                semanticActions,
+                sbn.packageName,
+                token,
+                notif.contentIntent,
+                device,
+                active,
+                resumeAction = resumeAction,
+                playbackLocation = playbackLocation,
+                notificationKey = key,
+                hasCheckedForResume = hasCheckedForResume,
+                isPlaying = isPlaying,
+                isClearable = !sbn.isOngoing,
+                lastActive = lastActive,
+                createdTimestampMillis = createdTimestampMillis,
+                instanceId = instanceId,
+                appUid = appUid,
+                isExplicit = isExplicit,
+                smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()),
+            )
+
+        if (isSameMediaData(context, mediaController, mediaData, currentEntry)) {
+            mediaLogger.logDuplicateMediaNotification(key)
+            return
+        }
+
         if (isNewlyActiveEntry) {
             logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId)
             logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
@@ -1107,44 +1152,17 @@
             logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation)
         }
 
-        val lastActive = systemClock.elapsedRealtime()
-        val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
         foregroundExecutor.execute {
-            val resumeAction: Runnable? = mediaEntries[key]?.resumeAction
-            val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
-            val active = mediaEntries[key]?.active ?: true
-            onMediaDataLoaded(
-                key,
-                oldKey,
-                MediaData(
-                    sbn.normalizedUserId,
-                    true,
-                    appName,
-                    smallIcon,
-                    artist,
-                    song,
-                    artWorkIcon,
-                    actionIcons,
-                    actionsToShowCollapsed,
-                    semanticActions,
-                    sbn.packageName,
-                    token,
-                    notif.contentIntent,
-                    device,
-                    active,
-                    resumeAction = resumeAction,
-                    playbackLocation = playbackLocation,
-                    notificationKey = key,
-                    hasCheckedForResume = hasCheckedForResume,
-                    isPlaying = isPlaying,
-                    isClearable = !sbn.isOngoing,
-                    lastActive = lastActive,
-                    createdTimestampMillis = createdTimestampMillis,
-                    instanceId = instanceId,
-                    appUid = appUid,
-                    isExplicit = isExplicit,
+            val oldResumeAction: Runnable? = mediaEntries[key]?.resumeAction
+            val oldHasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true
+            val oldActive = mediaEntries[key]?.active ?: true
+            mediaData =
+                mediaData.copy(
+                    resumeAction = oldResumeAction,
+                    hasCheckedForResume = oldHasCheckedForResume,
+                    active = oldActive
                 )
-            )
+            onMediaDataLoaded(key, oldKey, mediaData)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/resume/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/resume/MediaResumeListener.kt
index e4047e5..9ee59d1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/resume/MediaResumeListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/resume/MediaResumeListener.kt
@@ -80,6 +80,7 @@
             field?.disconnect()
             field = value
         }
+
     private var currentUserId: Int = context.userId
 
     @VisibleForTesting
@@ -89,7 +90,7 @@
                 if (Intent.ACTION_USER_UNLOCKED == intent.action) {
                     val userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
                     if (userId == currentUserId) {
-                        loadMediaResumptionControls()
+                        backgroundExecutor.execute { loadMediaResumptionControls() }
                     }
                 }
             }
@@ -254,15 +255,15 @@
             if (data.resumeAction == null && !data.hasCheckedForResume && isEligibleForResume) {
                 // TODO also check for a media button receiver intended for restarting (b/154127084)
                 // Set null action to prevent additional attempts to connect
-                mediaDataManager.setResumeAction(key, null)
-                Log.d(TAG, "Checking for service component for " + data.packageName)
-                val pm = context.packageManager
-                val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE)
-                val resumeInfo = pm.queryIntentServicesAsUser(serviceIntent, 0, currentUserId)
+                backgroundExecutor.execute {
+                    mediaDataManager.setResumeAction(key, null)
+                    Log.d(TAG, "Checking for service component for " + data.packageName)
+                    val pm = context.packageManager
+                    val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE)
+                    val resumeInfo = pm.queryIntentServicesAsUser(serviceIntent, 0, currentUserId)
 
-                val inf = resumeInfo?.filter { it.serviceInfo.packageName == data.packageName }
-                if (inf != null && inf.size > 0) {
-                    backgroundExecutor.execute {
+                    val inf = resumeInfo?.filter { it.serviceInfo.packageName == data.packageName }
+                    if (inf != null && inf.size > 0) {
                         tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName)
                     }
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt
index cef1e69..2a9fe83 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt
@@ -32,6 +32,7 @@
 import androidx.core.view.GestureDetectorCompat
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
+import com.android.systemui.Flags
 import com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.plugins.FalsingManager
@@ -102,9 +103,11 @@
             }
             _progress.postValue(value)
         }
+
     private val _progress = MutableLiveData<Progress>().apply { postValue(_data) }
     val progress: LiveData<Progress>
         get() = _progress
+
     private var controller: MediaController? = null
         set(value) {
             if (field?.sessionToken != value?.sessionToken) {
@@ -113,6 +116,7 @@
                 field = value
             }
         }
+
     private var playbackState: PlaybackState? = null
     private var callback =
         object : MediaController.Callback() {
@@ -128,6 +132,15 @@
             override fun onSessionDestroyed() {
                 clearController()
             }
+
+            override fun onMetadataChanged(metadata: MediaMetadata?) {
+                if (!Flags.mediaControlsPostsOptimization()) return
+
+                val (enabled, duration) = getEnabledStateAndDuration(metadata)
+                if (_data.duration != duration) {
+                    _data = _data.copy(enabled = enabled, duration = duration)
+                }
+            }
         }
     private var cancel: Runnable? = null
 
@@ -233,22 +246,13 @@
     fun updateController(mediaController: MediaController?) {
         controller = mediaController
         playbackState = controller?.playbackState
-        val mediaMetadata = controller?.metadata
+        val (enabled, duration) = getEnabledStateAndDuration(controller?.metadata)
         val seekAvailable = ((playbackState?.actions ?: 0L) and PlaybackState.ACTION_SEEK_TO) != 0L
         val position = playbackState?.position?.toInt()
-        val duration = mediaMetadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.toInt() ?: 0
         val playing =
             NotificationMediaManager.isPlayingState(
                 playbackState?.state ?: PlaybackState.STATE_NONE
             )
-        val enabled =
-            if (
-                playbackState == null ||
-                    playbackState?.getState() == PlaybackState.STATE_NONE ||
-                    (duration <= 0)
-            )
-                false
-            else true
         _data = Progress(enabled, seekAvailable, playing, scrubbing, position, duration, listening)
         checkIfPollingNeeded()
     }
@@ -368,6 +372,16 @@
         }
     }
 
+    /** returns a pair of whether seekbar is enabled and the duration of media. */
+    private fun getEnabledStateAndDuration(metadata: MediaMetadata?): Pair<Boolean, Int> {
+        val duration = metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.toInt() ?: 0
+        val enabled =
+            !(playbackState == null ||
+                playbackState?.state == PlaybackState.STATE_NONE ||
+                (duration <= 0))
+        return Pair(enabled, duration)
+    }
+
     /**
      * This method specifies if user made a bad seekbar grab or not. If the vertical distance from
      * first touch on seekbar is more than the horizontal distance, this means that the seekbar grab
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
index 18c6f53..4251b81 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java
@@ -307,7 +307,7 @@
 
     private void setUpDialog(AlertDialog dialog) {
         SystemUIDialog.registerDismissListener(dialog);
-        SystemUIDialog.applyFlags(dialog);
+        SystemUIDialog.applyFlags(dialog, /* showWhenLocked= */ false);
         SystemUIDialog.setDialogSize(dialog);
 
         dialog.setOnCancelListener(this::onDialogDismissedOrCancelled);
diff --git a/packages/SystemUI/src/com/android/systemui/qrcodescanner/dagger/QRCodeScannerModule.kt b/packages/SystemUI/src/com/android/systemui/qrcodescanner/dagger/QRCodeScannerModule.kt
index 3a8fe71..ef1f834 100644
--- a/packages/SystemUI/src/com/android/systemui/qrcodescanner/dagger/QRCodeScannerModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qrcodescanner/dagger/QRCodeScannerModule.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.Flags
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.qs.tiles.QRCodeScannerTile
 import com.android.systemui.qs.tiles.base.interactor.QSTileAvailabilityInteractor
@@ -51,7 +52,7 @@
     @IntoMap
     @StringKey(QR_CODE_SCANNER_TILE_SPEC)
     fun provideQrCodeScannerAvailabilityInteractor(
-            impl: QRCodeScannerTileDataInteractor
+        impl: QRCodeScannerTileDataInteractor
     ): QSTileAvailabilityInteractor
 
     companion object {
@@ -69,6 +70,7 @@
                         labelRes = R.string.qr_code_scanner_title,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.UTILITIES,
             )
 
         /** Inject QR Code Scanner Tile into tileViewModelMap in QSModule. */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.kt
index 28c1fbf..2f054b0a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.kt
@@ -24,10 +24,10 @@
 import com.android.systemui.qs.panels.shared.model.EditTileData
 import com.android.systemui.qs.pipeline.data.repository.InstalledTilesComponentRepository
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 import com.android.systemui.settings.UserTracker
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.withContext
 
 @SysUISingleton
@@ -60,6 +60,7 @@
                             Icon.Loaded(icon, ContentDescription.Loaded(label.toString())),
                             Text.Loaded(label.toString()),
                             Text.Loaded(appName.toString()),
+                            TileCategory.PROVIDED_BY_APP,
                         )
                     } else {
                         null
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.kt
index 3b29422..a2cee3b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.qs.panels.data.repository.StockTilesRepository
 import com.android.systemui.qs.panels.domain.model.EditTilesModel
 import com.android.systemui.qs.panels.shared.model.EditTileData
+import com.android.systemui.qs.shared.model.TileCategory
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
 import javax.inject.Inject
 
@@ -53,6 +54,7 @@
                         ),
                         Text.Resource(config.uiConfig.labelRes),
                         null,
+                        category = config.category,
                     )
                 } else {
                     EditTileData(
@@ -62,7 +64,8 @@
                             ContentDescription.Loaded(it.spec)
                         ),
                         Text.Loaded(it.spec),
-                        null
+                        null,
+                        category = TileCategory.UNKNOWN,
                     )
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt
index 8b70bb9..b153ef7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt
@@ -19,12 +19,14 @@
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 
 data class EditTileData(
     val tileSpec: TileSpec,
     val icon: Icon,
     val label: Text,
     val appName: Text?,
+    val category: TileCategory,
 ) {
     init {
         check(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
index 24af09d..93037d1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
@@ -34,6 +34,7 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.LocalOverscrollConfiguration
+import androidx.compose.foundation.background
 import androidx.compose.foundation.basicMarquee
 import androidx.compose.foundation.border
 import androidx.compose.foundation.combinedClickable
@@ -95,6 +96,8 @@
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.util.fastMap
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.animation.Expandable
 import com.android.compose.modifiers.background
@@ -115,6 +118,7 @@
 import com.android.systemui.qs.panels.ui.viewmodel.toUiState
 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.groupAndSort
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.res.R
 import java.util.function.Supplier
@@ -472,31 +476,39 @@
     onClick: (TileSpec) -> Unit,
     dragAndDropState: DragAndDropState,
 ) {
-    // Available tiles aren't visible during drag and drop, so the row isn't needed
-    val (otherTilesStock, otherTilesCustom) =
-        tiles.map { TileGridCell(it, 0) }.partition { it.tile.appName == null }
     val availableTileHeight = tileHeight(true)
     val availableGridHeight = gridHeight(tiles.size, availableTileHeight, columns, tilePadding)
 
+    // Available tiles aren't visible during drag and drop, so the row isn't needed
+    val groupedTiles =
+        remember(tiles.fastMap { it.tile.category }, tiles.fastMap { it.tile.label }) {
+            groupAndSort(tiles.fastMap { TileGridCell(it, 0) })
+        }
+    val labelColors = TileDefaults.inactiveTileColors()
     // Available tiles
     TileLazyGrid(
         modifier = Modifier.height(availableGridHeight).testTag(AVAILABLE_TILES_GRID_TEST_TAG),
         columns = GridCells.Fixed(columns)
     ) {
-        editTiles(
-            otherTilesStock,
-            ClickAction.ADD,
-            onClick,
-            dragAndDropState = dragAndDropState,
-            showLabels = true,
-        )
-        editTiles(
-            otherTilesCustom,
-            ClickAction.ADD,
-            onClick,
-            dragAndDropState = dragAndDropState,
-            showLabels = true,
-        )
+        groupedTiles.forEach { category, tiles ->
+            stickyHeader {
+                Text(
+                    text = category.label.load() ?: "",
+                    fontSize = 20.sp,
+                    color = labelColors.label,
+                    modifier =
+                        Modifier.background(Color.Black)
+                            .padding(start = 16.dp, bottom = 8.dp, top = 8.dp)
+                )
+            }
+            editTiles(
+                tiles,
+                ClickAction.ADD,
+                onClick,
+                dragAndDropState = dragAndDropState,
+                showLabels = true,
+            )
+        }
     }
 }
 
@@ -618,7 +630,7 @@
     showLabels: Boolean,
     modifier: Modifier = Modifier,
 ) {
-    val label = tileViewModel.label.load() ?: tileViewModel.tileSpec.spec
+    val label = tileViewModel.label.text
     val colors = TileDefaults.inactiveTileColors()
 
     TileContainer(
@@ -638,7 +650,7 @@
         } else {
             LargeTileContent(
                 label = label,
-                secondaryLabel = tileViewModel.appName?.load(),
+                secondaryLabel = tileViewModel.appName?.text,
                 icon = tileViewModel.icon,
                 colors = colors,
                 iconShape = RoundedCornerShape(TileDefaults.InactiveCornerRadius),
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt
index 8ca8de7..08ee856 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/model/TileGridCell.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.shared.model.splitInRowsSequence
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.shared.model.CategoryAndName
 
 /** Represents an item from a grid associated with a row and a span */
 interface GridCell {
@@ -38,7 +39,7 @@
     override val row: Int,
     override val width: Int,
     override val span: GridItemSpan = GridItemSpan(width)
-) : GridCell, SizedTile<EditTileViewModel> {
+) : GridCell, SizedTile<EditTileViewModel>, CategoryAndName by tile {
     val key: String = "${tile.tileSpec.spec}-$row"
 
     constructor(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
index 42715be..4a8aa83e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
@@ -16,6 +16,9 @@
 
 package com.android.systemui.qs.panels.ui.viewmodel
 
+import android.content.Context
+import androidx.compose.ui.util.fastMap
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.qs.panels.domain.interactor.EditTilesListInteractor
@@ -27,6 +30,7 @@
 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END
 import com.android.systemui.qs.pipeline.domain.interactor.MinimumTilesInteractor
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.util.kotlin.emitOnStart
 import javax.inject.Inject
 import javax.inject.Named
 import kotlinx.coroutines.CoroutineScope
@@ -35,6 +39,7 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
@@ -49,6 +54,8 @@
     private val currentTilesInteractor: CurrentTilesInteractor,
     private val tilesAvailabilityInteractor: TilesAvailabilityInteractor,
     private val minTilesInteractor: MinimumTilesInteractor,
+    private val configurationInteractor: ConfigurationInteractor,
+    @Application private val applicationContext: Context,
     @Named("Default") private val defaultGridLayout: GridLayout,
     @Application private val applicationScope: CoroutineScope,
     gridLayoutTypeInteractor: GridLayoutTypeInteractor,
@@ -99,38 +106,45 @@
                             .map { it.tileSpec }
                             .minus(currentTilesInteractor.currentTilesSpecs.toSet())
                     )
-                currentTilesInteractor.currentTiles.map { tiles ->
-                    val currentSpecs = tiles.map { it.spec }
-                    val canRemoveTiles = currentSpecs.size > minimumTiles
-                    val allTiles = editTilesData.stockTiles + editTilesData.customTiles
-                    val allTilesMap = allTiles.associate { it.tileSpec to it }
-                    val currentTiles = currentSpecs.map { allTilesMap.get(it) }.filterNotNull()
-                    val nonCurrentTiles = allTiles.filter { it.tileSpec !in currentSpecs }
+                currentTilesInteractor.currentTiles
+                    .map { tiles ->
+                        val currentSpecs = tiles.map { it.spec }
+                        val canRemoveTiles = currentSpecs.size > minimumTiles
+                        val allTiles = editTilesData.stockTiles + editTilesData.customTiles
+                        val allTilesMap = allTiles.associate { it.tileSpec to it }
+                        val currentTiles = currentSpecs.map { allTilesMap.get(it) }.filterNotNull()
+                        val nonCurrentTiles = allTiles.filter { it.tileSpec !in currentSpecs }
 
-                    (currentTiles + nonCurrentTiles)
-                        .filterNot { it.tileSpec in unavailable }
-                        .map {
-                            val current = it.tileSpec in currentSpecs
-                            val availableActions = buildSet {
-                                if (current) {
-                                    add(AvailableEditActions.MOVE)
-                                    if (canRemoveTiles) {
-                                        add(AvailableEditActions.REMOVE)
+                        (currentTiles + nonCurrentTiles)
+                            .filterNot { it.tileSpec in unavailable }
+                            .map {
+                                val current = it.tileSpec in currentSpecs
+                                val availableActions = buildSet {
+                                    if (current) {
+                                        add(AvailableEditActions.MOVE)
+                                        if (canRemoveTiles) {
+                                            add(AvailableEditActions.REMOVE)
+                                        }
+                                    } else {
+                                        add(AvailableEditActions.ADD)
                                     }
-                                } else {
-                                    add(AvailableEditActions.ADD)
                                 }
+                                UnloadedEditTileViewModel(
+                                    it.tileSpec,
+                                    it.icon,
+                                    it.label,
+                                    it.appName,
+                                    current,
+                                    availableActions,
+                                    it.category,
+                                )
                             }
-                            EditTileViewModel(
-                                it.tileSpec,
-                                it.icon,
-                                it.label,
-                                it.appName,
-                                current,
-                                availableActions
-                            )
-                        }
-                }
+                    }
+                    .combine(configurationInteractor.onAnyConfigurationChange.emitOnStart()) {
+                        tiles,
+                        _ ->
+                        tiles.fastMap { it.load(applicationContext) }
+                    }
             } else {
                 emptyFlow()
             }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
index a4c8638..ee12736f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
@@ -16,9 +16,15 @@
 
 package com.android.systemui.qs.panels.ui.viewmodel
 
+import android.content.Context
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.text.AnnotatedString
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.shared.model.Text
+import com.android.systemui.common.ui.compose.toAnnotatedString
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.CategoryAndName
+import com.android.systemui.qs.shared.model.TileCategory
 
 /**
  * View model for each tile that is available to be added/removed/moved in Edit mode.
@@ -26,14 +32,41 @@
  * [isCurrent] indicates whether this tile is part of the current set of tiles that the user sees in
  * Quick Settings.
  */
-data class EditTileViewModel(
+data class UnloadedEditTileViewModel(
     val tileSpec: TileSpec,
     val icon: Icon,
     val label: Text,
     val appName: Text?,
     val isCurrent: Boolean,
     val availableEditActions: Set<AvailableEditActions>,
-)
+    val category: TileCategory,
+) {
+    fun load(context: Context): EditTileViewModel {
+        return EditTileViewModel(
+            tileSpec,
+            icon,
+            label.toAnnotatedString(context) ?: AnnotatedString(tileSpec.spec),
+            appName?.toAnnotatedString(context),
+            isCurrent,
+            availableEditActions,
+            category,
+        )
+    }
+}
+
+@Immutable
+data class EditTileViewModel(
+    val tileSpec: TileSpec,
+    val icon: Icon,
+    val label: AnnotatedString,
+    val appName: AnnotatedString?,
+    val isCurrent: Boolean,
+    val availableEditActions: Set<AvailableEditActions>,
+    override val category: TileCategory,
+) : CategoryAndName {
+    override val name
+        get() = label.text
+}
 
 enum class AvailableEditActions {
     ADD,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/shared/model/TileCategory.kt b/packages/SystemUI/src/com/android/systemui/qs/shared/model/TileCategory.kt
new file mode 100644
index 0000000..59cb7d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/shared/model/TileCategory.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.shared.model
+
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.res.R
+
+/** Categories for tiles. This can be used to sort tiles in edit mode. */
+enum class TileCategory(val label: Text) {
+    CONNECTIVITY(Text.Resource(R.string.qs_edit_mode_category_connectivity)),
+    UTILITIES(Text.Resource(R.string.qs_edit_mode_category_utilities)),
+    DISPLAY(Text.Resource(R.string.qs_edit_mode_category_display)),
+    PRIVACY(Text.Resource(R.string.qs_edit_mode_category_privacy)),
+    ACCESSIBILITY(Text.Resource(R.string.qs_edit_mode_category_accessibility)),
+    PROVIDED_BY_APP(Text.Resource(R.string.qs_edit_mode_category_providedByApps)),
+    UNKNOWN(Text.Resource(R.string.qs_edit_mode_category_unknown)),
+}
+
+interface CategoryAndName {
+    val category: TileCategory
+    val name: String
+}
+
+/**
+ * Groups the elements of the list by [CategoryAndName.category] (with the keys sorted in the
+ * natural order of [TileCategory]), and sorts the elements of each group based on the
+ * [CategoryAndName.name].
+ */
+fun <T : CategoryAndName> groupAndSort(list: List<T>): Map<TileCategory, List<T>> {
+    val groupedByCategory = list.groupBy { it.category }.toSortedMap()
+    return groupedByCategory.mapValues { it.value.sortedBy { it.name } }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
index cdcefdb..3a9cb17 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfig.kt
@@ -21,6 +21,7 @@
 import androidx.annotation.StringRes
 import com.android.internal.logging.InstanceId
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 
 data class QSTileConfig
 @JvmOverloads
@@ -28,6 +29,7 @@
     val tileSpec: TileSpec,
     val uiConfig: QSTileUIConfig,
     val instanceId: InstanceId,
+    val category: TileCategory,
     val metricsSpec: String = tileSpec.spec,
     val policy: QSTilePolicy = QSTilePolicy.NoRestrictions,
     val autoRemoveOnUnavailable: Boolean = true,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProvider.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProvider.kt
index 0609e79..4dbddd9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigProvider.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 import javax.inject.Inject
 
 interface QSTileConfigProvider {
@@ -73,6 +74,7 @@
                     spec,
                     QSTileUIConfig.Empty,
                     qsEventLogger.getNewInstanceId(),
+                    category = TileCategory.PROVIDED_BY_APP,
                 )
             is TileSpec.Invalid ->
                 throw IllegalArgumentException("TileSpec.Invalid doesn't support configs")
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueModule.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueModule.kt
index 907b92c..c092c2f 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueModule.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.Flags
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.qs.tiles.RecordIssueTile
 import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
@@ -61,6 +62,7 @@
                         labelRes = R.string.qs_record_issue_label
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.UTILITIES,
             )
 
         /** Inject FlashlightTile into tileViewModelMap in QSModule */
diff --git a/packages/SystemUI/src/com/android/systemui/rotationlock/RotationLockNewModule.kt b/packages/SystemUI/src/com/android/systemui/rotationlock/RotationLockNewModule.kt
index 0589e6c6..c9712fc 100644
--- a/packages/SystemUI/src/com/android/systemui/rotationlock/RotationLockNewModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/rotationlock/RotationLockNewModule.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.camera.CameraRotationModule
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 import com.android.systemui.qs.tiles.base.interactor.QSTileAvailabilityInteractor
 import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
 import com.android.systemui.qs.tiles.impl.rotation.domain.interactor.RotationLockTileDataInteractor
@@ -42,8 +43,9 @@
     @IntoMap
     @StringKey(ROTATION_TILE_SPEC)
     fun provideRotationAvailabilityInteractor(
-            impl: RotationLockTileDataInteractor
+        impl: RotationLockTileDataInteractor
     ): QSTileAvailabilityInteractor
+
     companion object {
         private const val ROTATION_TILE_SPEC = "rotation"
 
@@ -60,6 +62,7 @@
                         labelRes = R.string.quick_settings_rotation_unlocked_label,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.DISPLAY,
             )
 
         /** Inject Rotation tile into tileViewModelMap in QSModule */
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt
index 738b184..e477efe 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.NotificationPresenter
 import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
@@ -34,6 +35,7 @@
 import javax.inject.Inject
 import javax.inject.Provider
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
@@ -45,6 +47,7 @@
 import kotlinx.coroutines.launch
 
 /** Business logic about the visibility of various parts of the window root view. */
+@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class WindowRootViewVisibilityInteractor
 @Inject
@@ -80,9 +83,9 @@
                         is ObservableTransitionState.Idle ->
                             flowOf(
                                 state.currentScene == Scenes.Shade ||
-                                    state.currentScene == Scenes.NotificationsShade ||
-                                    state.currentScene == Scenes.QuickSettingsShade ||
-                                    state.currentScene == Scenes.Lockscreen
+                                    state.currentScene == Scenes.Lockscreen ||
+                                    Overlays.NotificationsShade in state.currentOverlays ||
+                                    Overlays.QuickSettingsShade in state.currentOverlays
                             )
                         is ObservableTransitionState.Transition ->
                             if (
@@ -94,12 +97,12 @@
                             } else {
                                 flowOf(
                                     state.toContent == Scenes.Shade ||
-                                        state.toContent == Scenes.NotificationsShade ||
-                                        state.toContent == Scenes.QuickSettingsShade ||
+                                        state.toContent == Overlays.NotificationsShade ||
+                                        state.toContent == Overlays.QuickSettingsShade ||
                                         state.toContent == Scenes.Lockscreen ||
                                         state.fromContent == Scenes.Shade ||
-                                        state.fromContent == Scenes.NotificationsShade ||
-                                        state.fromContent == Scenes.QuickSettingsShade ||
+                                        state.fromContent == Overlays.NotificationsShade ||
+                                        state.fromContent == Overlays.QuickSettingsShade ||
                                         state.fromContent == Scenes.Lockscreen
                                 )
                             }
@@ -115,10 +118,9 @@
      * false if the device is asleep.
      */
     val isLockscreenOrShadeVisibleAndInteractive: StateFlow<Boolean> =
-        combine(
-                isLockscreenOrShadeVisible,
-                powerInteractor.isAwake,
-            ) { isKeyguardAodOrShadeVisible, isAwake ->
+        combine(isLockscreenOrShadeVisible, powerInteractor.isAwake) {
+                isKeyguardAodOrShadeVisible,
+                isAwake ->
                 isKeyguardAodOrShadeVisible && isAwake
             }
             .stateIn(scope, SharingStarted.Eagerly, initialValue = false)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt
index d1629c7..e352bfe 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/ScrimStartable.kt
@@ -19,7 +19,9 @@
 package com.android.systemui.scene.domain.startable
 
 import androidx.annotation.VisibleForTesting
+import com.android.compose.animation.scene.ContentKey
 import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.OverlayKey
 import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.CoreStartable
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
@@ -33,6 +35,7 @@
 import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor
 import com.android.systemui.statusbar.phone.DozeServiceHost
@@ -74,6 +77,7 @@
                 deviceEntryInteractor.isDeviceEntered,
                 occlusionInteractor.invisibleDueToOcclusion,
                 sceneInteractor.currentScene,
+                sceneInteractor.currentOverlays,
                 sceneInteractor.transitionState,
                 keyguardInteractor.isDozing,
                 keyguardInteractor.isDreaming,
@@ -95,34 +99,29 @@
                 val isDeviceEntered = flowValues[0] as Boolean
                 val isOccluded = flowValues[1] as Boolean
                 val currentScene = flowValues[2] as SceneKey
-                val transitionState = flowValues[3] as ObservableTransitionState
-                val isDozing = flowValues[4] as Boolean
-                val isDreaming = flowValues[5] as Boolean
-                val biometricUnlockState = flowValues[6] as BiometricUnlockModel
-                val isBrightnessMirrorVisible = flowValues[7] as Boolean
-                val isPulsing = flowValues[8] as Boolean
-                val hasPendingScreenOffCallback = flowValues[9] as Boolean
+                val currentOverlays = flowValues[3] as Set<OverlayKey>
+                val transitionState = flowValues[4] as ObservableTransitionState
+                val isDozing = flowValues[5] as Boolean
+                val isDreaming = flowValues[6] as Boolean
+                val biometricUnlockState = flowValues[7] as BiometricUnlockModel
+                val isBrightnessMirrorVisible = flowValues[8] as Boolean
+                val isPulsing = flowValues[9] as Boolean
+                val hasPendingScreenOffCallback = flowValues[10] as Boolean
 
                 // This is true when the lockscreen scene is either the current scene or somewhere
-                // in the
-                // navigation back stack of scenes.
+                // in the navigation back stack of scenes.
                 val isOnKeyguard = !isDeviceEntered
                 val isCurrentSceneBouncer = currentScene == Scenes.Bouncer
                 // This is true when moving away from one of the keyguard scenes to the gone scene.
-                // It
-                // happens only when unlocking or when dismissing a dismissible lockscreen.
+                // It happens only when unlocking or when dismissing a dismissible lockscreen.
                 val isTransitioningAwayFromKeyguard =
                     transitionState is ObservableTransitionState.Transition.ChangeScene &&
                         transitionState.fromScene.isKeyguard() &&
                         transitionState.toScene == Scenes.Gone
 
-                // This is true when any of the shade scenes is the current scene.
-                val isCurrentSceneShade = currentScene.isShade()
-                // This is true when moving into one of the shade scenes when a non-shade scene.
-                val isTransitioningToShade =
-                    transitionState is ObservableTransitionState.Transition.ChangeScene &&
-                        !transitionState.fromScene.isShade() &&
-                        transitionState.toScene.isShade()
+                // This is true when any of the shade scenes or overlays is the current content.
+                val isCurrentContentShade =
+                    currentScene.isShade() || currentOverlays.any { it.isShade() }
 
                 // This is true after completing a transition to communal.
                 val isIdleOnCommunal = transitionState.isIdle(Scenes.Communal)
@@ -137,10 +136,10 @@
 
                 if (alternateBouncerInteractor.isVisibleState()) {
                     // This will cancel the keyguardFadingAway animation if it is running. We need
-                    // to do
-                    // this as otherwise it can remain pending and leave keyguard in a weird state.
+                    // to do this as otherwise it can remain pending and leave keyguard in a weird
+                    // state.
                     onKeyguardFadedAway(isTransitioningAwayFromKeyguard)
-                    if (!isTransitioningToShade) {
+                    if (!transitionState.isTransitioningToShade()) {
                         // Safeguard which prevents the scrim from being stuck in the wrong
                         // state
                         Model(scrimState = ScrimState.KEYGUARD, unlocking = unlocking)
@@ -163,7 +162,7 @@
                     )
                 } else if (isBrightnessMirrorVisible) {
                     Model(scrimState = ScrimState.BRIGHTNESS_MIRROR, unlocking = unlocking)
-                } else if (isCurrentSceneShade && !isDeviceEntered) {
+                } else if (isCurrentContentShade && !isDeviceEntered) {
                     Model(scrimState = ScrimState.SHADE_LOCKED, unlocking = unlocking)
                 } else if (isPulsing) {
                     Model(scrimState = ScrimState.PULSING, unlocking = unlocking)
@@ -171,8 +170,8 @@
                     Model(scrimState = ScrimState.OFF, unlocking = unlocking)
                 } else if (isDozing && !unlocking) {
                     // This will cancel the keyguardFadingAway animation if it is running. We need
-                    // to do
-                    // this as otherwise it can remain pending and leave keyguard in a weird state.
+                    // to do this as otherwise it can remain pending and leave keyguard in a weird
+                    // state.
                     onKeyguardFadedAway(isTransitioningAwayFromKeyguard)
                     Model(scrimState = ScrimState.AOD, unlocking = false)
                 } else if (isIdleOnCommunal) {
@@ -222,15 +221,24 @@
         return this == Scenes.Lockscreen || this == Scenes.Bouncer
     }
 
-    private fun SceneKey.isShade(): Boolean {
+    private fun ContentKey.isShade(): Boolean {
         return this == Scenes.Shade ||
             this == Scenes.QuickSettings ||
-            this == Scenes.NotificationsShade ||
-            this == Scenes.QuickSettingsShade
+            this == Overlays.NotificationsShade ||
+            this == Overlays.QuickSettingsShade
     }
 
-    private data class Model(
-        val scrimState: ScrimState,
-        val unlocking: Boolean,
-    )
+    private fun ObservableTransitionState.isTransitioningToShade(): Boolean {
+        return when (this) {
+            is ObservableTransitionState.Idle -> false
+            is ObservableTransitionState.Transition.ChangeScene ->
+                !fromScene.isShade() && toScene.isShade()
+            is ObservableTransitionState.Transition.ReplaceOverlay ->
+                !fromOverlay.isShade() && toOverlay.isShade()
+            is ObservableTransitionState.Transition.ShowOrHideOverlay ->
+                !fromContent.isShade() && toContent.isShade()
+        }
+    }
+
+    private data class Model(val scrimState: ScrimState, val unlocking: Boolean)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt
index a830e1b..9a9c576 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordModule.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.log.LogBufferFactory
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.qs.tiles.ScreenRecordTile
 import com.android.systemui.qs.tiles.base.interactor.QSTileAvailabilityInteractor
@@ -74,6 +75,7 @@
                         labelRes = R.string.quick_settings_screen_record_label,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.DISPLAY,
             )
 
         /** Inject ScreenRecord Tile into tileViewModelMap in QSModule */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index f88fd7d..862f33bb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -63,6 +63,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.internal.annotations.KeepForWeakReference;
 import com.android.internal.os.SomeArgs;
 import com.android.internal.statusbar.IAddTileResultCallback;
 import com.android.internal.statusbar.IStatusBar;
@@ -192,10 +193,10 @@
     private static final String SHOW_IME_SWITCHER_KEY = "showImeSwitcherKey";
 
     private final Object mLock = new Object();
-    private ArrayList<Callbacks> mCallbacks = new ArrayList<>();
-    private Handler mHandler = new H(Looper.getMainLooper());
+    private final ArrayList<Callbacks> mCallbacks = new ArrayList<>();
+    private final Handler mHandler = new H(Looper.getMainLooper());
     /** A map of display id - disable flag pair */
-    private SparseArray<Pair<Integer, Integer>> mDisplayDisabled = new SparseArray<>();
+    private final SparseArray<Pair<Integer, Integer>> mDisplayDisabled = new SparseArray<>();
     /**
      * The last ID of the display where IME window for which we received setImeWindowStatus
      * event.
@@ -207,6 +208,21 @@
     private final @Nullable DumpHandler mDumpHandler;
     private final @Nullable Lazy<PowerInteractor> mPowerInteractor;
 
+    @KeepForWeakReference
+    private final DisplayTracker.Callback mDisplayTrackerCallback = new DisplayTracker.Callback() {
+        @Override
+        public void onDisplayRemoved(int displayId) {
+            synchronized (mLock) {
+                mDisplayDisabled.remove(displayId);
+            }
+            // This callback is registered with {@link #mHandler} that already posts to run on
+            // main thread, so it is safe to dispatch directly.
+            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+                mCallbacks.get(i).onDisplayRemoved(displayId);
+            }
+        }
+    };
+
     /**
      * These methods are called back on the main thread.
      */
@@ -576,19 +592,8 @@
         mDisplayTracker = displayTracker;
         mRegistry = registry;
         mDumpHandler = dumpHandler;
-        mDisplayTracker.addDisplayChangeCallback(new DisplayTracker.Callback() {
-            @Override
-            public void onDisplayRemoved(int displayId) {
-                synchronized (mLock) {
-                    mDisplayDisabled.remove(displayId);
-                }
-                // This callback is registered with {@link #mHandler} that already posts to run on
-                // main thread, so it is safe to dispatch directly.
-                for (int i = mCallbacks.size() - 1; i >= 0; i--) {
-                    mCallbacks.get(i).onDisplayRemoved(displayId);
-                }
-            }
-        }, new HandlerExecutor(mHandler));
+        mDisplayTracker.addDisplayChangeCallback(mDisplayTrackerCallback,
+                new HandlerExecutor(mHandler));
         // We always have default display.
         setDisabled(mDisplayTracker.getDefaultDisplayId(), DISABLE_NONE, DISABLE2_NONE);
         mPowerInteractor = powerInteractor;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
index 400f8af..dac0102 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.flags.Flags.SIGNAL_CALLBACK_DEPRECATION
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.qs.tiles.AirplaneModeTile
 import com.android.systemui.qs.tiles.BluetoothTile
@@ -95,21 +96,21 @@
     @IntoMap
     @StringKey(AIRPLANE_MODE_TILE_SPEC)
     fun provideAirplaneModeAvailabilityInteractor(
-            impl: AirplaneModeTileDataInteractor
+        impl: AirplaneModeTileDataInteractor
     ): QSTileAvailabilityInteractor
 
     @Binds
     @IntoMap
     @StringKey(DATA_SAVER_TILE_SPEC)
     fun provideDataSaverAvailabilityInteractor(
-            impl: DataSaverTileDataInteractor
+        impl: DataSaverTileDataInteractor
     ): QSTileAvailabilityInteractor
 
     @Binds
     @IntoMap
     @StringKey(INTERNET_TILE_SPEC)
     fun provideInternetAvailabilityInteractor(
-            impl: InternetTileDataInteractor
+        impl: InternetTileDataInteractor
     ): QSTileAvailabilityInteractor
 
     companion object {
@@ -149,6 +150,7 @@
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
                 policy = QSTilePolicy.Restricted(listOf(UserManager.DISALLOW_AIRPLANE_MODE)),
+                category = TileCategory.CONNECTIVITY,
             )
 
         /** Inject AirplaneModeTile into tileViewModelMap in QSModule */
@@ -180,6 +182,7 @@
                         labelRes = R.string.data_saver,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.CONNECTIVITY,
             )
 
         /** Inject DataSaverTile into tileViewModelMap in QSModule */
@@ -211,6 +214,7 @@
                         labelRes = R.string.quick_settings_internet_label,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.CONNECTIVITY,
             )
 
         /** Inject InternetTile into tileViewModelMap in QSModule */
@@ -242,6 +246,7 @@
                         labelRes = R.string.quick_settings_hotspot_label,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.CONNECTIVITY,
             )
 
         @Provides
@@ -256,6 +261,7 @@
                         labelRes = R.string.quick_settings_cast_title,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.CONNECTIVITY,
             )
 
         @Provides
@@ -270,6 +276,7 @@
                         labelRes = R.string.quick_settings_bluetooth_label,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.CONNECTIVITY,
             )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index c046168..0ad1042a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -480,10 +480,16 @@
     }
 
     public static AlertDialog applyFlags(AlertDialog dialog) {
+        return applyFlags(dialog, true);
+    }
+
+    public static AlertDialog applyFlags(AlertDialog dialog, boolean showWhenLocked) {
         final Window window = dialog.getWindow();
         window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
-        window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
-                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+        window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
+        if (showWhenLocked) {
+            window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+        }
         window.getAttributes().setFitInsetsTypes(
                 window.getAttributes().getFitInsetsTypes() & ~Type.statusBars());
         return dialog;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
index fc7a672..bc7d376 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -64,9 +64,6 @@
         const val COL_NAME_IS_ENABLED = "isEnabled"
         /** Column name to use for [isWifiDefault] for table logging. */
         const val COL_NAME_IS_DEFAULT = "isDefault"
-
-        const val CARRIER_MERGED_INVALID_SUB_ID_REASON =
-            "Wifi network was carrier merged but had invalid sub ID"
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
index 7163e67..f4bb1a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
@@ -46,7 +46,7 @@
     private val _isWifiDefault = MutableStateFlow(false)
     override val isWifiDefault: StateFlow<Boolean> = _isWifiDefault
 
-    private val _wifiNetwork = MutableStateFlow<WifiNetworkModel>(WifiNetworkModel.Inactive)
+    private val _wifiNetwork = MutableStateFlow<WifiNetworkModel>(WifiNetworkModel.Inactive())
     override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork
 
     private val _secondaryNetworks = MutableStateFlow<List<WifiNetworkModel>>(emptyList())
@@ -82,7 +82,7 @@
         _isWifiEnabled.value = false
         _isWifiDefault.value = false
         _wifiActivity.value = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
-        _wifiNetwork.value = WifiNetworkModel.Inactive
+        _wifiNetwork.value = WifiNetworkModel.Inactive()
     }
 
     private fun processEnabledWifiState(event: FakeWifiEventModel.Wifi) {
@@ -100,7 +100,7 @@
     }
 
     private fun FakeWifiEventModel.Wifi.toWifiNetworkModel(): WifiNetworkModel =
-        WifiNetworkModel.Active(
+        WifiNetworkModel.Active.of(
             isValidated = validated ?: true,
             level = level ?: 0,
             ssid = ssid ?: DEMO_NET_SSID,
@@ -108,7 +108,7 @@
         )
 
     private fun FakeWifiEventModel.CarrierMerged.toCarrierMergedModel(): WifiNetworkModel =
-        WifiNetworkModel.CarrierMerged(
+        WifiNetworkModel.CarrierMerged.of(
             subscriptionId = subscriptionId,
             level = level,
             numberOfLevels = numberOfLevels,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
index b6e73e0..76024cd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt
@@ -19,7 +19,6 @@
 import android.annotation.SuppressLint
 import android.net.wifi.ScanResult
 import android.net.wifi.WifiManager
-import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.LifecycleRegistry
@@ -39,18 +38,14 @@
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.CARRIER_MERGED_INVALID_SUB_ID_REASON
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_DEFAULT
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_ENABLED
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
-import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel.Inactive.toHotspotDeviceType
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel.Unavailable.toHotspotDeviceType
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiScanEntry
 import com.android.wifitrackerlib.HotspotNetworkEntry
 import com.android.wifitrackerlib.MergedCarrierEntry
 import com.android.wifitrackerlib.WifiEntry
-import com.android.wifitrackerlib.WifiEntry.WIFI_LEVEL_MAX
-import com.android.wifitrackerlib.WifiEntry.WIFI_LEVEL_MIN
-import com.android.wifitrackerlib.WifiEntry.WIFI_LEVEL_UNREACHABLE
 import com.android.wifitrackerlib.WifiPickerTracker
 import java.util.concurrent.Executor
 import javax.inject.Inject
@@ -246,36 +241,28 @@
     }
 
     private fun MergedCarrierEntry.convertCarrierMergedToModel(): WifiNetworkModel {
-        return if (this.subscriptionId == INVALID_SUBSCRIPTION_ID) {
-            WifiNetworkModel.Invalid(CARRIER_MERGED_INVALID_SUB_ID_REASON)
-        } else {
-            WifiNetworkModel.CarrierMerged(
-                subscriptionId = this.subscriptionId,
-                level = this.level,
-                // WifiManager APIs to calculate the signal level start from 0, so
-                // maxSignalLevel + 1 represents the total level buckets count.
-                numberOfLevels = wifiManager.maxSignalLevel + 1,
-            )
-        }
+        // WifiEntry instance values aren't guaranteed to be stable between method calls
+        // because
+        // WifiPickerTracker is continuously updating the same object. Save the level in a
+        // local
+        // variable so that checking the level validity here guarantees that the level will
+        // still be
+        // valid when we create the `WifiNetworkModel.Active` instance later. Otherwise, the
+        // level
+        // could be valid here but become invalid later, and `WifiNetworkModel.Active` will
+        // throw
+        // an exception. See b/362384551.
+
+        return WifiNetworkModel.CarrierMerged.of(
+            subscriptionId = this.subscriptionId,
+            level = this.level,
+            // WifiManager APIs to calculate the signal level start from 0, so
+            // maxSignalLevel + 1 represents the total level buckets count.
+            numberOfLevels = wifiManager.maxSignalLevel + 1,
+        )
     }
 
     private fun WifiEntry.convertNormalToModel(): WifiNetworkModel {
-        // WifiEntry instance values aren't guaranteed to be stable between method calls because
-        // WifiPickerTracker is continuously updating the same object. Save the level in a local
-        // variable so that checking the level validity here guarantees that the level will still be
-        // valid when we create the `WifiNetworkModel.Active` instance later. Otherwise, the level
-        // could be valid here but become invalid later, and `WifiNetworkModel.Active` will throw
-        // an exception. See b/362384551.
-        val currentLevel = this.level
-        if (
-            currentLevel == WIFI_LEVEL_UNREACHABLE ||
-                currentLevel !in WIFI_LEVEL_MIN..WIFI_LEVEL_MAX
-        ) {
-            // If our level means the network is unreachable or the level is otherwise invalid, we
-            // don't have an active network.
-            return WifiNetworkModel.Inactive
-        }
-
         val hotspotDeviceType =
             if (this is HotspotNetworkEntry) {
                 this.deviceType.toHotspotDeviceType()
@@ -283,9 +270,9 @@
                 WifiNetworkModel.HotspotDeviceType.NONE
             }
 
-        return WifiNetworkModel.Active(
+        return WifiNetworkModel.Active.of(
             isValidated = this.hasInternetAccess(),
-            level = currentLevel,
+            level = this.level,
             ssid = this.title,
             hotspotDeviceType = hotspotDeviceType,
         )
@@ -421,7 +408,7 @@
 
     companion object {
         // Start out with no known wifi network.
-        @VisibleForTesting val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive
+        @VisibleForTesting val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive()
 
         private const val WIFI_STATE_DEFAULT = WifiManager.WIFI_STATE_DISABLED
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModel.kt
index 39842fb..3220377 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.shared.model
 
+import android.net.wifi.WifiManager
 import android.net.wifi.WifiManager.UNKNOWN_SSID
 import android.net.wifi.sharedconnectivity.app.NetworkProviderInfo
 import android.telephony.SubscriptionManager
@@ -23,8 +24,12 @@
 import com.android.systemui.log.table.Diffable
 import com.android.systemui.log.table.TableRowLogger
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel.Active.Companion.MAX_VALID_LEVEL
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel.Active.Companion.isValid
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel.Active.Companion.of
 import com.android.wifitrackerlib.HotspotNetworkEntry.DeviceType
 import com.android.wifitrackerlib.WifiEntry
+import com.android.wifitrackerlib.WifiEntry.WIFI_LEVEL_UNREACHABLE
 
 /** Provides information about the current wifi network. */
 sealed class WifiNetworkModel : Diffable<WifiNetworkModel> {
@@ -64,7 +69,7 @@
         /** A description of why the wifi information was invalid. */
         val invalidReason: String,
     ) : WifiNetworkModel() {
-        override fun toString() = "WifiNetwork.Invalid[$invalidReason]"
+        override fun toString() = "WifiNetwork.Invalid[reason=$invalidReason]"
 
         override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
             if (prevVal !is Invalid) {
@@ -73,12 +78,12 @@
             }
 
             if (invalidReason != prevVal.invalidReason) {
-                row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason")
+                row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE[reason=$invalidReason]")
             }
         }
 
         override fun logFull(row: TableRowLogger) {
-            row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE $invalidReason")
+            row.logChange(COL_NETWORK_TYPE, "$TYPE_UNAVAILABLE[reason=$invalidReason]")
             row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
             row.logChange(COL_VALIDATED, false)
             row.logChange(COL_LEVEL, LEVEL_DEFAULT)
@@ -89,20 +94,25 @@
     }
 
     /** A model representing that we have no active wifi network. */
-    object Inactive : WifiNetworkModel() {
-        override fun toString() = "WifiNetwork.Inactive"
+    data class Inactive(
+        /** An optional description of why the wifi information was inactive. */
+        val inactiveReason: String? = null,
+    ) : WifiNetworkModel() {
+        override fun toString() = "WifiNetwork.Inactive[reason=$inactiveReason]"
 
         override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) {
-            if (prevVal is Inactive) {
+            if (prevVal !is Inactive) {
+                logFull(row)
                 return
             }
 
-            // When changing to Inactive, we need to log diffs to all the fields.
-            logFull(row)
+            if (inactiveReason != prevVal.inactiveReason) {
+                row.logChange(COL_NETWORK_TYPE, "$TYPE_INACTIVE[reason=$inactiveReason]")
+            }
         }
 
         override fun logFull(row: TableRowLogger) {
-            row.logChange(COL_NETWORK_TYPE, TYPE_INACTIVE)
+            row.logChange(COL_NETWORK_TYPE, "$TYPE_INACTIVE[reason=$inactiveReason]")
             row.logChange(COL_SUB_ID, SUB_ID_DEFAULT)
             row.logChange(COL_VALIDATED, false)
             row.logChange(COL_LEVEL, LEVEL_DEFAULT)
@@ -117,31 +127,71 @@
      * treated as more of a mobile network.
      *
      * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information.
+     *
+     * IMPORTANT: Do *not* call [copy] on this class. Instead, use the factory [of] methods. [of]
+     * will verify preconditions correctly.
      */
-    data class CarrierMerged(
+    data class CarrierMerged
+    private constructor(
         /**
          * The subscription ID that this connection represents.
          *
          * Comes from [android.net.wifi.WifiInfo.getSubscriptionId].
          *
-         * Per that method, this value must not be [INVALID_SUBSCRIPTION_ID] (if it was invalid,
-         * then this is *not* a carrier merged network).
+         * Per that method, this value must not be [SubscriptionManager.INVALID_SUBSCRIPTION_ID] (if
+         * it was invalid, then this is *not* a carrier merged network).
          */
         val subscriptionId: Int,
 
-        /** The signal level, guaranteed to be 0 <= level <= numberOfLevels. */
+        /** The signal level, required to be 0 <= level <= numberOfLevels. */
         val level: Int,
 
         /** The maximum possible level. */
-        val numberOfLevels: Int = MobileConnectionRepository.DEFAULT_NUM_LEVELS,
+        val numberOfLevels: Int,
     ) : WifiNetworkModel() {
-        init {
-            require(level in MIN_VALID_LEVEL..numberOfLevels) {
-                "CarrierMerged: $MIN_VALID_LEVEL <= wifi level <= $numberOfLevels required; " +
+        companion object {
+            /**
+             * Creates a [CarrierMerged] instance, or an [Invalid] instance if any of the arguments
+             * are invalid.
+             */
+            fun of(
+                subscriptionId: Int,
+                level: Int,
+                numberOfLevels: Int = MobileConnectionRepository.DEFAULT_NUM_LEVELS
+            ): WifiNetworkModel {
+                if (!subscriptionId.isSubscriptionIdValid()) {
+                    return Invalid(INVALID_SUB_ID_ERROR_STRING)
+                }
+                if (!level.isLevelValid(numberOfLevels)) {
+                    return Invalid(getInvalidLevelErrorString(level, numberOfLevels))
+                }
+                return CarrierMerged(subscriptionId, level, numberOfLevels)
+            }
+
+            private fun Int.isLevelValid(maxLevel: Int): Boolean {
+                return this != WIFI_LEVEL_UNREACHABLE && this in MIN_VALID_LEVEL..maxLevel
+            }
+
+            private fun getInvalidLevelErrorString(level: Int, maxLevel: Int): String {
+                return "Wifi network was carrier merged but had invalid level. " +
+                    "$MIN_VALID_LEVEL <= wifi level <= $maxLevel required; " +
                     "level was $level"
             }
-            require(subscriptionId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
-                "subscription ID cannot be invalid"
+
+            private fun Int.isSubscriptionIdValid(): Boolean {
+                return this != SubscriptionManager.INVALID_SUBSCRIPTION_ID
+            }
+
+            private const val INVALID_SUB_ID_ERROR_STRING =
+                "Wifi network was carrier merged but had invalid sub ID"
+        }
+
+        init {
+            require(level.isLevelValid(numberOfLevels)) {
+                "${getInvalidLevelErrorString(level, numberOfLevels)}. $DO_NOT_USE_COPY_ERROR"
+            }
+            require(subscriptionId.isSubscriptionIdValid()) {
+                "$INVALID_SUB_ID_ERROR_STRING. $DO_NOT_USE_COPY_ERROR"
             }
         }
 
@@ -173,28 +223,64 @@
         }
     }
 
-    /** Provides information about an active wifi network. */
-    data class Active(
+    /**
+     * Provides information about an active wifi network.
+     *
+     * IMPORTANT: Do *not* call [copy] on this class. Instead, use the factory [of] method. [of]
+     * will verify preconditions correctly.
+     */
+    data class Active
+    private constructor(
         /** See [android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED]. */
-        val isValidated: Boolean = false,
+        val isValidated: Boolean,
 
-        /** The wifi signal level, guaranteed to be 0 <= level <= 4. */
+        /** The wifi signal level, required to be 0 <= level <= 4. */
         val level: Int,
 
         /** See [android.net.wifi.WifiInfo.ssid]. */
-        val ssid: String? = null,
+        val ssid: String?,
 
         /**
          * The type of device providing a hotspot connection, or [HotspotDeviceType.NONE] if this
          * isn't a hotspot connection.
          */
-        val hotspotDeviceType: HotspotDeviceType = WifiNetworkModel.HotspotDeviceType.NONE,
+        val hotspotDeviceType: HotspotDeviceType,
     ) : WifiNetworkModel() {
-        init {
-            require(level in MIN_VALID_LEVEL..MAX_VALID_LEVEL) {
-                "Active: $MIN_VALID_LEVEL <= wifi level <= $MAX_VALID_LEVEL required; " +
+        companion object {
+            /**
+             * Creates an [Active] instance, or an [Inactive] instance if any of the arguments are
+             * invalid.
+             */
+            @JvmStatic
+            fun of(
+                isValidated: Boolean = false,
+                level: Int,
+                ssid: String? = null,
+                hotspotDeviceType: HotspotDeviceType = HotspotDeviceType.NONE,
+            ): WifiNetworkModel {
+                if (!level.isValid()) {
+                    return Inactive(getInvalidLevelErrorString(level))
+                }
+                return Active(isValidated, level, ssid, hotspotDeviceType)
+            }
+
+            private fun Int.isValid(): Boolean {
+                return this != WIFI_LEVEL_UNREACHABLE && this in MIN_VALID_LEVEL..MAX_VALID_LEVEL
+            }
+
+            private fun getInvalidLevelErrorString(level: Int): String {
+                return "Wifi network was active but had invalid level. " +
+                    "$MIN_VALID_LEVEL <= wifi level <= $MAX_VALID_LEVEL required; " +
                     "level was $level"
             }
+
+            @VisibleForTesting internal const val MAX_VALID_LEVEL = WifiEntry.WIFI_LEVEL_MAX
+        }
+
+        init {
+            require(level.isValid()) {
+                "${getInvalidLevelErrorString(level)}. $DO_NOT_USE_COPY_ERROR"
+            }
         }
 
         /** Returns true if this network has a valid SSID and false otherwise. */
@@ -231,10 +317,6 @@
             row.logChange(COL_SSID, ssid)
             row.logChange(COL_HOTSPOT, hotspotDeviceType.name)
         }
-
-        companion object {
-            @VisibleForTesting internal const val MAX_VALID_LEVEL = WifiEntry.WIFI_LEVEL_MAX
-        }
     }
 
     companion object {
@@ -292,3 +374,7 @@
 val LEVEL_DEFAULT: String? = null
 val NUM_LEVELS_DEFAULT: String? = null
 val SUB_ID_DEFAULT: String? = null
+
+private const val DO_NOT_USE_COPY_ERROR =
+    "This should only be an issue if the caller incorrectly used `copy` to get a new instance. " +
+        "Please use the `of` method instead."
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
index 591d7af..cf9f9f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.Flags
 import com.android.systemui.qs.QsEventLogger
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 import com.android.systemui.qs.tileimpl.QSTileImpl
 import com.android.systemui.qs.tiles.AlarmTile
 import com.android.systemui.qs.tiles.CameraToggleTile
@@ -157,6 +158,7 @@
                         labelRes = R.string.quick_settings_flashlight_label,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.UTILITIES,
             )
 
         /** Inject FlashlightTile into tileViewModelMap in QSModule */
@@ -192,7 +194,8 @@
                 policy =
                     QSTilePolicy.Restricted(
                         listOf(DISALLOW_SHARE_LOCATION, DISALLOW_CONFIG_LOCATION)
-                    )
+                    ),
+                category = TileCategory.PRIVACY,
             )
 
         /** Inject LocationTile into tileViewModelMap in QSModule */
@@ -225,6 +228,7 @@
                         labelRes = R.string.status_bar_alarm,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.UTILITIES,
             )
 
         /** Inject AlarmTile into tileViewModelMap in QSModule */
@@ -257,6 +261,7 @@
                         labelRes = R.string.quick_settings_ui_mode_night_label,
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
+                category = TileCategory.DISPLAY,
             )
 
         /** Inject uimodenight into tileViewModelMap in QSModule */
@@ -290,6 +295,7 @@
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
                 autoRemoveOnUnavailable = false,
+                category = TileCategory.PRIVACY,
             )
 
         /** Inject work mode into tileViewModelMap in QSModule */
@@ -323,6 +329,7 @@
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
                 policy = QSTilePolicy.Restricted(listOf(DISALLOW_CAMERA_TOGGLE)),
+                category = TileCategory.PRIVACY,
             )
 
         /** Inject camera toggle tile into tileViewModelMap in QSModule */
@@ -365,6 +372,7 @@
                     ),
                 instanceId = uiEventLogger.getNewInstanceId(),
                 policy = QSTilePolicy.Restricted(listOf(DISALLOW_MICROPHONE_TOGGLE)),
+                category = TileCategory.PRIVACY,
             )
 
         /** Inject microphone toggle tile into tileViewModelMap in QSModule */
@@ -407,6 +415,7 @@
                             labelRes = R.string.quick_settings_modes_label,
                         ),
                     instanceId = uiEventLogger.getNewInstanceId(),
+                    category = TileCategory.CONNECTIVITY,
                 )
             } else {
                 QSTileConfig(
@@ -417,6 +426,7 @@
                             labelRes = R.string.quick_settings_dnd_label,
                         ),
                     instanceId = uiEventLogger.getNewInstanceId(),
+                    category = TileCategory.CONNECTIVITY,
                 )
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java b/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java
index ea213cb..dd1c11d 100644
--- a/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java
+++ b/packages/SystemUI/src/com/android/systemui/wallet/dagger/WalletModule.java
@@ -25,6 +25,7 @@
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.qs.QsEventLogger;
 import com.android.systemui.qs.pipeline.shared.TileSpec;
+import com.android.systemui.qs.shared.model.TileCategory;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.qs.tiles.QuickAccessWalletTile;
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig;
@@ -34,8 +35,6 @@
 import com.android.systemui.wallet.controller.WalletContextualLocationsService;
 import com.android.systemui.wallet.ui.WalletActivity;
 
-import java.util.concurrent.Executor;
-
 import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
@@ -43,6 +42,8 @@
 import dagger.multibindings.IntoMap;
 import dagger.multibindings.StringKey;
 
+import java.util.concurrent.Executor;
+
 /**
  * Module for injecting classes in Wallet.
  */
@@ -90,6 +91,7 @@
                         R.string.wallet_title
                 ),
                 uiEventLogger.getNewInstanceId(),
+                TileCategory.UTILITIES,
                 tileSpec.getSpec(),
                 QSTilePolicy.NoRestrictions.INSTANCE
         );
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java
index d85b774..bf13ceb 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/CarrierTextManagerTest.java
@@ -445,7 +445,7 @@
 
         assertFalse(mWifiRepository.isWifiConnectedWithValidSsid());
         mWifiRepository.setWifiNetwork(
-                new WifiNetworkModel.Active(
+                WifiNetworkModel.Active.Companion.of(
                         /* isValidated= */ false,
                         /* level= */ 0,
                         /* ssid= */ "",
diff --git a/packages/SystemUI/tests/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartableTest.kt
new file mode 100644
index 0000000..9da6885
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartableTest.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.inputdevice.tutorial
+
+import android.content.Context
+import android.content.Intent
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.inputdevice.tutorial.ui.TutorialNotificationCoordinator
+import com.android.systemui.testKosmos
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyboardTouchpadTutorialCoreStartableTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val broadcastDispatcher = kosmos.broadcastDispatcher
+    private val context = mock<Context>()
+    private val underTest =
+        KeyboardTouchpadTutorialCoreStartable(
+            { mock<TutorialNotificationCoordinator>() },
+            broadcastDispatcher,
+            context
+        )
+
+    @Test
+    fun registersBroadcastReceiverStartingActivityAsSystemUser() {
+        underTest.start()
+
+        broadcastDispatcher.sendIntentToMatchingReceiversOnly(
+            context,
+            Intent("com.android.systemui.action.KEYBOARD_TOUCHPAD_TUTORIAL")
+        )
+
+        verify(context).startActivityAsUser(any(), eq(UserHandle.SYSTEM))
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
index 3c74374..823a23d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
@@ -38,6 +38,8 @@
 import android.media.session.PlaybackState
 import android.net.Uri
 import android.os.Bundle
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.FlagsParameterization
 import android.provider.Settings
 import android.service.notification.StatusBarNotification
@@ -61,6 +63,8 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.media.controls.domain.resume.MediaResumeListener
 import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
+import com.android.systemui.media.controls.shared.mediaLogger
+import com.android.systemui.media.controls.shared.mockMediaLogger
 import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
 import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC
 import com.android.systemui.media.controls.shared.model.MediaData
@@ -69,7 +73,6 @@
 import com.android.systemui.media.controls.util.MediaUiEventLogger
 import com.android.systemui.media.controls.util.fakeMediaControllerFactory
 import com.android.systemui.media.controls.util.mediaFlags
-import com.android.systemui.plugins.activityStarter
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.SbnBuilder
 import com.android.systemui.testKosmos
@@ -186,11 +189,10 @@
         mSetFlagsRule.setFlagsParameterization(flags)
     }
 
-    private val kosmos = testKosmos()
+    private val kosmos = testKosmos().apply { mediaLogger = mockMediaLogger }
     private val testDispatcher = kosmos.testDispatcher
     private val testScope = kosmos.testScope
     private val fakeFeatureFlags = kosmos.fakeFeatureFlagsClassic
-    private val activityStarter = kosmos.activityStarter
     private val mediaControllerFactory = kosmos.fakeMediaControllerFactory
     private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20)
 
@@ -240,7 +242,6 @@
                 mediaDeviceManager = mediaDeviceManager,
                 mediaDataCombineLatest = mediaDataCombineLatest,
                 mediaDataFilter = mediaDataFilter,
-                activityStarter = activityStarter,
                 smartspaceMediaDataProvider = smartspaceMediaDataProvider,
                 useMediaResumption = true,
                 useQsMediaPlayer = true,
@@ -251,6 +252,7 @@
                 smartspaceManager = smartspaceManager,
                 keyguardUpdateMonitor = keyguardUpdateMonitor,
                 mediaDataLoader = { kosmos.mediaDataLoader },
+                mediaLogger = kosmos.mediaLogger,
             )
         verify(tunerService)
             .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
@@ -2404,6 +2406,45 @@
         assertThat(mediaDataCaptor.value.artwork).isNull()
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
+    fun postDuplicateNotification_doesNotCallListeners() {
+        addNotificationAndLoad()
+        reset(listener)
+        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+
+        testScope.assertRunAllReady(foreground = 0, background = 1)
+        verify(listener, never())
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        verify(kosmos.mediaLogger).logDuplicateMediaNotification(eq(KEY))
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
+    fun postDuplicateNotification_callsListeners() {
+        addNotificationAndLoad()
+        reset(listener)
+        mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+        testScope.assertRunAllReady(foreground = 1, background = 1)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY))
+    }
+
     private fun TestScope.assertRunAllReady(foreground: Int = 0, background: Int = 0) {
         runCurrent()
         if (Flags.mediaLoadMetadataViaMediaDataLoader()) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/resume/MediaResumeListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/resume/MediaResumeListenerTest.kt
index 02d7413..bc29d2a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/resume/MediaResumeListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/resume/MediaResumeListenerTest.kt
@@ -138,6 +138,7 @@
         whenever(mockContext.packageManager).thenReturn(context.packageManager)
         whenever(mockContext.contentResolver).thenReturn(context.contentResolver)
         whenever(mockContext.userId).thenReturn(context.userId)
+        whenever(mockContext.resources).thenReturn(context.resources)
         whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(false)
 
         executor = FakeExecutor(clock)
@@ -210,7 +211,7 @@
     @Test
     fun testOnLoad_checksForResume_noService() {
         // When media data is loaded that has not been checked yet, and does not have a MBS
-        resumeListener.onMediaDataLoaded(KEY, null, data)
+        onMediaDataLoaded(KEY, null, data)
 
         // Then we report back to the manager
         verify(mediaDataManager).setResumeAction(KEY, null)
@@ -223,8 +224,7 @@
         whenever(resumeBrowser.testConnection()).thenAnswer { callbackCaptor.value.onError() }
 
         // When media data is loaded that has not been checked yet, and does not have a MBS
-        resumeListener.onMediaDataLoaded(KEY, null, data)
-        executor.runAllReady()
+        onMediaDataLoaded(KEY, null, data)
 
         // Then we report back to the manager
         verify(mediaDataManager).setResumeAction(eq(KEY), eq(null))
@@ -234,7 +234,7 @@
     fun testOnLoad_localCast_doesNotCheck() {
         // When media data is loaded that has not been checked yet, and is a local cast
         val dataCast = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
-        resumeListener.onMediaDataLoaded(KEY, null, dataCast)
+        onMediaDataLoaded(KEY, null, dataCast, false)
 
         // Then we do not take action
         verify(mediaDataManager, never()).setResumeAction(any(), any())
@@ -244,7 +244,7 @@
     fun testOnload_remoteCast_doesNotCheck() {
         // When media data is loaded that has not been checked yet, and is a remote cast
         val dataRcn = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_REMOTE)
-        resumeListener.onMediaDataLoaded(KEY, null, dataRcn)
+        onMediaDataLoaded(KEY, null, dataRcn, resume = false)
 
         // Then we do not take action
         verify(mediaDataManager, never()).setResumeAction(any(), any())
@@ -257,7 +257,7 @@
 
         // When media data is loaded that has not been checked yet, and is a local cast
         val dataCast = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_LOCAL)
-        resumeListener.onMediaDataLoaded(KEY, null, dataCast)
+        onMediaDataLoaded(KEY, null, dataCast)
 
         // Then we report back to the manager
         verify(mediaDataManager).setResumeAction(KEY, null)
@@ -270,7 +270,7 @@
 
         // When media data is loaded that has not been checked yet, and is a remote cast
         val dataRcn = data.copy(playbackLocation = MediaData.PLAYBACK_CAST_REMOTE)
-        resumeListener.onMediaDataLoaded(KEY, null, dataRcn)
+        onMediaDataLoaded(KEY, null, dataRcn, false)
 
         // Then we do not take action
         verify(mediaDataManager, never()).setResumeAction(any(), any())
@@ -288,10 +288,9 @@
 
         // When media data is loaded that has not been checked yet, and does have a MBS
         val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
-        resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
+        onMediaDataLoaded(KEY, null, dataCopy)
 
         // Then we test whether the service is valid
-        executor.runAllReady()
         verify(mediaDataManager).setResumeAction(eq(KEY), eq(null))
         verify(resumeBrowser).testConnection()
 
@@ -307,7 +306,7 @@
     fun testOnLoad_doesNotCheckAgain() {
         // When a media data is loaded that has been checked already
         var dataCopy = data.copy(hasCheckedForResume = true)
-        resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
+        onMediaDataLoaded(KEY, null, dataCopy, resume = false)
 
         // Then we should not check it again
         verify(resumeBrowser, never()).testConnection()
@@ -320,17 +319,15 @@
         setUpMbsWithValidResolveInfo()
         resumeListener.onMediaDataLoaded(KEY, null, data)
 
-        // We notify the manager to set a null action
-        verify(mediaDataManager).setResumeAction(KEY, null)
-
         // If we then get another update from the app before the first check completes
         assertThat(executor.numPending()).isEqualTo(1)
         var dataWithCheck = data.copy(hasCheckedForResume = true)
         resumeListener.onMediaDataLoaded(KEY, null, dataWithCheck)
 
         // We do not try to start another check
-        assertThat(executor.numPending()).isEqualTo(1)
+        executor.runAllReady()
         verify(mediaDataManager).setResumeAction(KEY, null)
+        verify(resumeBrowserFactory, times(1)).create(any(), any(), anyInt())
     }
 
     @Test
@@ -363,6 +360,7 @@
         resumeListener.userUnlockReceiver.onReceive(context, intent)
 
         // Then we should attempt to find recent media for each saved component
+        executor.runAllReady()
         verify(resumeBrowser, times(3)).findRecentMedia()
 
         // Then since the mock service found media, the manager should be informed
@@ -382,10 +380,9 @@
 
         // When media data is loaded that has not been checked yet, and does have a MBS
         val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
-        resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
+        onMediaDataLoaded(KEY, null, dataCopy)
 
         // Then we test whether the service is valid and set the resume action
-        executor.runAllReady()
         verify(mediaDataManager).setResumeAction(eq(KEY), eq(null))
         verify(resumeBrowser).testConnection()
         verify(mediaDataManager, times(2)).setResumeAction(eq(KEY), capture(actionCaptor))
@@ -455,6 +452,7 @@
         resumeListener.userUnlockReceiver.onReceive(mockContext, intent)
 
         // We add its resume controls
+        executor.runAllReady()
         verify(resumeBrowser).findRecentMedia()
         verify(mediaDataManager)
             .addResumptionControls(anyInt(), any(), any(), any(), any(), any(), eq(PACKAGE_NAME))
@@ -527,7 +525,7 @@
 
         // When media data is loaded that has not been checked yet, and does have a MBS
         val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
-        resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
+        onMediaDataLoaded(KEY, null, dataCopy)
 
         // Then we store the new lastPlayed time
         verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor)))
@@ -546,10 +544,9 @@
     fun testOnMediaDataLoaded_newKeyDifferent_oldMediaBrowserDisconnected() {
         setUpMbsWithValidResolveInfo()
 
-        resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data)
-        executor.runAllReady()
+        onMediaDataLoaded(key = KEY, oldKey = null, data)
 
-        resumeListener.onMediaDataLoaded(key = "newKey", oldKey = KEY, data)
+        onMediaDataLoaded(key = "newKey", oldKey = KEY, data)
 
         verify(resumeBrowser).disconnect()
     }
@@ -561,8 +558,7 @@
         // Set up mocks to return with an error
         whenever(resumeBrowser.testConnection()).thenAnswer { callbackCaptor.value.onError() }
 
-        resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data)
-        executor.runAllReady()
+        onMediaDataLoaded(key = KEY, oldKey = null, data)
 
         // Ensure we disconnect the browser
         verify(resumeBrowser).disconnect()
@@ -579,8 +575,7 @@
             callbackCaptor.value.addTrack(description, component, resumeBrowser)
         }
 
-        resumeListener.onMediaDataLoaded(key = KEY, oldKey = null, data)
-        executor.runAllReady()
+        onMediaDataLoaded(key = KEY, oldKey = null, data)
 
         // Ensure we disconnect the browser
         verify(resumeBrowser).disconnect()
@@ -598,8 +593,7 @@
 
         // Load media data that will require us to get the resume action
         val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false)
-        resumeListener.onMediaDataLoaded(KEY, null, dataCopy)
-        executor.runAllReady()
+        onMediaDataLoaded(KEY, null, dataCopy)
         verify(mediaDataManager, times(2)).setResumeAction(eq(KEY), capture(actionCaptor))
 
         // Set up our factory to return a new browser so we can verify we disconnected the old one
@@ -634,6 +628,7 @@
         // When the first user unlocks and we query their recent media
         userCallbackCaptor.value.onUserChanged(firstUserId, context)
         resumeListener.userUnlockReceiver.onReceive(context, unlockIntent)
+        executor.runAllReady()
         whenever(resumeBrowser.userId).thenReturn(userIdCaptor.value)
         verify(resumeBrowser, times(3)).findRecentMedia()
 
@@ -688,4 +683,16 @@
         whenever(pm.resolveServiceAsUser(any(), anyInt(), anyInt())).thenReturn(resolveInfo)
         whenever(pm.getApplicationLabel(any())).thenReturn(PACKAGE_NAME)
     }
+
+    private fun onMediaDataLoaded(
+        key: String,
+        oldKey: String?,
+        data: MediaData,
+        resume: Boolean = true
+    ) {
+        resumeListener.onMediaDataLoaded(key, oldKey, data)
+        if (resume) {
+            assertThat(executor.runAllReady()).isEqualTo(1)
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
index d9faa30..70af5e7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
@@ -31,17 +31,18 @@
 import androidx.compose.ui.test.onNodeWithContentDescription
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.text.AnnotatedString
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.common.shared.model.Text
 import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.shared.model.SizedTileImpl
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -199,10 +200,11 @@
                             android.R.drawable.star_on,
                             ContentDescription.Loaded(tileSpec)
                         ),
-                    label = Text.Loaded(tileSpec),
+                    label = AnnotatedString(tileSpec),
                     appName = null,
                     isCurrent = true,
                     availableEditActions = emptySet(),
+                    category = TileCategory.UNKNOWN,
                 ),
                 getWidth(tileSpec),
             )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
index e6ec07e..9f84e34 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt
@@ -258,7 +258,7 @@
     companion object {
         const val WIFI_SSID = "test ssid"
         val ACTIVE_WIFI =
-            WifiNetworkModel.Active(
+            WifiNetworkModel.Active.of(
                 isValidated = true,
                 level = 4,
                 ssid = WIFI_SSID,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt
index b6e23c1..715e3b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt
@@ -85,7 +85,7 @@
                 underTest.dataConnectionState.onEach { latestConnState = it }.launchIn(this)
             val netJob = underTest.resolvedNetworkType.onEach { latestNetType = it }.launchIn(this)
 
-            wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive())
 
             assertThat(latestConnState).isEqualTo(DataConnectionState.Disconnected)
             assertThat(latestNetType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType)
@@ -104,7 +104,7 @@
                 underTest.dataConnectionState.onEach { latestConnState = it }.launchIn(this)
             val netJob = underTest.resolvedNetworkType.onEach { latestNetType = it }.launchIn(this)
 
-            wifiRepository.setWifiNetwork(WifiNetworkModel.Active(level = 1))
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Active.of(level = 1))
 
             assertThat(latestConnState).isEqualTo(DataConnectionState.Disconnected)
             assertThat(latestNetType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType)
@@ -123,7 +123,7 @@
             wifiRepository.setIsWifiDefault(true)
 
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.CarrierMerged(
+                WifiNetworkModel.CarrierMerged.of(
                     subscriptionId = SUB_ID,
                     level = 3,
                 )
@@ -143,7 +143,7 @@
             wifiRepository.setIsWifiEnabled(true)
             wifiRepository.setIsWifiDefault(true)
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.CarrierMerged(
+                WifiNetworkModel.CarrierMerged.of(
                     subscriptionId = SUB_ID,
                     level = 3,
                 )
@@ -180,7 +180,7 @@
             val typeJob = underTest.resolvedNetworkType.onEach { latestType = it }.launchIn(this)
 
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.CarrierMerged(
+                WifiNetworkModel.CarrierMerged.of(
                     subscriptionId = SUB_ID + 10,
                     level = 3,
                 )
@@ -201,7 +201,7 @@
             val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this)
 
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.CarrierMerged(
+                WifiNetworkModel.CarrierMerged.of(
                     subscriptionId = SUB_ID,
                     level = 3,
                 )
@@ -221,7 +221,7 @@
             val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this)
 
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.CarrierMerged(
+                WifiNetworkModel.CarrierMerged.of(
                     subscriptionId = SUB_ID,
                     level = 3,
                 )
@@ -240,7 +240,7 @@
             val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this)
 
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.CarrierMerged(
+                WifiNetworkModel.CarrierMerged.of(
                     subscriptionId = SUB_ID,
                     level = 1,
                     numberOfLevels = 6,
@@ -303,7 +303,7 @@
 
             whenever(telephonyManager.simOperatorName).thenReturn("New SIM name")
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.CarrierMerged(
+                WifiNetworkModel.CarrierMerged.of(
                     subscriptionId = SUB_ID,
                     level = 3,
                 )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
index a03980a..fd23655 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
@@ -488,7 +488,7 @@
 
             // WHEN we set up carrier merged info
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.CarrierMerged(
+                WifiNetworkModel.CarrierMerged.of(
                     SUB_ID,
                     level = 3,
                 )
@@ -499,7 +499,7 @@
 
             // WHEN we update the info
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.CarrierMerged(
+                WifiNetworkModel.CarrierMerged.of(
                     SUB_ID,
                     level = 1,
                 )
@@ -538,7 +538,7 @@
 
             // WHEN isCarrierMerged is set to true
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.CarrierMerged(
+                WifiNetworkModel.CarrierMerged.of(
                     SUB_ID,
                     level = 3,
                 )
@@ -550,7 +550,7 @@
 
             // WHEN the carrier merge network is updated
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.CarrierMerged(
+                WifiNetworkModel.CarrierMerged.of(
                     SUB_ID,
                     level = 4,
                 )
@@ -602,7 +602,7 @@
 
             // THEN updates to the carrier merged level aren't logged
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.CarrierMerged(
+                WifiNetworkModel.CarrierMerged.of(
                     SUB_ID,
                     level = 4,
                 )
@@ -610,7 +610,7 @@
             assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}4")
 
             wifiRepository.setWifiNetwork(
-                WifiNetworkModel.CarrierMerged(
+                WifiNetworkModel.CarrierMerged.of(
                     SUB_ID,
                     level = 3,
                 )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
index a1cb29b..c0a206a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
@@ -574,7 +574,7 @@
             val latest by collectLastValue(underTest.isWifiActive)
 
             // WHEN wifi is active
-            wifiRepository.setWifiNetwork(WifiNetworkModel.Active(level = 1))
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Active.of(level = 1))
 
             // THEN the interactor returns true due to the wifi network being active
             assertThat(latest).isTrue()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
index c1abf98..e7e4969 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt
@@ -375,7 +375,7 @@
             repo.isSatelliteProvisioned.value = true
 
             // GIVEN wifi network is active
-            wifiRepository.setWifiNetwork(WifiNetworkModel.Active(level = 1))
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Active.of(level = 1))
 
             // THEN icon is null because the device is connected to wifi
             assertThat(latest).isNull()
@@ -573,7 +573,7 @@
             repo.isSatelliteProvisioned.value = true
 
             // GIVEN wifi network is active
-            wifiRepository.setWifiNetwork(WifiNetworkModel.Active(level = 1))
+            wifiRepository.setWifiNetwork(WifiNetworkModel.Active.of(level = 1))
 
             // THEN carrier text is null because the device is connected to wifi
             assertThat(latest).isNull()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModelTest.kt
index fed3317..975e2ca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModelTest.kt
@@ -154,7 +154,7 @@
             val latest by collectLastValue(underTest.tileModel)
 
             val networkModel =
-                WifiNetworkModel.Active(
+                WifiNetworkModel.Active.of(
                     level = 4,
                     ssid = "test ssid",
                 )
@@ -183,7 +183,7 @@
             val latest by collectLastValue(underTest.tileModel)
 
             val networkModel =
-                WifiNetworkModel.Active(
+                WifiNetworkModel.Active.of(
                     level = 4,
                     ssid = "test ssid",
                     hotspotDeviceType = WifiNetworkModel.HotspotDeviceType.NONE,
@@ -295,7 +295,7 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.tileModel)
 
-            val networkModel = WifiNetworkModel.Inactive
+            val networkModel = WifiNetworkModel.Inactive()
 
             connectivityRepository.setWifiConnected(validated = false)
             wifiRepository.setIsWifiDefault(true)
@@ -310,7 +310,7 @@
         testScope.runTest {
             val latest by collectLastValue(underTest.tileModel)
 
-            val networkModel = WifiNetworkModel.Inactive
+            val networkModel = WifiNetworkModel.Inactive()
 
             connectivityRepository.setWifiConnected(validated = false)
             wifiRepository.setIsWifiDefault(true)
@@ -390,7 +390,7 @@
 
     private fun setWifiNetworkWithHotspot(hotspot: WifiNetworkModel.HotspotDeviceType) {
         val networkModel =
-            WifiNetworkModel.Active(
+            WifiNetworkModel.Active.of(
                 level = 4,
                 ssid = "test ssid",
                 hotspotDeviceType = hotspot,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
index 46f34e8..fd4b77d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt
@@ -295,7 +295,10 @@
             whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
             getCallback().onWifiEntriesChanged()
 
-            assertThat(latest).isEqualTo(WifiNetworkModel.Inactive)
+            assertThat(latest).isInstanceOf(WifiNetworkModel.Inactive::class.java)
+            val inactiveReason = (latest as WifiNetworkModel.Inactive).inactiveReason
+            assertThat(inactiveReason).contains("level")
+            assertThat(inactiveReason).contains("$WIFI_LEVEL_UNREACHABLE")
         }
 
     @Test
@@ -311,7 +314,10 @@
             whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
             getCallback().onWifiEntriesChanged()
 
-            assertThat(latest).isEqualTo(WifiNetworkModel.Inactive)
+            assertThat(latest).isInstanceOf(WifiNetworkModel.Inactive::class.java)
+            val inactiveReason = (latest as WifiNetworkModel.Inactive).inactiveReason
+            assertThat(inactiveReason).contains("level")
+            assertThat(inactiveReason).contains("${WIFI_LEVEL_MAX + 1}")
         }
 
     @Test
@@ -327,7 +333,10 @@
             whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
             getCallback().onWifiEntriesChanged()
 
-            assertThat(latest).isEqualTo(WifiNetworkModel.Inactive)
+            assertThat(latest).isInstanceOf(WifiNetworkModel.Inactive::class.java)
+            val inactiveReason = (latest as WifiNetworkModel.Inactive).inactiveReason
+            assertThat(inactiveReason).contains("level")
+            assertThat(inactiveReason).contains("${WIFI_LEVEL_MIN - 1}")
         }
 
     @Test
@@ -530,6 +539,25 @@
         }
 
     @Test
+    fun wifiNetwork_carrierMergedButInvalidLevel_flowHasInvalid() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.wifiNetwork)
+
+            val mergedEntry =
+                mock<MergedCarrierEntry>().apply {
+                    whenever(this.isPrimaryNetwork).thenReturn(true)
+                    whenever(this.subscriptionId).thenReturn(3)
+                    whenever(this.isDefaultNetwork).thenReturn(true)
+                    whenever(this.level).thenReturn(WIFI_LEVEL_UNREACHABLE)
+                }
+            whenever(wifiPickerTracker.mergedCarrierEntry).thenReturn(mergedEntry)
+
+            getCallback().onWifiEntriesChanged()
+
+            assertThat(latest).isInstanceOf(WifiNetworkModel.Invalid::class.java)
+        }
+
+    @Test
     fun wifiNetwork_notValidated_networkNotValidated() =
         testScope.runTest {
             val latest by collectLastValue(underTest.wifiNetwork)
@@ -571,7 +599,7 @@
             whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
             getCallback().onWifiEntriesChanged()
 
-            assertThat(latest).isEqualTo(WifiNetworkModel.Inactive)
+            assertThat(latest).isEqualTo(WifiNetworkModel.Inactive())
         }
 
     @Test
@@ -587,7 +615,7 @@
             whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null)
             getCallback().onWifiEntriesChanged()
 
-            assertThat(latest).isEqualTo(WifiNetworkModel.Inactive)
+            assertThat(latest).isEqualTo(WifiNetworkModel.Inactive())
         }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModelTest.kt
index 92860ef..1495519 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiNetworkModelTest.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.log.table.TableRowLogger
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel.Active.Companion.MAX_VALID_LEVEL
 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel.Companion.MIN_VALID_LEVEL
+import com.android.wifitrackerlib.WifiEntry.WIFI_LEVEL_UNREACHABLE
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -32,59 +33,109 @@
 @RunWith(AndroidJUnit4::class)
 class WifiNetworkModelTest : SysuiTestCase() {
     @Test
-    fun active_levelsInValidRange_noException() {
+    fun active_levelsInValidRange_createsActive() {
         (MIN_VALID_LEVEL..MAX_VALID_LEVEL).forEach { level ->
-            WifiNetworkModel.Active(level = level)
-            // No assert, just need no crash
+            val result = WifiNetworkModel.Active.of(level = level)
+            assertThat(result).isInstanceOf(WifiNetworkModel.Active::class.java)
         }
     }
 
-    @Test(expected = IllegalArgumentException::class)
-    fun active_levelNegative_exceptionThrown() {
-        WifiNetworkModel.Active(level = MIN_VALID_LEVEL - 1)
+    fun active_levelTooLow_returnsInactive() {
+        val result = WifiNetworkModel.Active.of(level = MIN_VALID_LEVEL - 1)
+        assertThat(result).isInstanceOf(WifiNetworkModel.Inactive::class.java)
     }
 
     @Test(expected = IllegalArgumentException::class)
-    fun active_levelTooHigh_exceptionThrown() {
-        WifiNetworkModel.Active(level = MAX_VALID_LEVEL + 1)
+    fun active_levelTooLow_createdByCopy_exceptionThrown() {
+        val starting = WifiNetworkModel.Active.of(level = MIN_VALID_LEVEL)
+
+        (starting as WifiNetworkModel.Active).copy(level = MIN_VALID_LEVEL - 1)
+    }
+
+    fun active_levelTooHigh_returnsInactive() {
+        val result = WifiNetworkModel.Active.of(level = MAX_VALID_LEVEL + 1)
+
+        assertThat(result).isInstanceOf(WifiNetworkModel.Inactive::class.java)
     }
 
     @Test(expected = IllegalArgumentException::class)
-    fun carrierMerged_invalidSubId_exceptionThrown() {
-        WifiNetworkModel.CarrierMerged(INVALID_SUBSCRIPTION_ID, 1)
+    fun active_levelTooHigh_createdByCopy_exceptionThrown() {
+        val starting = WifiNetworkModel.Active.of(level = MAX_VALID_LEVEL)
+
+        (starting as WifiNetworkModel.Active).copy(level = MAX_VALID_LEVEL + 1)
+    }
+
+    fun active_levelUnreachable_returnsInactive() {
+        val result = WifiNetworkModel.Active.of(level = WIFI_LEVEL_UNREACHABLE)
+
+        assertThat(result).isInstanceOf(WifiNetworkModel.Inactive::class.java)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun active_levelUnreachable_createdByCopy_exceptionThrown() {
+        val starting = WifiNetworkModel.Active.of(level = MAX_VALID_LEVEL)
+
+        (starting as WifiNetworkModel.Active).copy(level = WIFI_LEVEL_UNREACHABLE)
+    }
+
+    fun carrierMerged_invalidSubId_returnsInvalid() {
+        val result = WifiNetworkModel.CarrierMerged.of(INVALID_SUBSCRIPTION_ID, level = 1)
+
+        assertThat(result).isInstanceOf(WifiNetworkModel.Invalid::class.java)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun carrierMerged_invalidSubId_createdByCopy_exceptionThrown() {
+        val starting = WifiNetworkModel.CarrierMerged.of(subscriptionId = 1, level = 1)
+
+        (starting as WifiNetworkModel.CarrierMerged).copy(subscriptionId = INVALID_SUBSCRIPTION_ID)
+    }
+
+    fun carrierMerged_levelUnreachable_returnsInvalid() {
+        val result =
+            WifiNetworkModel.CarrierMerged.of(subscriptionId = 1, level = WIFI_LEVEL_UNREACHABLE)
+
+        assertThat(result).isInstanceOf(WifiNetworkModel.Invalid::class.java)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun carrierMerged_levelUnreachable_createdByCopy_exceptionThrown() {
+        val starting = WifiNetworkModel.CarrierMerged.of(subscriptionId = 1, level = 1)
+
+        (starting as WifiNetworkModel.CarrierMerged).copy(level = WIFI_LEVEL_UNREACHABLE)
     }
 
     @Test
     fun active_hasValidSsid_nullSsid_false() {
         val network =
-            WifiNetworkModel.Active(
+            WifiNetworkModel.Active.of(
                 level = MAX_VALID_LEVEL,
                 ssid = null,
             )
 
-        assertThat(network.hasValidSsid()).isFalse()
+        assertThat((network as WifiNetworkModel.Active).hasValidSsid()).isFalse()
     }
 
     @Test
     fun active_hasValidSsid_unknownSsid_false() {
         val network =
-            WifiNetworkModel.Active(
+            WifiNetworkModel.Active.of(
                 level = MAX_VALID_LEVEL,
                 ssid = UNKNOWN_SSID,
             )
 
-        assertThat(network.hasValidSsid()).isFalse()
+        assertThat((network as WifiNetworkModel.Active).hasValidSsid()).isFalse()
     }
 
     @Test
     fun active_hasValidSsid_validSsid_true() {
         val network =
-            WifiNetworkModel.Active(
+            WifiNetworkModel.Active.of(
                 level = MAX_VALID_LEVEL,
                 ssid = "FakeSsid",
             )
 
-        assertThat(network.hasValidSsid()).isTrue()
+        assertThat((network as WifiNetworkModel.Active).hasValidSsid()).isTrue()
     }
 
     // Non-exhaustive logDiffs test -- just want to make sure the logging logic isn't totally broken
@@ -93,14 +144,15 @@
     fun logDiffs_carrierMergedToInactive_resetsAllFields() {
         val logger = TestLogger()
         val prevVal =
-            WifiNetworkModel.CarrierMerged(
+            WifiNetworkModel.CarrierMerged.of(
                 subscriptionId = 3,
                 level = 1,
             )
 
-        WifiNetworkModel.Inactive.logDiffs(prevVal, logger)
+        WifiNetworkModel.Inactive(inactiveReason = "TestReason").logDiffs(prevVal, logger)
 
-        assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_INACTIVE))
+        assertThat(logger.changes)
+            .contains(Pair(COL_NETWORK_TYPE, "$TYPE_INACTIVE[reason=TestReason]"))
         assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false"))
         assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString()))
         assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
@@ -110,12 +162,12 @@
     fun logDiffs_inactiveToCarrierMerged_logsAllFields() {
         val logger = TestLogger()
         val carrierMerged =
-            WifiNetworkModel.CarrierMerged(
+            WifiNetworkModel.CarrierMerged.of(
                 subscriptionId = 3,
                 level = 2,
             )
 
-        carrierMerged.logDiffs(prevVal = WifiNetworkModel.Inactive, logger)
+        carrierMerged.logDiffs(prevVal = WifiNetworkModel.Inactive(), logger)
 
         assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_CARRIER_MERGED))
         assertThat(logger.changes).contains(Pair(COL_SUB_ID, "3"))
@@ -128,14 +180,14 @@
     fun logDiffs_inactiveToActive_logsAllActiveFields() {
         val logger = TestLogger()
         val activeNetwork =
-            WifiNetworkModel.Active(
+            WifiNetworkModel.Active.of(
                 isValidated = true,
                 level = 3,
                 ssid = "Test SSID",
                 hotspotDeviceType = WifiNetworkModel.HotspotDeviceType.LAPTOP,
             )
 
-        activeNetwork.logDiffs(prevVal = WifiNetworkModel.Inactive, logger)
+        activeNetwork.logDiffs(prevVal = WifiNetworkModel.Inactive(), logger)
 
         assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_ACTIVE))
         assertThat(logger.changes).contains(Pair(COL_VALIDATED, "true"))
@@ -148,11 +200,13 @@
     fun logDiffs_activeToInactive_resetsAllActiveFields() {
         val logger = TestLogger()
         val activeNetwork =
-            WifiNetworkModel.Active(isValidated = true, level = 3, ssid = "Test SSID")
+            WifiNetworkModel.Active.of(isValidated = true, level = 3, ssid = "Test SSID")
 
-        WifiNetworkModel.Inactive.logDiffs(prevVal = activeNetwork, logger)
+        WifiNetworkModel.Inactive(inactiveReason = "TestReason")
+            .logDiffs(prevVal = activeNetwork, logger)
 
-        assertThat(logger.changes).contains(Pair(COL_NETWORK_TYPE, TYPE_INACTIVE))
+        assertThat(logger.changes)
+            .contains(Pair(COL_NETWORK_TYPE, "$TYPE_INACTIVE[reason=TestReason]"))
         assertThat(logger.changes).contains(Pair(COL_VALIDATED, "false"))
         assertThat(logger.changes).contains(Pair(COL_LEVEL, LEVEL_DEFAULT.toString()))
         assertThat(logger.changes).contains(Pair(COL_SSID, "null"))
@@ -163,14 +217,14 @@
     fun logDiffs_carrierMergedToActive_logsAllActiveFields() {
         val logger = TestLogger()
         val activeNetwork =
-            WifiNetworkModel.Active(
+            WifiNetworkModel.Active.of(
                 isValidated = true,
                 level = 3,
                 ssid = "Test SSID",
                 hotspotDeviceType = WifiNetworkModel.HotspotDeviceType.AUTO,
             )
         val prevVal =
-            WifiNetworkModel.CarrierMerged(
+            WifiNetworkModel.CarrierMerged.of(
                 subscriptionId = 3,
                 level = 1,
             )
@@ -188,9 +242,9 @@
     fun logDiffs_activeToCarrierMerged_logsAllFields() {
         val logger = TestLogger()
         val activeNetwork =
-            WifiNetworkModel.Active(isValidated = true, level = 3, ssid = "Test SSID")
+            WifiNetworkModel.Active.of(isValidated = true, level = 3, ssid = "Test SSID")
         val carrierMerged =
-            WifiNetworkModel.CarrierMerged(
+            WifiNetworkModel.CarrierMerged.of(
                 subscriptionId = 3,
                 level = 2,
             )
@@ -208,9 +262,9 @@
     fun logDiffs_activeChangesLevel_onlyLevelLogged() {
         val logger = TestLogger()
         val prevActiveNetwork =
-            WifiNetworkModel.Active(isValidated = true, level = 3, ssid = "Test SSID")
+            WifiNetworkModel.Active.of(isValidated = true, level = 3, ssid = "Test SSID")
         val newActiveNetwork =
-            WifiNetworkModel.Active(isValidated = true, level = 2, ssid = "Test SSID")
+            WifiNetworkModel.Active.of(isValidated = true, level = 2, ssid = "Test SSID")
 
         newActiveNetwork.logDiffs(prevActiveNetwork, logger)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
index ff398f9..37c7a48 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
@@ -195,7 +195,7 @@
     @Test
     fun isIconVisible_notEnabled_outputsFalse() {
         wifiRepository.setIsWifiEnabled(false)
-        wifiRepository.setWifiNetwork(WifiNetworkModel.Active(isValidated = true, level = 2))
+        wifiRepository.setWifiNetwork(WifiNetworkModel.Active.of(isValidated = true, level = 2))
 
         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
@@ -210,7 +210,7 @@
     @Test
     fun isIconVisible_enabled_outputsTrue() {
         wifiRepository.setIsWifiEnabled(true)
-        wifiRepository.setWifiNetwork(WifiNetworkModel.Active(isValidated = true, level = 2))
+        wifiRepository.setWifiNetwork(WifiNetworkModel.Active.of(isValidated = true, level = 2))
 
         val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
index 82acb40..96a0194 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt
@@ -206,51 +206,51 @@
                 // Enabled = false => no networks shown
                 TestCase(
                     enabled = false,
-                    network = WifiNetworkModel.CarrierMerged(subscriptionId = 1, level = 1),
+                    network = WifiNetworkModel.CarrierMerged.of(subscriptionId = 1, level = 1),
                     expected = null,
                 ),
                 TestCase(
                     enabled = false,
-                    network = WifiNetworkModel.Inactive,
+                    network = WifiNetworkModel.Inactive(),
                     expected = null,
                 ),
                 TestCase(
                     enabled = false,
-                    network = WifiNetworkModel.Active(isValidated = false, level = 1),
+                    network = WifiNetworkModel.Active.of(isValidated = false, level = 1),
                     expected = null,
                 ),
                 TestCase(
                     enabled = false,
-                    network = WifiNetworkModel.Active(isValidated = true, level = 3),
+                    network = WifiNetworkModel.Active.of(isValidated = true, level = 3),
                     expected = null,
                 ),
 
                 // forceHidden = true => no networks shown
                 TestCase(
                     forceHidden = true,
-                    network = WifiNetworkModel.CarrierMerged(subscriptionId = 1, level = 1),
+                    network = WifiNetworkModel.CarrierMerged.of(subscriptionId = 1, level = 1),
                     expected = null,
                 ),
                 TestCase(
                     forceHidden = true,
-                    network = WifiNetworkModel.Inactive,
+                    network = WifiNetworkModel.Inactive(),
                     expected = null,
                 ),
                 TestCase(
                     enabled = false,
-                    network = WifiNetworkModel.Active(isValidated = false, level = 2),
+                    network = WifiNetworkModel.Active.of(isValidated = false, level = 2),
                     expected = null,
                 ),
                 TestCase(
                     forceHidden = true,
-                    network = WifiNetworkModel.Active(isValidated = true, level = 1),
+                    network = WifiNetworkModel.Active.of(isValidated = true, level = 1),
                     expected = null,
                 ),
 
                 // alwaysShowIconWhenEnabled = true => all Inactive and Active networks shown
                 TestCase(
                     alwaysShowIconWhenEnabled = true,
-                    network = WifiNetworkModel.Inactive,
+                    network = WifiNetworkModel.Inactive(),
                     expected =
                         Expected(
                             iconResource = WIFI_NO_NETWORK,
@@ -263,7 +263,7 @@
                 ),
                 TestCase(
                     alwaysShowIconWhenEnabled = true,
-                    network = WifiNetworkModel.Active(isValidated = false, level = 4),
+                    network = WifiNetworkModel.Active.of(isValidated = false, level = 4),
                     expected =
                         Expected(
                             iconResource = WIFI_NO_INTERNET_ICONS[4],
@@ -276,7 +276,7 @@
                 ),
                 TestCase(
                     alwaysShowIconWhenEnabled = true,
-                    network = WifiNetworkModel.Active(isValidated = true, level = 2),
+                    network = WifiNetworkModel.Active.of(isValidated = true, level = 2),
                     expected =
                         Expected(
                             iconResource = WIFI_FULL_ICONS[2],
@@ -290,7 +290,7 @@
                 // hasDataCapabilities = false => all Inactive and Active networks shown
                 TestCase(
                     hasDataCapabilities = false,
-                    network = WifiNetworkModel.Inactive,
+                    network = WifiNetworkModel.Inactive(),
                     expected =
                         Expected(
                             iconResource = WIFI_NO_NETWORK,
@@ -303,7 +303,7 @@
                 ),
                 TestCase(
                     hasDataCapabilities = false,
-                    network = WifiNetworkModel.Active(isValidated = false, level = 2),
+                    network = WifiNetworkModel.Active.of(isValidated = false, level = 2),
                     expected =
                         Expected(
                             iconResource = WIFI_NO_INTERNET_ICONS[2],
@@ -316,7 +316,7 @@
                 ),
                 TestCase(
                     hasDataCapabilities = false,
-                    network = WifiNetworkModel.Active(isValidated = true, level = 0),
+                    network = WifiNetworkModel.Active.of(isValidated = true, level = 0),
                     expected =
                         Expected(
                             iconResource = WIFI_FULL_ICONS[0],
@@ -330,7 +330,7 @@
                 // isDefault = true => all Inactive and Active networks shown
                 TestCase(
                     isDefault = true,
-                    network = WifiNetworkModel.Inactive,
+                    network = WifiNetworkModel.Inactive(),
                     expected =
                         Expected(
                             iconResource = WIFI_NO_NETWORK,
@@ -343,7 +343,7 @@
                 ),
                 TestCase(
                     isDefault = true,
-                    network = WifiNetworkModel.Active(isValidated = false, level = 3),
+                    network = WifiNetworkModel.Active.of(isValidated = false, level = 3),
                     expected =
                         Expected(
                             iconResource = WIFI_NO_INTERNET_ICONS[3],
@@ -356,7 +356,7 @@
                 ),
                 TestCase(
                     isDefault = true,
-                    network = WifiNetworkModel.Active(isValidated = true, level = 1),
+                    network = WifiNetworkModel.Active.of(isValidated = true, level = 1),
                     expected =
                         Expected(
                             iconResource = WIFI_FULL_ICONS[1],
@@ -372,14 +372,14 @@
                     enabled = true,
                     isDefault = true,
                     forceHidden = false,
-                    network = WifiNetworkModel.CarrierMerged(subscriptionId = 1, level = 1),
+                    network = WifiNetworkModel.CarrierMerged.of(subscriptionId = 1, level = 1),
                     expected = null,
                 ),
 
                 // isDefault = false => no networks shown
                 TestCase(
                     isDefault = false,
-                    network = WifiNetworkModel.Inactive,
+                    network = WifiNetworkModel.Inactive(),
                     expected = null,
                 ),
                 TestCase(
@@ -389,7 +389,7 @@
                 ),
                 TestCase(
                     isDefault = false,
-                    network = WifiNetworkModel.Active(isValidated = false, level = 3),
+                    network = WifiNetworkModel.Active.of(isValidated = false, level = 3),
                     expected = null,
                 ),
 
@@ -397,7 +397,7 @@
                 // because wifi isn't the default connection (b/272509965).
                 TestCase(
                     isDefault = false,
-                    network = WifiNetworkModel.Active(isValidated = true, level = 4),
+                    network = WifiNetworkModel.Active.of(isValidated = true, level = 4),
                     expected = null,
                 ),
             )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
index 811c653..251a6b1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorKosmos.kt
@@ -50,6 +50,13 @@
     Kosmos.Fixture {
         KeyboardTouchpadEduStatsInteractorImpl(
             backgroundScope = testScope.backgroundScope,
-            contextualEducationInteractor = contextualEducationInteractor
+            contextualEducationInteractor = contextualEducationInteractor,
+            inputDeviceRepository =
+                UserInputDeviceRepository(
+                    testDispatcher,
+                    keyboardRepository,
+                    touchpadRepository,
+                    userRepository
+                )
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.kt
index b03542c..33227a4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.qs.panels.ui.viewmodel
 
+import android.content.applicationContext
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.qs.panels.domain.interactor.editTilesListInteractor
@@ -33,6 +35,8 @@
             currentTilesInteractor,
             tilesAvailabilityInteractor,
             minimumTilesInteractor,
+            configurationInteractor,
+            applicationContext,
             infiniteGridLayout,
             applicationCoroutineScope,
             gridLayoutTypeInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt
index dceb8bf..f66125a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.qs.instanceIdSequenceFake
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
 import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
 import com.android.systemui.qs.tiles.viewmodel.QSTileState
@@ -47,6 +48,7 @@
                         tileSpec,
                         QSTileUIConfig.Empty,
                         instanceIdSequenceFake.newInstanceId(),
+                        category = TileCategory.PROVIDED_BY_APP,
                     )
                 object : QSTileViewModel {
                     override val state: StateFlow<QSTileState?> =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigTestBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigTestBuilder.kt
index 2a0ee88..73d9b32 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigTestBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/viewmodel/QSTileConfigTestBuilder.kt
@@ -18,6 +18,7 @@
 
 import com.android.internal.logging.InstanceId
 import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
 
 object QSTileConfigTestBuilder {
 
@@ -30,12 +31,14 @@
         var instanceId: InstanceId = InstanceId.fakeInstanceId(0)
         var metricsSpec: String = tileSpec.spec
         var policy: QSTilePolicy = QSTilePolicy.NoRestrictions
+        var category: TileCategory = TileCategory.UNKNOWN
 
         fun build() =
             QSTileConfig(
                 tileSpec,
                 uiConfig,
                 instanceId,
+                category,
                 metricsSpec,
                 policy,
             )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
index 709be5e..7ca90ea 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
@@ -32,7 +32,7 @@
     override val isWifiDefault: StateFlow<Boolean> = _isWifiDefault
 
     private val _wifiNetwork: MutableStateFlow<WifiNetworkModel> =
-        MutableStateFlow(WifiNetworkModel.Inactive)
+        MutableStateFlow(WifiNetworkModel.Inactive())
     override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork
 
     override val secondaryNetworks = MutableStateFlow<List<WifiNetworkModel>>(emptyList())
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt
index 888351f..ba6ffd7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt
@@ -33,11 +33,7 @@
 
 class FakeAudioRepository : AudioRepository {
 
-    private val unMutableStreams =
-        setOf(
-            AudioManager.STREAM_VOICE_CALL,
-            AudioManager.STREAM_ALARM,
-        )
+    private val unMutableStreams = setOf(AudioManager.STREAM_VOICE_CALL, AudioManager.STREAM_ALARM)
 
     private val mutableMode = MutableStateFlow(AudioManager.MODE_NORMAL)
     override val mode: StateFlow<Int> = mutableMode.asStateFlow()
@@ -126,7 +122,7 @@
         lastAudibleVolumes[audioStream] = volume
     }
 
-    override suspend fun setRingerMode(audioStream: AudioStream, mode: RingerMode) {
+    override suspend fun setRingerModeInternal(audioStream: AudioStream, mode: RingerMode) {
         mutableRingerMode.value = mode
     }
 
diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
index 0947238..39aa27a 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
@@ -27,6 +27,7 @@
 import android.app.appsearch.GetByDocumentIdRequest;
 import android.app.appsearch.GetSchemaResponse;
 import android.app.appsearch.PutDocumentsRequest;
+import android.app.appsearch.RemoveByDocumentIdRequest;
 import android.app.appsearch.SearchResult;
 import android.app.appsearch.SearchResults;
 import android.app.appsearch.SearchSpec;
@@ -146,6 +147,22 @@
                         });
     }
 
+    /** Removes documents from the AppSearchSession database. */
+    public AndroidFuture<AppSearchBatchResult<String, Void>> remove(
+            @NonNull RemoveByDocumentIdRequest removeRequest) {
+        return getSessionAsync()
+                .thenCompose(
+                        session -> {
+                            AndroidFuture<AppSearchBatchResult<String, Void>>
+                                    settableBatchResultFuture = new AndroidFuture<>();
+                            session.remove(
+                                    removeRequest,
+                                    mExecutor,
+                                    new BatchResultCallbackAdapter<>(settableBatchResultFuture));
+                            return settableBatchResultFuture;
+                        });
+    }
+
     /**
      * Retrieves documents from the open AppSearchSession that match a given query string and type
      * of search provided.
@@ -200,9 +217,7 @@
         Objects.requireNonNull(namespace);
 
         GetByDocumentIdRequest request =
-                new GetByDocumentIdRequest.Builder(namespace)
-                        .addIds(documentId)
-                        .build();
+                new GetByDocumentIdRequest.Builder(namespace).addIds(documentId).build();
         return getSessionAsync()
                 .thenCompose(
                         session -> {
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
index be5770b..35fa47a 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.WorkerThread;
+import android.app.appsearch.PropertyPath;
 import android.app.appsearch.SearchResult;
 import android.app.appsearch.SearchSpec;
 import android.util.ArrayMap;
@@ -110,26 +111,29 @@
     }
 
     /**
-     * This method returns a map of package names to a set of function ids.
+     * This method returns a map of package names to a set of function ids from the AppFunction
+     * metadata.
      *
-     * @param queryExpression The query expression to use when searching for AppFunction metadata.
-     * @param metadataSearchSpec The search spec to use when searching for AppFunction metadata.
-     * @return A map of package names to a set of function ids.
-     * @throws ExecutionException If the future search results fail to execute.
-     * @throws InterruptedException If the future search results are interrupted.
+     * @param schemaType The name space of the AppFunction metadata.
+     * @return A map of package names to a set of function ids from the AppFunction metadata.
      */
     @NonNull
     @VisibleForTesting
     @WorkerThread
     ArrayMap<String, ArraySet<String>> getPackageToFunctionIdMap(
-            @NonNull String queryExpression,
-            @NonNull SearchSpec metadataSearchSpec,
+            @NonNull String schemaType,
             @NonNull String propertyFunctionId,
             @NonNull String propertyPackageName)
             throws ExecutionException, InterruptedException {
         ArrayMap<String, ArraySet<String>> packageToFunctionIds = new ArrayMap<>();
+
         FutureSearchResults futureSearchResults =
-                mFutureAppSearchSession.search(queryExpression, metadataSearchSpec).get();
+                mFutureAppSearchSession
+                        .search(
+                                "",
+                                buildMetadataSearchSpec(
+                                        schemaType, propertyFunctionId, propertyPackageName))
+                        .get();
         List<SearchResult> searchResultsList = futureSearchResults.getNextPage().get();
         // TODO(b/357551503): This could be expensive if we have more functions
         while (!searchResultsList.isEmpty()) {
@@ -146,4 +150,27 @@
         }
         return packageToFunctionIds;
     }
+
+    /**
+     * This method returns a {@link SearchSpec} for searching the AppFunction metadata.
+     *
+     * @param schemaType The schema type of the AppFunction metadata.
+     * @param propertyFunctionId The property name of the function id in the AppFunction metadata.
+     * @param propertyPackageName The property name of the package name in the AppFunction metadata.
+     * @return A {@link SearchSpec} for searching the AppFunction metadata.
+     */
+    @NonNull
+    private static SearchSpec buildMetadataSearchSpec(
+            @NonNull String schemaType,
+            @NonNull String propertyFunctionId,
+            @NonNull String propertyPackageName) {
+        return new SearchSpec.Builder()
+                .addFilterSchemas(schemaType)
+                .addProjectionPaths(
+                        schemaType,
+                        List.of(
+                                new PropertyPath(propertyFunctionId),
+                                new PropertyPath(propertyPackageName)))
+                .build();
+    }
 }
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 99c3eca..439bca0 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -453,7 +453,9 @@
       proto.write(StorageRequestMessage.FlagOverrideMessage.PACKAGE_NAME, packageName);
       proto.write(StorageRequestMessage.FlagOverrideMessage.FLAG_NAME, flagName);
       proto.write(StorageRequestMessage.FlagOverrideMessage.FLAG_VALUE, flagValue);
-      proto.write(StorageRequestMessage.FlagOverrideMessage.IS_LOCAL, isLocal);
+      proto.write(StorageRequestMessage.FlagOverrideMessage.OVERRIDE_TYPE, isLocal
+                ? StorageRequestMessage.LOCAL_ON_REBOOT
+                : StorageRequestMessage.SERVER_ON_REBOOT);
       proto.end(msgToken);
       proto.end(msgsToken);
     }
diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java
index 2a16872..dba6c33 100644
--- a/services/core/java/com/android/server/biometrics/AuthService.java
+++ b/services/core/java/com/android/server/biometrics/AuthService.java
@@ -50,7 +50,6 @@
 import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.biometrics.SensorPropertiesInternal;
 import android.hardware.biometrics.face.IFace;
-import android.hardware.biometrics.fingerprint.IFingerprint;
 import android.hardware.face.FaceSensorConfigurations;
 import android.hardware.face.FaceSensorProperties;
 import android.hardware.face.FaceSensorPropertiesInternal;
@@ -74,6 +73,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
 import com.android.server.SystemService;
+import com.android.server.biometrics.sensors.fingerprint.FingerprintService;
 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
 
 import java.util.ArrayList;
@@ -203,7 +203,7 @@
          */
         @VisibleForTesting
         public String[] getFingerprintAidlInstances() {
-            return ServiceManager.getDeclaredInstances(IFingerprint.DESCRIPTOR);
+            return FingerprintService.getDeclaredInstances();
         }
 
         /**
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
index 60cfd5a..2f6ba0b 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
@@ -26,6 +26,7 @@
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_USER_CANCELED;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_VENDOR;
 import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;
+import static android.hardware.fingerprint.FingerprintSensorConfigurations.getIFingerprint;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -78,6 +79,7 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.SystemService;
@@ -1015,7 +1017,7 @@
         this(context, BiometricContext.getInstance(context),
                 () -> IBiometricService.Stub.asInterface(
                         ServiceManager.getService(Context.BIOMETRIC_SERVICE)),
-                () -> ServiceManager.getDeclaredInstances(IFingerprint.DESCRIPTOR),
+                () -> getDeclaredInstances(),
                 null /* fingerprintProvider */,
                 null /* fingerprintProviderFunction */);
     }
@@ -1039,8 +1041,7 @@
         mFingerprintProvider = fingerprintProvider != null ? fingerprintProvider :
                 (name) -> {
                     final String fqName = IFingerprint.DESCRIPTOR + "/" + name;
-                    final IFingerprint fp = IFingerprint.Stub.asInterface(
-                            Binder.allowBlocking(ServiceManager.waitForDeclaredService(fqName)));
+                    final IFingerprint fp = getIFingerprint(fqName);
                     if (fp != null) {
                         try {
                             return new FingerprintProvider(getContext(),
@@ -1129,6 +1130,24 @@
         publishBinderService(Context.FINGERPRINT_SERVICE, mServiceWrapper);
     }
 
+    /**
+     * Get all fingerprint hal instances declared in manifest
+     * @return instance names
+     */
+    public static String[] getDeclaredInstances() {
+        String[] a = ServiceManager.getDeclaredInstances(IFingerprint.DESCRIPTOR);
+        Slog.i(TAG, "Before:getDeclaredInstances: IFingerprint instance found, a.length="
+                + a.length);
+        if (!ArrayUtils.contains(a, "virtual")) {
+            // Now, the virtual hal is registered with IVirtualHal interface and it is also
+            //   moved from vendor to system_ext partition without a device manifest. So
+            //   if the old vhal is not declared, add here.
+            a = ArrayUtils.appendElement(String.class, a, "virtual");
+        }
+        Slog.i(TAG, "After:getDeclaredInstances: a.length=" + a.length);
+        return a;
+    }
+
     @NonNull
     private List<Fingerprint> getEnrolledFingerprintsDeprecated(int userId, String opPackageName) {
         final Pair<Integer, ServiceProvider> provider = mRegistry.getSingleProvider();
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java
index caa2c1c..fd3d996 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java
@@ -20,9 +20,9 @@
 import android.content.Context;
 import android.hardware.biometrics.ITestSession;
 import android.hardware.biometrics.ITestSessionCallback;
-import android.hardware.biometrics.fingerprint.AcquiredInfoAndVendorCode;
-import android.hardware.biometrics.fingerprint.EnrollmentProgressStep;
-import android.hardware.biometrics.fingerprint.NextEnrollment;
+import android.hardware.biometrics.fingerprint.virtualhal.AcquiredInfoAndVendorCode;
+import android.hardware.biometrics.fingerprint.virtualhal.EnrollmentProgressStep;
+import android.hardware.biometrics.fingerprint.virtualhal.NextEnrollment;
 import android.hardware.fingerprint.Fingerprint;
 import android.hardware.fingerprint.FingerprintEnrollOptions;
 import android.hardware.fingerprint.FingerprintManager;
@@ -300,4 +300,4 @@
         super.getSensorId_enforcePermission();
         return mSensorId;
     }
-}
\ No newline at end of file
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
index 9edaa4e..8195efe 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
@@ -17,6 +17,8 @@
 package com.android.server.biometrics.sensors.fingerprint.aidl;
 
 import static android.hardware.fingerprint.FingerprintManager.SENSOR_ID_ANY;
+import static android.hardware.fingerprint.FingerprintSensorConfigurations.getIFingerprint;
+import static android.hardware.fingerprint.FingerprintSensorConfigurations.remapFqName;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -34,9 +36,9 @@
 import android.hardware.biometrics.ITestSessionCallback;
 import android.hardware.biometrics.SensorLocationInternal;
 import android.hardware.biometrics.fingerprint.IFingerprint;
-import android.hardware.biometrics.fingerprint.IVirtualHal;
 import android.hardware.biometrics.fingerprint.PointerContext;
 import android.hardware.biometrics.fingerprint.SensorProps;
+import android.hardware.biometrics.fingerprint.virtualhal.IVirtualHal;
 import android.hardware.fingerprint.Fingerprint;
 import android.hardware.fingerprint.FingerprintAuthenticateOptions;
 import android.hardware.fingerprint.FingerprintEnrollOptions;
@@ -291,7 +293,8 @@
         if (mTestHalEnabled) {
             return true;
         }
-        return (ServiceManager.checkService(IFingerprint.DESCRIPTOR + "/" + mHalInstanceName)
+        return (ServiceManager.checkService(
+                remapFqName(IFingerprint.DESCRIPTOR + "/" + mHalInstanceName))
                 != null);
     }
 
@@ -330,10 +333,7 @@
 
         Slog.d(getTag(), "Daemon was null, reconnecting");
 
-        mDaemon = IFingerprint.Stub.asInterface(
-                Binder.allowBlocking(
-                        ServiceManager.waitForDeclaredService(
-                                IFingerprint.DESCRIPTOR + "/" + mHalInstanceNameCurrent)));
+        mDaemon = getIFingerprint(IFingerprint.DESCRIPTOR + "/" + mHalInstanceNameCurrent);
         if (mDaemon == null) {
             Slog.e(getTag(), "Unable to get daemon");
             return null;
@@ -905,8 +905,8 @@
     void setTestHalEnabled(boolean enabled) {
         final boolean changed = enabled != mTestHalEnabled;
         mTestHalEnabled = enabled;
-        Slog.i(getTag(), "setTestHalEnabled(): useVhalForTesting=" + Flags.useVhalForTesting()
-                + "mTestHalEnabled=" + mTestHalEnabled + " changed=" + changed);
+        Slog.i(getTag(), "setTestHalEnabled(): useVhalForTestingFlags=" + Flags.useVhalForTesting()
+                + " mTestHalEnabled=" + mTestHalEnabled + " changed=" + changed);
         if (changed && useVhalForTesting()) {
             getHalInstance();
         }
@@ -999,12 +999,13 @@
      */
     public IVirtualHal getVhal() throws RemoteException {
         if (mVhal == null && useVhalForTesting()) {
-            mVhal = IVirtualHal.Stub.asInterface(mDaemon.asBinder().getExtension());
-            if (mVhal == null) {
-                Slog.e(getTag(), "Unable to get fingerprint virtualhal interface");
-            }
+            mVhal = IVirtualHal.Stub.asInterface(
+                    Binder.allowBlocking(
+                            ServiceManager.waitForService(
+                                    IVirtualHal.DESCRIPTOR + "/"
+                                            + mHalInstanceNameCurrent)));
+            Slog.d(getTag(), "getVhal " + mHalInstanceNameCurrent);
         }
-
         return mVhal;
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
index d12d7b2..25d1fe7 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/Sensor.java
@@ -16,6 +16,8 @@
 
 package com.android.server.biometrics.sensors.fingerprint.aidl;
 
+import static android.hardware.fingerprint.FingerprintSensorConfigurations.remapFqName;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -356,8 +358,8 @@
         if (mTestHalEnabled) {
             return true;
         }
-        return (ServiceManager.checkService(IFingerprint.DESCRIPTOR + "/" + halInstance)
-                != null);
+        return (ServiceManager.checkService(
+                remapFqName(IFingerprint.DESCRIPTOR + "/" + halInstance)) != null);
     }
 
     @NonNull protected BiometricContext getBiometricContext() {
diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java
index 8a3e392..1d68ee54 100644
--- a/services/core/java/com/android/server/display/BrightnessRangeController.java
+++ b/services/core/java/com/android/server/display/BrightnessRangeController.java
@@ -16,6 +16,7 @@
 
 package com.android.server.display;
 
+import android.annotation.Nullable;
 import android.hardware.display.BrightnessInfo;
 import android.os.Handler;
 import android.os.IBinder;
@@ -92,7 +93,7 @@
         return mHbmController.getNormalBrightnessMax();
     }
 
-    void loadFromConfig(HighBrightnessModeMetadata hbmMetadata, IBinder token,
+    void loadFromConfig(@Nullable HighBrightnessModeMetadata hbmMetadata, IBinder token,
             DisplayDeviceInfo info, DisplayDeviceConfig displayDeviceConfig) {
         applyChanges(
                 () -> mNormalBrightnessModeController.resetNbmData(
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index ed16b14..9644b1d 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -2170,9 +2170,7 @@
 
             HighBrightnessModeMetadata hbmMetadata =
                     mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display);
-            if (hbmMetadata != null) {
-                dpc.onDisplayChanged(hbmMetadata, leadDisplayId);
-            }
+            dpc.onDisplayChanged(hbmMetadata, leadDisplayId);
         }
     }
 
@@ -2291,9 +2289,7 @@
 
             HighBrightnessModeMetadata hbmMetadata =
                     mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display);
-            if (hbmMetadata != null) {
-                dpc.onDisplayChanged(hbmMetadata, leadDisplayId);
-            }
+            dpc.onDisplayChanged(hbmMetadata, leadDisplayId);
         }
     }
 
@@ -3560,6 +3556,18 @@
         DisplayManagerFlags getFlags() {
             return new DisplayManagerFlags();
         }
+
+        DisplayPowerController getDisplayPowerController(Context context,
+                DisplayPowerController.Injector injector,
+                DisplayManagerInternal.DisplayPowerCallbacks callbacks, Handler handler,
+                SensorManager sensorManager, DisplayBlanker blanker, LogicalDisplay logicalDisplay,
+                BrightnessTracker brightnessTracker, BrightnessSetting brightnessSetting,
+                Runnable onBrightnessChangeRunnable, HighBrightnessModeMetadata hbmMetadata,
+                boolean bootCompleted, DisplayManagerFlags flags) {
+            return new DisplayPowerController(context, injector, callbacks, handler, sensorManager,
+                    blanker, logicalDisplay, brightnessTracker, brightnessSetting,
+                    onBrightnessChangeRunnable, hbmMetadata, bootCompleted, flags);
+        }
     }
 
     @VisibleForTesting
@@ -3612,7 +3620,7 @@
         // with the corresponding displaydevice.
         HighBrightnessModeMetadata hbmMetadata =
                 mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display);
-        displayPowerController = new DisplayPowerController(
+        displayPowerController = mInjector.getDisplayPowerController(
                 mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler,
                 mSensorManager, mDisplayBlanker, display, mBrightnessTracker, brightnessSetting,
                 () -> handleBrightnessChange(display), hbmMetadata, mBootCompleted, mFlags);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 7c591e3..c3faec0 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -849,7 +849,8 @@
      *
      * Make sure DisplayManagerService.mSyncRoot lock is held when this is called
      */
-    public void onDisplayChanged(HighBrightnessModeMetadata hbmMetadata, int leadDisplayId) {
+    public void onDisplayChanged(@Nullable HighBrightnessModeMetadata hbmMetadata,
+            int leadDisplayId) {
         mLeadDisplayId = leadDisplayId;
         final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
         if (device == null) {
@@ -949,7 +950,7 @@
     }
 
     private void loadFromDisplayDeviceConfig(IBinder token, DisplayDeviceInfo info,
-            HighBrightnessModeMetadata hbmMetadata) {
+            @Nullable HighBrightnessModeMetadata hbmMetadata) {
         // All properties that depend on the associated DisplayDevice and the DDC must be
         // updated here.
         mScreenBrightnessDozeConfig = BrightnessUtils.clampAbsoluteBrightness(
diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java
index da9eef2..135cab6 100644
--- a/services/core/java/com/android/server/display/HighBrightnessModeController.java
+++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java
@@ -266,7 +266,7 @@
         mSettingsObserver.stopObserving();
     }
 
-    void setHighBrightnessModeMetadata(HighBrightnessModeMetadata hbmInfo) {
+    void setHighBrightnessModeMetadata(@Nullable HighBrightnessModeMetadata hbmInfo) {
         mHighBrightnessModeMetadata = hbmInfo;
     }
 
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 1285a61..dfcea9f 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -2269,14 +2269,8 @@
     // Native callback.
     @SuppressWarnings("unused")
     private void notifyTouchpadHardwareState(TouchpadHardwareState hardwareStates, int deviceId) {
-        Slog.d(TAG, "notifyTouchpadHardwareState: Time: "
-                + hardwareStates.getTimestamp() + ", No. Buttons: "
-                + hardwareStates.getButtonsDown() + ", No. Fingers: "
-                + hardwareStates.getFingerCount() + ", No. Touch: "
-                + hardwareStates.getTouchCount() + ", Id: "
-                + deviceId);
         if (mTouchpadDebugViewController != null) {
-            mTouchpadDebugViewController.updateTouchpadHardwareState(hardwareStates);
+            mTouchpadDebugViewController.updateTouchpadHardwareState(hardwareStates, deviceId);
         }
     }
 
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
index ba56ad0..7d8ce5f 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
@@ -22,6 +22,7 @@
 import android.graphics.Color;
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
+import android.hardware.input.InputManager;
 import android.util.Slog;
 import android.view.Gravity;
 import android.view.MotionEvent;
@@ -65,7 +66,7 @@
         mTouchpadId = touchpadId;
         mWindowManager =
                 Objects.requireNonNull(getContext().getSystemService(WindowManager.class));
-        init(context);
+        init(context, touchpadId);
         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
 
         // TODO(b/360137366): Use the hardware properties to initialise layout parameters.
@@ -88,32 +89,40 @@
         mWindowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
     }
 
-    private void init(Context context) {
+    private void init(Context context, int touchpadId) {
         setOrientation(VERTICAL);
         setLayoutParams(new LayoutParams(
                 LayoutParams.WRAP_CONTENT,
                 LayoutParams.WRAP_CONTENT));
-        setBackgroundColor(Color.RED);
+        setBackgroundColor(Color.TRANSPARENT);
 
-        // TODO(b/286551975): Replace this content with the touchpad debug view.
-        TextView textView1 = new TextView(context);
-        textView1.setBackgroundColor(Color.TRANSPARENT);
-        textView1.setTextSize(20);
-        textView1.setText("Touchpad Debug View 1");
-        textView1.setGravity(Gravity.CENTER);
-        textView1.setTextColor(Color.WHITE);
-        textView1.setLayoutParams(new LayoutParams(1000, 200));
+        TextView nameView = new TextView(context);
+        nameView.setBackgroundColor(Color.RED);
+        nameView.setTextSize(20);
+        nameView.setText(Objects.requireNonNull(Objects.requireNonNull(
+                        mContext.getSystemService(InputManager.class))
+                .getInputDevice(touchpadId)).getName());
+        nameView.setGravity(Gravity.CENTER);
+        nameView.setTextColor(Color.WHITE);
+        nameView.setLayoutParams(new LayoutParams(1000, 200));
 
-        TextView textView2 = new TextView(context);
-        textView2.setBackgroundColor(Color.TRANSPARENT);
-        textView2.setTextSize(20);
-        textView2.setText("Touchpad Debug View 2");
-        textView2.setGravity(Gravity.CENTER);
-        textView2.setTextColor(Color.WHITE);
-        textView2.setLayoutParams(new LayoutParams(1000, 200));
+        TouchpadVisualisationView touchpadVisualisationView =
+                new TouchpadVisualisationView(context);
+        touchpadVisualisationView.setBackgroundColor(Color.WHITE);
+        touchpadVisualisationView.setLayoutParams(new LayoutParams(1000, 200));
 
-        addView(textView1);
-        addView(textView2);
+        //TODO(b/365562952): Add a display for recognized gesture info here
+        TextView gestureInfoView = new TextView(context);
+        gestureInfoView.setBackgroundColor(Color.GRAY);
+        gestureInfoView.setTextSize(20);
+        gestureInfoView.setText("Touchpad Debug View 3");
+        gestureInfoView.setGravity(Gravity.CENTER);
+        gestureInfoView.setTextColor(Color.BLACK);
+        gestureInfoView.setLayoutParams(new LayoutParams(1000, 200));
+
+        addView(nameView);
+        addView(touchpadVisualisationView);
+        addView(gestureInfoView);
 
         updateScreenDimensions();
     }
@@ -204,7 +213,15 @@
         return mWindowLayoutParams;
     }
 
-    public void updateHardwareState(TouchpadHardwareState touchpadHardwareState) {
+    /**
+     * Notify the view of a change in TouchpadHardwareState and changing the
+     * color of the view based on the status of the button click.
+     */
+    public void updateHardwareState(TouchpadHardwareState touchpadHardwareState, int deviceId) {
+        if (deviceId != mTouchpadId) {
+            return;
+        }
+
         if (mLastTouchpadState.getButtonsDown() == 0) {
             if (touchpadHardwareState.getButtonsDown() > 0) {
                 onTouchpadButtonPress();
@@ -219,18 +236,11 @@
 
     private void onTouchpadButtonPress() {
         Slog.d("TouchpadDebugView", "You clicked me!");
-
-        // Iterate through all child views
-        // Temporary demonstration for testing
-        for (int i = 0; i < getChildCount(); i++) {
-            getChildAt(i).setBackgroundColor(Color.BLUE);
-        }
+        getChildAt(0).setBackgroundColor(Color.BLUE);
     }
 
     private void onTouchpadButtonRelease() {
         Slog.d("TouchpadDebugView", "You released the click");
-        for (int i = 0; i < getChildCount(); i++) {
-            getChildAt(i).setBackgroundColor(Color.RED);
-        }
+        getChildAt(0).setBackgroundColor(Color.RED);
     }
 }
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
index bc53c49..4d527bd 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
@@ -68,6 +68,13 @@
     @Override
     public void onInputDeviceRemoved(int deviceId) {
         hideDebugView(deviceId);
+        if (mTouchpadDebugView == null) {
+            final InputManager inputManager = Objects.requireNonNull(
+                    mContext.getSystemService(InputManager.class));
+            for (int id : inputManager.getInputDeviceIds()) {
+                onInputDeviceAdded(id);
+            }
+        }
     }
 
     @Override
@@ -134,9 +141,13 @@
         Slog.d(TAG, "Touchpad debug view removed.");
     }
 
-    public void updateTouchpadHardwareState(TouchpadHardwareState touchpadHardwareState) {
+    /**
+     * Notify the TouchpadDebugView with the new TouchpadHardwareState.
+     */
+    public void updateTouchpadHardwareState(TouchpadHardwareState touchpadHardwareState,
+                                            int deviceId) {
         if (mTouchpadDebugView != null) {
-            mTouchpadDebugView.updateHardwareState(touchpadHardwareState);
+            mTouchpadDebugView.updateHardwareState(touchpadHardwareState, deviceId);
         }
     }
 }
diff --git a/services/core/java/com/android/server/input/debug/TouchpadVisualisationView.java b/services/core/java/com/android/server/input/debug/TouchpadVisualisationView.java
new file mode 100644
index 0000000..38442bc
--- /dev/null
+++ b/services/core/java/com/android/server/input/debug/TouchpadVisualisationView.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 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.input.debug;
+
+import android.content.Context;
+import android.view.View;
+
+public class TouchpadVisualisationView extends View {
+    public TouchpadVisualisationView(Context context) {
+        super(context);
+    }
+}
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index 6303ecd..a41675a 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -298,12 +298,13 @@
 
             restoreSettings();
 
-            // Wipe all shell overlays on boot, to recover from a potentially broken device
-            String shellPkgName = TextUtils.emptyIfNull(
-                    getContext().getString(android.R.string.config_systemShell));
-            mSettings.removeIf(overlayInfo -> overlayInfo.isFabricated
-                    && shellPkgName.equals(overlayInfo.packageName));
-
+            if (Build.IS_USER) {
+                // Wipe all shell overlays on boot, to recover from a potentially broken device
+                String shellPkgName = TextUtils.emptyIfNull(
+                        getContext().getString(android.R.string.config_systemShell));
+                mSettings.removeIf(overlayInfo -> overlayInfo.isFabricated
+                        && shellPkgName.equals(overlayInfo.packageName));
+            }
             initIfNeeded();
             onStartUser(UserHandle.USER_SYSTEM);
 
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 4dcc6e1..78359bd 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -2224,15 +2224,21 @@
     public ParcelFileDescriptor getWallpaperWithFeature(String callingPkg, String callingFeatureId,
             IWallpaperManagerCallback cb, final int which, Bundle outParams, int wallpaperUserId,
             boolean getCropped) {
-        final boolean hasPrivilege = hasPermission(READ_WALLPAPER_INTERNAL)
-                || hasPermission(MANAGE_EXTERNAL_STORAGE);
+        final int callingPid = Binder.getCallingPid();
+        final int callingUid = Binder.getCallingUid();
+        final boolean hasPrivilege = hasPermission(READ_WALLPAPER_INTERNAL);
         if (!hasPrivilege) {
-            mContext.getSystemService(StorageManager.class).checkPermissionReadImages(true,
-                    Binder.getCallingPid(), Binder.getCallingUid(), callingPkg, callingFeatureId);
+            boolean hasManageExternalStorage = hasPermission(MANAGE_EXTERNAL_STORAGE)
+                    || hasAppOpPermission(MANAGE_EXTERNAL_STORAGE, callingUid, callingPkg,
+                        callingFeatureId, "getWallpaperWithFeature from package: " + callingPkg);
+            if (!hasManageExternalStorage) {
+                mContext.getSystemService(StorageManager.class).checkPermissionReadImages(true,
+                        callingPid, callingUid, callingPkg, callingFeatureId);
+            }
         }
 
-        wallpaperUserId = ActivityManager.handleIncomingUser(Binder.getCallingPid(),
-                Binder.getCallingUid(), wallpaperUserId, false, true, "getWallpaper", null);
+        wallpaperUserId = ActivityManager.handleIncomingUser(callingPid, callingUid,
+                wallpaperUserId, false, true, "getWallpaper", null);
 
         if (which != FLAG_SYSTEM && which != FLAG_LOCK) {
             throw new IllegalArgumentException("Must specify exactly one kind of wallpaper to read");
@@ -2348,6 +2354,22 @@
         return mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED;
     }
 
+    private boolean hasAppOpPermission(String permission, int callingUid, String callingPackage,
+            String attributionTag, String message) {
+        final String op = AppOpsManager.permissionToOp(permission);
+        final int opMode = mAppOpsManager.noteOpNoThrow(op, callingUid, callingPackage,
+                attributionTag, message);
+        switch (opMode) {
+            case AppOpsManager.MODE_ALLOWED:
+            case AppOpsManager.MODE_FOREGROUND:
+                return true;
+            case AppOpsManager.MODE_DEFAULT:
+                return hasPermission(permission);
+            default:
+                return false;
+        }
+    }
+
     @Override
     public WallpaperInfo getWallpaperInfo(int userId) {
         return getWallpaperInfoWithFlags(FLAG_SYSTEM, userId);
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt
index a0f1a55..3bc4411 100644
--- a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt
@@ -19,8 +19,10 @@
 import android.app.appfunctions.AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_NAMESPACE
 import android.app.appfunctions.AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema
 import android.app.appfunctions.AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema
+import android.app.appsearch.AppSearchBatchResult
 import android.app.appsearch.AppSearchManager
 import android.app.appsearch.PutDocumentsRequest
+import android.app.appsearch.RemoveByDocumentIdRequest
 import android.app.appsearch.SearchSpec
 import android.app.appsearch.SetSchemaRequest
 import androidx.test.platform.app.InstrumentationRegistry
@@ -56,7 +58,7 @@
                 SetSchemaRequest.Builder()
                     .addSchemas(
                         createParentAppFunctionRuntimeSchema(),
-                        createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME)
+                        createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME),
                     )
                     .build()
 
@@ -74,7 +76,7 @@
                 SetSchemaRequest.Builder()
                     .addSchemas(
                         createParentAppFunctionRuntimeSchema(),
-                        createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME)
+                        createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME),
                     )
                     .build()
             val schema = session.setSchema(setSchemaRequest)
@@ -93,6 +95,40 @@
     }
 
     @Test
+    fun remove() {
+        val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build()
+        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session ->
+            val setSchemaRequest =
+                SetSchemaRequest.Builder()
+                    .addSchemas(
+                        createParentAppFunctionRuntimeSchema(),
+                        createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME),
+                    )
+                    .build()
+            val schema = session.setSchema(setSchemaRequest)
+            assertThat(schema.get()).isNotNull()
+            val appFunctionRuntimeMetadata =
+                AppFunctionRuntimeMetadata.Builder(TEST_PACKAGE_NAME, TEST_FUNCTION_ID, "").build()
+            val putDocumentsRequest: PutDocumentsRequest =
+                PutDocumentsRequest.Builder()
+                    .addGenericDocuments(appFunctionRuntimeMetadata)
+                    .build()
+            val putResult = session.put(putDocumentsRequest)
+            assertThat(putResult.get().isSuccess).isTrue()
+            val removeDocumentRequest =
+                RemoveByDocumentIdRequest.Builder(APP_FUNCTION_RUNTIME_NAMESPACE)
+                    .addIds(appFunctionRuntimeMetadata.id)
+                    .build()
+
+            val removeResult: AppSearchBatchResult<String, Void> =
+                session.remove(removeDocumentRequest).get()
+
+            assertThat(removeResult).isNotNull()
+            assertThat(removeResult.isSuccess).isTrue()
+        }
+    }
+
+    @Test
     fun search() {
         val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build()
         FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session ->
@@ -100,7 +136,7 @@
                 SetSchemaRequest.Builder()
                     .addSchemas(
                         createParentAppFunctionRuntimeSchema(),
-                        createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME)
+                        createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME),
                     )
                     .build()
             val schema = session.setSchema(setSchemaRequest)
@@ -132,7 +168,7 @@
                 SetSchemaRequest.Builder()
                     .addSchemas(
                         createParentAppFunctionRuntimeSchema(),
-                        createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME)
+                        createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME),
                     )
                     .build()
             val schema = session.setSchema(setSchemaRequest)
@@ -144,12 +180,13 @@
                     .build()
             val putResult = session.put(putDocumentsRequest)
 
-            val genricDocument = session
-                .getByDocumentId(
-                    /* documentId= */ "${TEST_PACKAGE_NAME}/${TEST_FUNCTION_ID}",
-                    APP_FUNCTION_RUNTIME_NAMESPACE
-                )
-                .get()
+            val genricDocument =
+                session
+                    .getByDocumentId(
+                        /* documentId= */ "${TEST_PACKAGE_NAME}/${TEST_FUNCTION_ID}",
+                        APP_FUNCTION_RUNTIME_NAMESPACE,
+                    )
+                    .get()
 
             val foundAppFunctionRuntimeMetadata = AppFunctionRuntimeMetadata(genricDocument)
             assertThat(foundAppFunctionRuntimeMetadata.functionId).isEqualTo(TEST_FUNCTION_ID)
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
index 1061da2..5769e07 100644
--- a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
@@ -16,12 +16,9 @@
 package com.android.server.appfunctions
 
 import android.app.appfunctions.AppFunctionRuntimeMetadata
-import android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID
-import android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME
 import android.app.appsearch.AppSearchManager
 import android.app.appsearch.AppSearchManager.SearchContext
 import android.app.appsearch.PutDocumentsRequest
-import android.app.appsearch.SearchSpec
 import android.app.appsearch.SetSchemaRequest
 import android.util.ArrayMap
 import android.util.ArraySet
@@ -76,20 +73,11 @@
                 testExecutor,
                 FutureAppSearchSession(appSearchManager, testExecutor, searchContext),
             )
-        val searchSpec: SearchSpec =
-            SearchSpec.Builder()
-                .addFilterSchemas(
-                    AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE,
-                    AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
-                        .schemaType,
-                )
-                .build()
         val packageToFunctionIdMap =
             metadataSyncAdapter.getPackageToFunctionIdMap(
-                "",
-                searchSpec,
-                PROPERTY_FUNCTION_ID,
-                PROPERTY_PACKAGE_NAME,
+                AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE,
+                AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID,
+                AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME,
             )
 
         assertThat(packageToFunctionIdMap).isNotNull()
@@ -135,21 +123,11 @@
                 testExecutor,
                 FutureAppSearchSession(appSearchManager, testExecutor, searchContext),
             )
-        val searchSpec: SearchSpec =
-            SearchSpec.Builder()
-                .setResultCountPerPage(1)
-                .addFilterSchemas(
-                    AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE,
-                    AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
-                        .schemaType,
-                )
-                .build()
         val packageToFunctionIdMap =
             metadataSyncAdapter.getPackageToFunctionIdMap(
-                "",
-                searchSpec,
-                PROPERTY_FUNCTION_ID,
-                PROPERTY_PACKAGE_NAME,
+                AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE,
+                AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID,
+                AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME,
             )
 
         assertThat(packageToFunctionIdMap).isNotNull()
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 52f1cbd..76c8e686 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -143,6 +143,7 @@
 import com.android.server.display.DisplayManagerService.SyncRoot;
 import com.android.server.display.config.SensorData;
 import com.android.server.display.feature.DisplayManagerFlags;
+import com.android.server.display.layout.Layout;
 import com.android.server.display.notifications.DisplayNotificationManager;
 import com.android.server.input.InputManagerInternal;
 import com.android.server.lights.LightsManager;
@@ -3201,6 +3202,45 @@
                 argThat(matchesFilter));
     }
 
+    @Test
+    public void testOnDisplayChanged_HbmMetadataNull() {
+        DisplayPowerController dpc = mock(DisplayPowerController.class);
+        DisplayManagerService.Injector injector = new BasicInjector() {
+            @Override
+            DisplayPowerController getDisplayPowerController(Context context,
+                    DisplayPowerController.Injector injector,
+                    DisplayManagerInternal.DisplayPowerCallbacks callbacks, Handler handler,
+                    SensorManager sensorManager, DisplayBlanker blanker,
+                    LogicalDisplay logicalDisplay, BrightnessTracker brightnessTracker,
+                    BrightnessSetting brightnessSetting, Runnable onBrightnessChangeRunnable,
+                    HighBrightnessModeMetadata hbmMetadata, boolean bootCompleted,
+                    DisplayManagerFlags flags) {
+                return dpc;
+            }
+        };
+        DisplayManagerService displayManager = new DisplayManagerService(mContext, injector);
+        DisplayManagerInternal localService = displayManager.new LocalService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        // Add the FakeDisplayDevice
+        FakeDisplayDevice displayDevice = new FakeDisplayDevice();
+        DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
+        displayDeviceInfo.state = Display.STATE_ON;
+        displayDevice.setDisplayDeviceInfo(displayDeviceInfo);
+        displayManager.getDisplayDeviceRepository()
+                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
+        initDisplayPowerController(localService);
+
+        // Simulate DisplayDevice change
+        DisplayDeviceInfo displayDeviceInfo2 = new DisplayDeviceInfo();
+        displayDeviceInfo2.copyFrom(displayDeviceInfo);
+        displayDeviceInfo2.state = Display.STATE_DOZE;
+        updateDisplayDeviceInfo(displayManager, displayDevice, displayDeviceInfo2);
+
+        verify(dpc).onDisplayChanged(/* hbmMetadata= */ null, Layout.NO_LEAD_DISPLAY);
+    }
+
     private void initDisplayPowerController(DisplayManagerInternal localService) {
         localService.initPowerManagement(new DisplayManagerInternal.DisplayPowerCallbacks() {
             @Override
diff --git a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
index cbf7935..def3355 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
@@ -19,7 +19,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.UserInfo;
+import android.os.UserHandle;
 import android.webkit.UserPackage;
 import android.webkit.WebViewProviderInfo;
 
@@ -137,16 +137,12 @@
         List<UserPackage> ret = new ArrayList();
         // Loop over defined users, and find the corresponding package for each user.
         for (int userId : mUsers) {
-            ret.add(new UserPackage(createUserInfo(userId),
+            ret.add(new UserPackage(UserHandle.of(userId),
                     userPackages == null ? null : userPackages.get(userId)));
         }
         return ret;
     }
 
-    private static UserInfo createUserInfo(int userId) {
-        return new UserInfo(userId, "User nr. " + userId, 0 /* flags */);
-    }
-
     /**
      * Set package for primary user.
      */
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
index 0f08be2..2a49eb1 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
@@ -296,33 +296,31 @@
 
     @Test
     public void testTouchpadClick() {
-        View child;
+        View child = mTouchpadDebugView.getChildAt(0);
 
         mTouchpadDebugView.updateHardwareState(
                 new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0,
-                        new TouchpadFingerState[0]));
+                        new TouchpadFingerState[0]), TOUCHPAD_DEVICE_ID);
 
-        for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) {
-            child = mTouchpadDebugView.getChildAt(i);
-            assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.BLUE);
-        }
+        assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.BLUE);
 
         mTouchpadDebugView.updateHardwareState(
                 new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0,
-                        new TouchpadFingerState[0]));
+                        new TouchpadFingerState[0]), TOUCHPAD_DEVICE_ID);
 
-        for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) {
-            child = mTouchpadDebugView.getChildAt(i);
-            assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.RED);
-        }
+        assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.RED);
 
         mTouchpadDebugView.updateHardwareState(
                 new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0,
-                        new TouchpadFingerState[0]));
+                        new TouchpadFingerState[0]), TOUCHPAD_DEVICE_ID);
 
-        for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) {
-            child = mTouchpadDebugView.getChildAt(i);
-            assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.BLUE);
-        }
+        assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.BLUE);
+
+        // Color should not change because hardware state of a different touchpad
+        mTouchpadDebugView.updateHardwareState(
+                new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0,
+                        new TouchpadFingerState[0]), TOUCHPAD_DEVICE_ID + 1);
+
+        assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.BLUE);
     }
 }
diff --git a/tools/aapt/Package.cpp b/tools/aapt/Package.cpp
index 60c4bf5..5e0f87f 100644
--- a/tools/aapt/Package.cpp
+++ b/tools/aapt/Package.cpp
@@ -292,12 +292,13 @@
             }
             if (!hasData) {
                 const String8& srcName = file->getSourceFile();
-                auto fileModWhen = getFileModDate(srcName.c_str());
-                if (fileModWhen == kInvalidModDate) { // file existence tested earlier,
-                    return false;                     //  not expecting an error here
+                time_t fileModWhen;
+                fileModWhen = getFileModDate(srcName.c_str());
+                if (fileModWhen == (time_t) -1) { // file existence tested earlier,
+                    return false;                 //  not expecting an error here
                 }
-
-                if (toTimeT(fileModWhen) > entry->getModWhen()) {
+    
+                if (fileModWhen > entry->getModWhen()) {
                     // mark as deleted so add() will succeed
                     if (bundle->getVerbose()) {
                         printf("      (removing old '%s')\n", storageName.c_str());