Merge "Enable SQLite JNI for host Mac and Windows" into main
diff --git a/Android.bp b/Android.bp
index 516fc9c..811755d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -368,6 +368,7 @@
     jarjar_rules: ":framework-jarjar-rules",
     javac_shard_size: 150,
     plugins: [
+        "cached-property-annotation-processor",
         "view-inspector-annotation-processor",
         "staledataclass-annotation-processor",
         "error_prone_android_framework",
diff --git a/core/api/current.txt b/core/api/current.txt
index c3c41f8..b740ef3 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -8673,6 +8673,8 @@
     field public static final int TAG_MAX_SCREEN_LOCK_TIMEOUT_SET = 210019; // 0x33463
     field public static final int TAG_MEDIA_MOUNT = 210013; // 0x3345d
     field public static final int TAG_MEDIA_UNMOUNT = 210014; // 0x3345e
+    field @FlaggedApi("android.nfc.nfc_state_change_security_log_event_enabled") public static final int TAG_NFC_DISABLED = 210046; // 0x3347e
+    field @FlaggedApi("android.nfc.nfc_state_change_security_log_event_enabled") public static final int TAG_NFC_ENABLED = 210045; // 0x3347d
     field public static final int TAG_OS_SHUTDOWN = 210010; // 0x3345a
     field public static final int TAG_OS_STARTUP = 210009; // 0x33459
     field public static final int TAG_PACKAGE_INSTALLED = 210041; // 0x33479
@@ -37432,6 +37434,7 @@
     field public static final String ACTION_APPLICATION_SETTINGS = "android.settings.APPLICATION_SETTINGS";
     field public static final String ACTION_APP_LOCALE_SETTINGS = "android.settings.APP_LOCALE_SETTINGS";
     field public static final String ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS = "android.settings.APP_NOTIFICATION_BUBBLE_SETTINGS";
+    field @FlaggedApi("android.app.api_rich_ongoing") public static final String ACTION_APP_NOTIFICATION_PROMOTION_SETTINGS = "android.settings.APP_NOTIFICATION_PROMOTION_SETTINGS";
     field public static final String ACTION_APP_NOTIFICATION_SETTINGS = "android.settings.APP_NOTIFICATION_SETTINGS";
     field public static final String ACTION_APP_OPEN_BY_DEFAULT_SETTINGS = "android.settings.APP_OPEN_BY_DEFAULT_SETTINGS";
     field public static final String ACTION_APP_SEARCH_SETTINGS = "android.settings.APP_SEARCH_SETTINGS";
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index dfed1f7..41abd68 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -958,6 +958,9 @@
      * Returns whether the calling app's properly formatted notifications can appear in a promoted
      * format, which may result in higher ranking, appearances on additional surfaces, and richer
      * presentation.
+     *
+     * Apps can request this permission by sending the user to the activity that matches the system
+     * intent action {@link android.provider.Settings#ACTION_APP_NOTIFICATION_PROMOTION_SETTINGS}.
      */
     @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
     public boolean canPostPromotedNotifications() {
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index 84a4eb4..e043a5d 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -431,16 +431,19 @@
     }
 
     /**
-     * Protected so that tests can override and returns something a fixed value.
+     * public so that tests can access and override
      */
     @VisibleForTesting
-    protected @NonNull DisplayMetrics getDisplayMetrics(int displayId, DisplayAdjustments da) {
+    public @NonNull DisplayMetrics getDisplayMetrics(int displayId, DisplayAdjustments da) {
         final DisplayManagerGlobal displayManagerGlobal = DisplayManagerGlobal.getInstance();
         final DisplayMetrics dm = new DisplayMetrics();
         final DisplayInfo displayInfo = displayManagerGlobal != null
                 ? displayManagerGlobal.getDisplayInfo(displayId) : null;
         if (displayInfo != null) {
-            displayInfo.getAppMetrics(dm, da);
+            final Configuration dajConfig = da.getConfiguration();
+            displayInfo.getAppMetrics(dm, da.getCompatibilityInfo(),
+                    (mResDisplayId == displayId && Configuration.EMPTY.equals(dajConfig))
+                            ? mResConfiguration : dajConfig);
         } else {
             dm.setToDefaults();
         }
diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java
index 7903f1c..2e6f3e1 100644
--- a/core/java/android/app/UiModeManager.java
+++ b/core/java/android/app/UiModeManager.java
@@ -16,6 +16,7 @@
 
 package android.app;
 
+import static android.app.Flags.enableCurrentModeTypeBinderCache;
 import static android.app.Flags.enableNightModeBinderCache;
 
 import android.annotation.CallbackExecutor;
@@ -682,6 +683,53 @@
         }
     }
 
+    private Integer getCurrentModeTypeFromServer() {
+        try {
+            if (sGlobals != null) {
+                return sGlobals.mService.getCurrentModeType();
+            }
+            return Configuration.UI_MODE_TYPE_NORMAL;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+
+    /**
+     * Retrieve the current running mode type for the user.
+     */
+    private final IpcDataCache.QueryHandler<Void, Integer> mCurrentModeTypeQuery =
+            new IpcDataCache.QueryHandler<>() {
+
+                @Override
+                @NonNull
+                public Integer apply(Void query) {
+                    return getCurrentModeTypeFromServer();
+                }
+            };
+
+    private static final String CURRENT_MODE_TYPE_API = "getCurrentModeType";
+
+    /**
+     * Cache the current running mode type for a user.
+     */
+    private final IpcDataCache<Void, Integer> mCurrentModeTypeCache =
+            new IpcDataCache<>(1, IpcDataCache.MODULE_SYSTEM,
+                    CURRENT_MODE_TYPE_API, /* cacheName= */ "CurrentModeTypeCache",
+                    mCurrentModeTypeQuery);
+
+    /**
+     * Invalidate the current mode type cache.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_ENABLE_CURRENT_MODE_TYPE_BINDER_CACHE)
+    public static void invalidateCurrentModeTypeCache() {
+        IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM,
+                CURRENT_MODE_TYPE_API);
+    }
+
+
     /**
      * Return the current running mode type.  May be one of
      * {@link Configuration#UI_MODE_TYPE_NORMAL Configuration.UI_MODE_TYPE_NORMAL},
@@ -693,14 +741,11 @@
      * {@link Configuration#UI_MODE_TYPE_VR_HEADSET Configuration.UI_MODE_TYPE_VR_HEADSET}.
      */
     public int getCurrentModeType() {
-        if (sGlobals != null) {
-            try {
-                return sGlobals.mService.getCurrentModeType();
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            }
+        if (enableCurrentModeTypeBinderCache()) {
+            return mCurrentModeTypeCache.query(null);
+        } else {
+            return getCurrentModeTypeFromServer();
         }
-        return Configuration.UI_MODE_TYPE_NORMAL;
     }
 
     /**
diff --git a/core/java/android/app/admin/SecurityLog.java b/core/java/android/app/admin/SecurityLog.java
index beb93fd..eb0ea1e 100644
--- a/core/java/android/app/admin/SecurityLog.java
+++ b/core/java/android/app/admin/SecurityLog.java
@@ -16,7 +16,10 @@
 
 package android.app.admin;
 
+import static android.nfc.Flags.FLAG_NFC_STATE_CHANGE_SECURITY_LOG_EVENT_ENABLED;
+
 import android.Manifest;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -100,6 +103,8 @@
             TAG_PACKAGE_UPDATED,
             TAG_PACKAGE_UNINSTALLED,
             TAG_BACKUP_SERVICE_TOGGLED,
+            TAG_NFC_ENABLED,
+            TAG_NFC_DISABLED,
     })
     public @interface SecurityLogTag {}
 
@@ -610,6 +615,18 @@
      */
     public static final int TAG_BACKUP_SERVICE_TOGGLED =
             SecurityLogTags.SECURITY_BACKUP_SERVICE_TOGGLED;
+
+    /**
+     * Indicates that NFC service is enabled. There is no extra payload in the log event.
+     */
+    @FlaggedApi(FLAG_NFC_STATE_CHANGE_SECURITY_LOG_EVENT_ENABLED)
+    public static final int TAG_NFC_ENABLED = SecurityLogTags.SECURITY_NFC_ENABLED;
+
+    /**
+     * Indicates that NFC service is disabled. There is no extra payload in the log event.
+     */
+    @FlaggedApi(FLAG_NFC_STATE_CHANGE_SECURITY_LOG_EVENT_ENABLED)
+    public static final int TAG_NFC_DISABLED = SecurityLogTags.SECURITY_NFC_DISABLED;
     /**
      * Event severity level indicating that the event corresponds to normal workflow.
      */
diff --git a/core/java/android/app/admin/SecurityLogTags.logtags b/core/java/android/app/admin/SecurityLogTags.logtags
index 7b3aa7b..8f22c76 100644
--- a/core/java/android/app/admin/SecurityLogTags.logtags
+++ b/core/java/android/app/admin/SecurityLogTags.logtags
@@ -48,4 +48,6 @@
 210041 security_package_installed               (package_name|3),(version_code|1),(user_id|1)
 210042 security_package_updated                 (package_name|3),(version_code|1),(user_id|1)
 210043 security_package_uninstalled             (package_name|3),(version_code|1),(user_id|1)
-210044 security_backup_service_toggled          (package|3),(admin_user|1),(enabled|1)
\ No newline at end of file
+210044 security_backup_service_toggled          (package|3),(admin_user|1),(enabled|1)
+210045 security_nfc_enabled
+210046 security_nfc_disabled
\ No newline at end of file
diff --git a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
index fe2db49..64dece9 100644
--- a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
+++ b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
@@ -16,6 +16,8 @@
 
 package android.app.appfunctions;
 
+import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DEFAULT;
+import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_ENABLED;
 import static android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_APP_FUNCTION_STATIC_METADATA_QUALIFIED_ID;
 import static android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_ENABLED;
 import static android.app.appfunctions.AppFunctionStaticMetadataHelper.APP_FUNCTION_INDEXER_PACKAGE;
@@ -166,15 +168,18 @@
             if (runtimeMetadataResults.isEmpty()) {
                 throw new IllegalArgumentException("App function not found.");
             }
-            boolean[] enabled =
+            long enabled =
                     runtimeMetadataResults
                             .getFirst()
                             .getGenericDocument()
-                            .getPropertyBooleanArray(PROPERTY_ENABLED);
-            if (enabled != null && enabled.length != 0) {
-                return enabled[0];
+                            .getPropertyLong(PROPERTY_ENABLED);
+            // If enabled is not equal to APP_FUNCTION_STATE_DEFAULT, it means it IS overridden and
+            // we should return the overridden value.
+            if (enabled != APP_FUNCTION_STATE_DEFAULT) {
+                return enabled == APP_FUNCTION_STATE_ENABLED;
             }
-            // Runtime metadata not found. Using the default value in the static metadata.
+            // Runtime metadata not found or enabled is equal to APP_FUNCTION_STATE_DEFAULT.
+            // Using the default value in the static metadata.
             return joinedStaticRuntimeResults
                     .getFirst()
                     .getGenericDocument()
diff --git a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java
index 8b7f326..08ecced 100644
--- a/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java
+++ b/core/java/android/app/appfunctions/AppFunctionRuntimeMetadata.java
@@ -16,11 +16,15 @@
 
 package android.app.appfunctions;
 
+import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DEFAULT;
+import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DISABLED;
+import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_ENABLED;
 import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;
 
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.appfunctions.AppFunctionManager.EnabledState;
 import android.app.appsearch.AppSearchSchema;
 import android.app.appsearch.GenericDocument;
 
@@ -162,15 +166,13 @@
      * Returns if the function is set to be enabled or not. If not set, the {@link
      * AppFunctionStaticMetadataHelper#STATIC_PROPERTY_ENABLED_BY_DEFAULT} value would be used.
      */
-    @Nullable
-    public Boolean getEnabled() {
-        // We can't use getPropertyBoolean here. getPropertyBoolean returns false instead of null
-        // if the value is missing.
-        boolean[] enabled = getPropertyBooleanArray(PROPERTY_ENABLED);
-        if (enabled == null || enabled.length == 0) {
-            return null;
-        }
-        return enabled[0];
+    @EnabledState
+    public int getEnabled() {
+        // getPropertyLong returns the first long associated with the given path or default value 0
+        // if there is no such value or the value is of a different type.
+        // APP_FUNCTION_STATE_DEFAULT also equals 0 which means the returned value will be 0 when an
+        // app as either never changed the enabled bit at runtime or has reset it to the default.
+        return (int) getPropertyLong(PROPERTY_ENABLED);
     }
 
     /** Returns the qualified id linking to the static metadata of the app function. */
@@ -217,12 +219,14 @@
          * TODO(369683073) Replace the tristate Boolean with IntDef EnabledState.
          */
         @NonNull
-        public Builder setEnabled(@Nullable Boolean enabled) {
-            if (enabled == null) {
-                setPropertyBoolean(PROPERTY_ENABLED);
-            } else {
-                setPropertyBoolean(PROPERTY_ENABLED, enabled);
+        public Builder setEnabled(@EnabledState int enabledState) {
+            if (enabledState != APP_FUNCTION_STATE_DEFAULT
+                    && enabledState != APP_FUNCTION_STATE_ENABLED
+                    && enabledState != APP_FUNCTION_STATE_DISABLED) {
+                throw new IllegalArgumentException(
+                        "Value of EnabledState is unsupported.");
             }
+            setPropertyLong(PROPERTY_ENABLED, enabledState);
             return this;
         }
 
diff --git a/core/java/android/app/appfunctions/OWNERS b/core/java/android/app/appfunctions/OWNERS
index c6827cc..6a69e15 100644
--- a/core/java/android/app/appfunctions/OWNERS
+++ b/core/java/android/app/appfunctions/OWNERS
@@ -4,3 +4,4 @@
 tonymak@google.com
 mingweiliao@google.com
 anothermark@google.com
+utkarshnigam@google.com
diff --git a/core/java/android/app/appfunctions/TEST_MAPPING b/core/java/android/app/appfunctions/TEST_MAPPING
index 91e82ec..27517c8 100644
--- a/core/java/android/app/appfunctions/TEST_MAPPING
+++ b/core/java/android/app/appfunctions/TEST_MAPPING
@@ -1,10 +1,7 @@
 {
-  "postsubmit": [
+  "imports": [
     {
-      "name": "FrameworksAppFunctionsTests"
-    },
-    {
-      "name": "CtsAppFunctionTestCases"
+      "path": "frameworks/base/services/appfunctions/TEST_MAPPING"
     }
   ]
 }
\ No newline at end of file
diff --git a/core/java/android/app/ui_mode_manager.aconfig b/core/java/android/app/ui_mode_manager.aconfig
index 9f44a4d..05b46e0 100644
--- a/core/java/android/app/ui_mode_manager.aconfig
+++ b/core/java/android/app/ui_mode_manager.aconfig
@@ -9,4 +9,15 @@
      metadata {
          purpose: PURPOSE_BUGFIX
      }
+}
+
+flag {
+     namespace: "systemui"
+     name: "enable_current_mode_type_binder_cache"
+     description: "Enables the use of binder caching for current running mode type."
+     bug: "362572732"
+     is_fixed_read_only: true
+     metadata {
+         purpose: PURPOSE_BUGFIX
+     }
 }
\ No newline at end of file
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index d5edc92..cf65539 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -456,7 +456,6 @@
   }
 }
 
-
 flag {
     name: "caching_development_improvements"
     namespace: "multiuser"
@@ -464,3 +463,13 @@
     bug: "364947162"
     is_fixed_read_only: true
 }
+
+flag {
+  name: "show_custom_unlock_title_inside_private_profile"
+  namespace: "profile_experiences"
+  description: "When private space is unlocked show dynamic title in unlock factor screens based on lock factor set for the profile"
+  bug: "323835257"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 461f1e0..3ae9511 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -69,6 +69,8 @@
 import android.view.WindowManager.LayoutParams;
 
 import com.android.internal.R;
+import com.android.internal.annotations.CachedProperty;
+import com.android.internal.annotations.CachedPropertyDefaults;
 
 import java.io.IOException;
 import java.lang.annotation.Retention;
@@ -90,6 +92,7 @@
  */
 @SystemService(Context.USER_SERVICE)
 @android.ravenwood.annotation.RavenwoodKeepPartialClass
+@CachedPropertyDefaults()
 public class UserManager {
 
     private static final String TAG = "UserManager";
@@ -108,6 +111,9 @@
     /** Whether the device is in headless system user mode; null until cached. */
     private static Boolean sIsHeadlessSystemUser = null;
 
+    /** Generated class containing IpcDataCaches. */
+    private final Object mIpcDataCache = new UserManagerCache();
+
     /** Maximum length of username.
      * @hide
      */
@@ -3766,62 +3772,18 @@
         return isUserUnlocked(user.getIdentifier());
     }
 
-    private static final String CACHE_KEY_IS_USER_UNLOCKED_PROPERTY =
-        PropertyInvalidatedCache.createPropertyName(
-            PropertyInvalidatedCache.MODULE_SYSTEM, "is_user_unlocked");
-
-    private final PropertyInvalidatedCache<Integer, Boolean> mIsUserUnlockedCache =
-            new PropertyInvalidatedCache<Integer, Boolean>(
-                32, CACHE_KEY_IS_USER_UNLOCKED_PROPERTY) {
-                @Override
-                public Boolean recompute(Integer query) {
-                    try {
-                        return mService.isUserUnlocked(query);
-                    } catch (RemoteException re) {
-                        throw re.rethrowFromSystemServer();
-                    }
-                }
-                @Override
-                public boolean bypass(Integer query) {
-                    return query < 0;
-                }
-            };
-
-    // Uses IS_USER_UNLOCKED_PROPERTY for invalidation as the APIs have the same dependencies.
-    private final PropertyInvalidatedCache<Integer, Boolean> mIsUserUnlockingOrUnlockedCache =
-            new PropertyInvalidatedCache<Integer, Boolean>(
-                32, CACHE_KEY_IS_USER_UNLOCKED_PROPERTY) {
-                @Override
-                public Boolean recompute(Integer query) {
-                    try {
-                        return mService.isUserUnlockingOrUnlocked(query);
-                    } catch (RemoteException re) {
-                        throw re.rethrowFromSystemServer();
-                    }
-                }
-                @Override
-                public boolean bypass(Integer query) {
-                    return query < 0;
-                }
-            };
-
     /** @hide */
     @UnsupportedAppUsage
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
+    @CachedProperty(modsFlagOnOrNone = {}, api = "is_user_unlocked")
     public boolean isUserUnlocked(@UserIdInt int userId) {
-        return mIsUserUnlockedCache.query(userId);
-    }
-
-    /** @hide */
-    public void disableIsUserUnlockedCache() {
-        mIsUserUnlockedCache.disableLocal();
-        mIsUserUnlockingOrUnlockedCache.disableLocal();
+        return ((UserManagerCache) mIpcDataCache).isUserUnlocked(mService::isUserUnlocked, userId);
     }
 
     /** @hide */
     public static final void invalidateIsUserUnlockedCache() {
-        PropertyInvalidatedCache.invalidateCache(CACHE_KEY_IS_USER_UNLOCKED_PROPERTY);
+        UserManagerCache.invalidateUserUnlocked();
     }
 
     /**
@@ -3852,8 +3814,10 @@
     /** @hide */
     @RequiresPermission(anyOf = {Manifest.permission.MANAGE_USERS,
             Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
+    @CachedProperty(modsFlagOnOrNone = {}, api = "is_user_unlocked")
     public boolean isUserUnlockingOrUnlocked(@UserIdInt int userId) {
-        return mIsUserUnlockingOrUnlockedCache.query(userId);
+        return ((UserManagerCache) mIpcDataCache)
+                .isUserUnlockingOrUnlocked(mService::isUserUnlockingOrUnlocked, userId);
     }
 
     /**
@@ -5686,31 +5650,9 @@
         }
     }
 
-    private static final String CACHE_KEY_QUIET_MODE_ENABLED_PROPERTY =
-        PropertyInvalidatedCache.createPropertyName(
-            PropertyInvalidatedCache.MODULE_SYSTEM, "quiet_mode_enabled");
-
-    private final PropertyInvalidatedCache<Integer, Boolean> mQuietModeEnabledCache =
-            new PropertyInvalidatedCache<Integer, Boolean>(
-                32, CACHE_KEY_QUIET_MODE_ENABLED_PROPERTY) {
-                @Override
-                public Boolean recompute(Integer query) {
-                    try {
-                        return mService.isQuietModeEnabled(query);
-                    } catch (RemoteException re) {
-                        throw re.rethrowFromSystemServer();
-                    }
-                }
-                @Override
-                public boolean bypass(Integer query) {
-                    return query < 0;
-                }
-            };
-
-
     /** @hide */
     public static final void invalidateQuietModeEnabledCache() {
-        PropertyInvalidatedCache.invalidateCache(CACHE_KEY_QUIET_MODE_ENABLED_PROPERTY);
+        UserManagerCache.invalidateQuietModeEnabled();
     }
 
     /**
@@ -5719,13 +5661,15 @@
      * @param userHandle The user handle of the profile to be queried.
      * @return true if the profile is in quiet mode, false otherwise.
      */
+    @CachedProperty(modsFlagOnOrNone = {})
     public boolean isQuietModeEnabled(UserHandle userHandle) {
-        if (android.multiuser.Flags.cacheQuietModeState()){
+        if (android.multiuser.Flags.cacheQuietModeState()) {
             final int userId = userHandle.getIdentifier();
             if (userId < 0) {
                 return false;
             }
-            return mQuietModeEnabledCache.query(userId);
+            return ((UserManagerCache) mIpcDataCache).isQuietModeEnabled(
+                    (UserHandle uh) -> mService.isQuietModeEnabled(uh.getIdentifier()), userHandle);
         }
         try {
             return mService.isQuietModeEnabled(userHandle.getIdentifier());
@@ -6424,41 +6368,21 @@
                 Settings.Global.DEVICE_DEMO_MODE, 0) > 0;
     }
 
-    private static final String CACHE_KEY_USER_SERIAL_NUMBER_PROPERTY =
-        PropertyInvalidatedCache.createPropertyName(
-            PropertyInvalidatedCache.MODULE_SYSTEM, "user_serial_number");
-
-    private final PropertyInvalidatedCache<Integer, Integer> mUserSerialNumberCache =
-            new PropertyInvalidatedCache<Integer, Integer>(
-                32, CACHE_KEY_USER_SERIAL_NUMBER_PROPERTY) {
-                @Override
-                public Integer recompute(Integer query) {
-                    try {
-                        return mService.getUserSerialNumber(query);
-                    } catch (RemoteException re) {
-                        throw re.rethrowFromSystemServer();
-                    }
-                }
-                @Override
-                public boolean bypass(Integer query) {
-                    return query <= 0;
-                }
-            };
-
-
     /** @hide */
     public static final void invalidateUserSerialNumberCache() {
-        PropertyInvalidatedCache.invalidateCache(CACHE_KEY_USER_SERIAL_NUMBER_PROPERTY);
+        UserManagerCache.invalidateUserSerialNumber();
     }
 
     /**
      * Returns a serial number on this device for a given userId. User handles can be recycled
-     * when deleting and creating users, but serial numbers are not reused until the device is wiped.
+     * when deleting and creating users, but serial numbers are not reused until the device is
+     * wiped.
      * @param userId
      * @return a serial number associated with that user, or -1 if the userId is not valid.
      * @hide
      */
     @UnsupportedAppUsage
+    @CachedProperty(modsFlagOnOrNone = {})
     public int getUserSerialNumber(@UserIdInt int userId) {
         // Read only flag should is to fix early access to this API
         // cacheUserSerialNumber to be removed after the
@@ -6470,7 +6394,8 @@
             if (userId == UserHandle.USER_SYSTEM) {
                return UserHandle.USER_SERIAL_SYSTEM;
             }
-            return mUserSerialNumberCache.query(userId);
+            return ((UserManagerCache) mIpcDataCache).getUserSerialNumber(
+                    mService::getUserSerialNumber, userId);
         }
         try {
             return mService.getUserSerialNumber(userId);
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index a2c41c1..1e6f025 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -2350,6 +2350,21 @@
             "android.settings.ALL_APPS_NOTIFICATION_SETTINGS_FOR_REVIEW";
 
     /**
+     * Activity Action: Show the permission screen for allowing apps to post promoted notifications.
+     * <p>
+     *     Input: {@link #EXTRA_APP_PACKAGE}, the package to display.
+     * <p>
+     * In some cases, a matching Activity may not exist, so ensure you
+     * safeguard against this.
+     * <p>
+     * Output: Nothing.
+     */
+    @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_APP_NOTIFICATION_PROMOTION_SETTINGS
+            = "android.settings.APP_NOTIFICATION_PROMOTION_SETTINGS";
+
+    /**
      * Activity Action: Show notification settings for a single app.
      * <p>
      *     Input: {@link #EXTRA_APP_PACKAGE}, the package to display.
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index 314bf89..0dc9263 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -28,6 +28,7 @@
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.annotation.TestApi;
 import android.app.PendingIntent;
 import android.app.WindowConfiguration;
@@ -44,6 +45,7 @@
 import android.view.InsetsFrameProvider;
 import android.view.InsetsSource;
 import android.view.SurfaceControl;
+import android.view.WindowInsets;
 import android.view.WindowInsets.Type.InsetsType;
 
 import com.android.window.flags.Flags;
@@ -267,6 +269,23 @@
     }
 
     /**
+     * Sets whether the IME insets should be excluded by {@link com.android.server.wm.InsetsPolicy}.
+     * @hide
+     */
+    @SuppressLint("UnflaggedApi")
+    @NonNull
+    public WindowContainerTransaction setExcludeImeInsets(
+            @NonNull WindowContainerToken container, boolean exclude) {
+        final HierarchyOp hierarchyOp =
+                new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_SET_EXCLUDE_INSETS_TYPES)
+                        .setContainer(container.asBinder())
+                        .setExcludeInsetsTypes(exclude ? WindowInsets.Type.ime() : 0)
+                        .build();
+        mHierarchyOps.add(hierarchyOp);
+        return this;
+    }
+
+    /**
      * Sets whether a container or its children should be hidden. When {@code false}, the existing
      * visibility of the container applies, but when {@code true} the container will be forced
      * to be hidden.
@@ -1449,6 +1468,7 @@
         public static final int HIERARCHY_OP_TYPE_MOVE_PIP_ACTIVITY_TO_PINNED_TASK = 18;
         public static final int HIERARCHY_OP_TYPE_SET_IS_TRIMMABLE = 19;
         public static final int HIERARCHY_OP_TYPE_RESTORE_BACK_NAVIGATION = 20;
+        public static final int HIERARCHY_OP_TYPE_SET_EXCLUDE_INSETS_TYPES = 21;
 
         // The following key(s) are for use with mLaunchOptions:
         // When launching a task (eg. from recents), this is the taskId to be launched.
@@ -1512,6 +1532,8 @@
 
         private boolean mIsTrimmableFromRecents;
 
+        private @InsetsType int mExcludeInsetsTypes;
+
         public static HierarchyOp createForReparent(
                 @NonNull IBinder container, @Nullable IBinder reparent, boolean toTop) {
             return new HierarchyOp.Builder(HIERARCHY_OP_TYPE_REPARENT)
@@ -1649,6 +1671,7 @@
             mAlwaysOnTop = copy.mAlwaysOnTop;
             mReparentLeafTaskIfRelaunch = copy.mReparentLeafTaskIfRelaunch;
             mIsTrimmableFromRecents = copy.mIsTrimmableFromRecents;
+            mExcludeInsetsTypes = copy.mExcludeInsetsTypes;
         }
 
         protected HierarchyOp(Parcel in) {
@@ -1671,6 +1694,7 @@
             mAlwaysOnTop = in.readBoolean();
             mReparentLeafTaskIfRelaunch = in.readBoolean();
             mIsTrimmableFromRecents = in.readBoolean();
+            mExcludeInsetsTypes = in.readInt();
         }
 
         public int getType() {
@@ -1772,6 +1796,10 @@
             return mIsTrimmableFromRecents;
         }
 
+        public @InsetsType int getExcludeInsetsTypes() {
+            return mExcludeInsetsTypes;
+        }
+
         /** Gets a string representation of a hierarchy-op type. */
         public static String hopToString(int type) {
             switch (type) {
@@ -1795,6 +1823,7 @@
                     return "setReparentLeafTaskIfRelaunch";
                 case HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION:
                     return "addTaskFragmentOperation";
+                case HIERARCHY_OP_TYPE_SET_EXCLUDE_INSETS_TYPES: return "setExcludeInsetsTypes";
                 default: return "HOP(" + type + ")";
             }
         }
@@ -1868,6 +1897,11 @@
                     sb.append("fragmentToken= ").append(mContainer)
                             .append(" operation= ").append(mTaskFragmentOperation);
                     break;
+                case HIERARCHY_OP_TYPE_SET_EXCLUDE_INSETS_TYPES:
+                    sb.append("container= ").append(mContainer)
+                            .append(" mExcludeInsetsTypes= ")
+                            .append(WindowInsets.Type.toString(mExcludeInsetsTypes));
+                    break;
                 case HIERARCHY_OP_TYPE_SET_IS_TRIMMABLE:
                     sb.append("container= ").append(mContainer)
                             .append(" isTrimmable= ")
@@ -1903,6 +1937,7 @@
             dest.writeBoolean(mAlwaysOnTop);
             dest.writeBoolean(mReparentLeafTaskIfRelaunch);
             dest.writeBoolean(mIsTrimmableFromRecents);
+            dest.writeInt(mExcludeInsetsTypes);
         }
 
         @Override
@@ -1974,6 +2009,8 @@
 
             private boolean mIsTrimmableFromRecents;
 
+            private @InsetsType int mExcludeInsetsTypes;
+
             Builder(int type) {
                 mType = type;
             }
@@ -2069,6 +2106,11 @@
                 return this;
             }
 
+            Builder setExcludeInsetsTypes(@InsetsType int excludeInsetsTypes) {
+                mExcludeInsetsTypes = excludeInsetsTypes;
+                return this;
+            }
+
             HierarchyOp build() {
                 final HierarchyOp hierarchyOp = new HierarchyOp(mType);
                 hierarchyOp.mContainer = mContainer;
@@ -2093,6 +2135,7 @@
                 hierarchyOp.mIncludingParents = mIncludingParents;
                 hierarchyOp.mReparentLeafTaskIfRelaunch = mReparentLeafTaskIfRelaunch;
                 hierarchyOp.mIsTrimmableFromRecents = mIsTrimmableFromRecents;
+                hierarchyOp.mExcludeInsetsTypes = mExcludeInsetsTypes;
 
                 return hierarchyOp;
             }
diff --git a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
index 3eefe04..b16c237 100644
--- a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
+++ b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
@@ -119,7 +119,7 @@
             }
 
             @Override
-            protected DisplayMetrics getDisplayMetrics(int displayId, DisplayAdjustments daj) {
+            public DisplayMetrics getDisplayMetrics(int displayId, DisplayAdjustments daj) {
                 return mDisplayMetricsMap.get(displayId);
             }
         };
@@ -470,6 +470,48 @@
 
     @Test
     @SmallTest
+    public void testResourceConfigurationAppliedWhenOverrideDoesNotExist() {
+        final int width = 240;
+        final int height = 360;
+        final float densityDpi = mDisplayMetricsMap.get(Display.DEFAULT_DISPLAY).densityDpi;
+        final int widthDp = (int) (width / densityDpi + 0.5f);
+        final int heightDp = (int) (height / densityDpi + 0.5f);
+
+        final int overrideWidth = 480;
+        final int overrideHeight = 720;
+        final int overrideWidthDp = (int) (overrideWidth / densityDpi + 0.5f);
+        final int overrideHeightDp = (int) (height / densityDpi + 0.5f);
+
+        // The method to be tested is overridden for other tests to provide a setup environment.
+        // Create a new one for this test only.
+        final ResourcesManager resourcesManager = new ResourcesManager();
+
+        Configuration newConfig = new Configuration();
+        newConfig.windowConfiguration.setAppBounds(0, 0, width, height);
+        newConfig.screenWidthDp = widthDp;
+        newConfig.screenHeightDp = heightDp;
+        resourcesManager.applyConfigurationToResources(newConfig, null);
+
+        assertEquals(width, resourcesManager.getDisplayMetrics(Display.DEFAULT_DISPLAY,
+                DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS).widthPixels);
+        assertEquals(height, resourcesManager.getDisplayMetrics(Display.DEFAULT_DISPLAY,
+                DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS).heightPixels);
+
+        Configuration overrideConfig = new Configuration();
+        overrideConfig.windowConfiguration.setAppBounds(0, 0, overrideWidth, overrideHeight);
+        overrideConfig.screenWidthDp = overrideWidthDp;
+        overrideConfig.screenHeightDp = overrideHeightDp;
+
+        final DisplayAdjustments daj = new DisplayAdjustments(overrideConfig);
+
+        assertEquals(overrideWidth, resourcesManager.getDisplayMetrics(
+                Display.DEFAULT_DISPLAY, daj).widthPixels);
+        assertEquals(overrideHeight, resourcesManager.getDisplayMetrics(
+                Display.DEFAULT_DISPLAY, daj).heightPixels);
+    }
+
+    @Test
+    @SmallTest
     @RequiresFlagsEnabled(Flags.FLAG_REGISTER_RESOURCE_PATHS)
     @DisabledOnRavenwood(blockedBy = PackageManager.class)
     public void testNewResourcesWithOutdatedImplAfterResourcePathsRegistration()
diff --git a/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java b/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java
index 9887c27..af26bd0 100644
--- a/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java
+++ b/errorprone/java/com/google/errorprone/bugpatterns/android/RequiresPermissionChecker.java
@@ -130,10 +130,6 @@
                             .onDescendantOf("android.content.Context")
                             .withNameMatching(
                                     Pattern.compile("^send(Ordered|Sticky)?Broadcast.*AsUser.*$")));
-    private static final Matcher<ExpressionTree> SEND_PENDING_INTENT = methodInvocation(
-            instanceMethod()
-                    .onDescendantOf("android.app.PendingIntent")
-                    .named("send"));
 
     private static final Matcher<ExpressionTree> INTENT_SET_ACTION = methodInvocation(
             instanceMethod().onDescendantOf("android.content.Intent").named("setAction"));
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 0047ec5..38087c0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -157,6 +157,14 @@
         }
     }
 
+    private void dispatchImeRequested(int displayId, boolean isRequested) {
+        synchronized (mPositionProcessors) {
+            for (ImePositionProcessor pp : mPositionProcessors) {
+                pp.onImeRequested(displayId, isRequested);
+            }
+        }
+    }
+
     @ImePositionProcessor.ImeAnimationFlags
     private int dispatchStartPositioning(int displayId, int hiddenTop, int shownTop,
             boolean show, boolean isFloating, SurfaceControl.Transaction t) {
@@ -398,6 +406,8 @@
         public void setImeInputTargetRequestedVisibility(boolean visible) {
             if (android.view.inputmethod.Flags.refactorInsetsController()) {
                 mImeRequestedVisible = visible;
+                dispatchImeRequested(mDisplayId, mImeRequestedVisible);
+
                 // In the case that the IME becomes visible, but we have the control with leash
                 // already (e.g., when focussing an editText in activity B, while and editText in
                 // activity A is focussed), we will not get a call of #insetsControlChanged, and
@@ -446,6 +456,8 @@
             if (imeSource == null || mImeSourceControl == null) {
                 return;
             }
+            // TODO(b/353463205): For hide: this still has the statsToken from the previous show
+            //  request
             final var statsToken = mImeSourceControl.getImeStatsToken();
 
             startAnimation(show, forceRestart, statsToken);
@@ -706,6 +718,14 @@
         }
 
         /**
+         * Called when the IME was requested by an app
+         *
+         * @param isRequested {@code true} if the IME was requested to be visible
+         */
+        default void onImeRequested(int displayId, boolean isRequested) {
+        }
+
+        /**
          * Called when the IME position is starting to animate.
          *
          * @param hiddenTop  The y position of the top of the IME surface when it is hidden.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index 4b55fd0..83ffaf4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -1106,6 +1106,11 @@
         default void onDoubleTappedDivider() {
         }
 
+        /**
+         * Sets the excludedInsetsTypes for the IME in the root WindowContainer.
+         */
+        void setExcludeImeInsets(boolean exclude);
+
         /** Returns split position of the token. */
         @SplitPosition
         int getSplitItemPosition(WindowContainerToken token);
@@ -1305,6 +1310,14 @@
         }
 
         @Override
+        public void onImeRequested(int displayId, boolean isRequested) {
+            if (displayId != mDisplayId) return;
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "IME was set to requested=%s",
+                    isRequested);
+            mSplitLayoutHandler.setExcludeImeInsets(true);
+        }
+
+        @Override
         public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop,
                 boolean showing, boolean isFloating, SurfaceControl.Transaction t) {
             if (displayId != mDisplayId || !mInitialized) {
@@ -1356,6 +1369,12 @@
             setDividerInteractive(!mImeShown || !mHasImeFocus || isFloating, true,
                     "onImeStartPositioning");
 
+            if (android.view.inputmethod.Flags.refactorInsetsController()) {
+                if (mImeShown) {
+                    mSplitLayoutHandler.setExcludeImeInsets(false);
+                }
+            }
+
             return mTargetYOffset != mLastYOffset ? IME_ANIMATION_NO_ALPHA : 0;
         }
 
@@ -1374,6 +1393,16 @@
                     "Split IME animation ending, canceled=%b", cancel);
             onProgress(1.0f);
             mSplitLayoutHandler.onLayoutPositionChanging(SplitLayout.this);
+            if (android.view.inputmethod.Flags.refactorInsetsController()) {
+                if (!mImeShown) {
+                    // The IME hide animation is started immediately and at that point, the IME
+                    // insets are not yet set to hidden. Therefore only resetting the
+                    // excludedTypes at the end of the animation. Note: InsetsPolicy will only
+                    // set the IME height to zero, when it is visible. When it becomes invisible,
+                    // we dispatch the insets (the height there is zero as well)
+                    mSplitLayoutHandler.setExcludeImeInsets(false);
+                }
+            }
         }
 
         @Override
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 e8eb10c..e527c02 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
@@ -901,6 +901,23 @@
         setEnterInstanceId(instanceId);
     }
 
+
+    @Override
+    public void setExcludeImeInsets(boolean exclude) {
+        if (android.view.inputmethod.Flags.refactorInsetsController()) {
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+            if (mRootTaskInfo == null) {
+                ProtoLog.e(WM_SHELL_SPLIT_SCREEN, "setExcludeImeInsets: mRootTaskInfo is null");
+                return;
+            }
+            ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
+                    "setExcludeImeInsets: root taskId=%s exclude=%s",
+                    mRootTaskInfo.taskId, exclude);
+            wct.setExcludeImeInsets(mRootTaskInfo.token, exclude);
+            mTaskOrganizer.applyTransaction(wct);
+        }
+    }
+
     /**
      * Checks if either of the apps in the desired split launch is currently in Pip. If so, it will
      * launch the non-pipped app as a fullscreen app, otherwise no-op.
@@ -1717,6 +1734,7 @@
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         wct.reparent(mMainStage.mRootTaskInfo.token, mRootTaskInfo.token, true);
         wct.reparent(mSideStage.mRootTaskInfo.token, mRootTaskInfo.token, true);
+
         // Make the stages adjacent to each other so they occlude what's behind them.
         wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
         setRootForceTranslucent(true, wct);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
index 30d7245..e61929f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
@@ -141,10 +141,13 @@
             pipHandler.setEnterAnimationType(ANIM_TYPE_ALPHA);
             pipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction,
                     finishCB);
+            // make a new finishTransaction because pip's startEnterAnimation "consumes" it so
+            // we need a separate one to send over to launcher.
+            SurfaceControl.Transaction otherFinishT = new SurfaceControl.Transaction();
             // Dispatch the rest of the transition normally. This will most-likely be taken by
             // recents or default handler.
             mixed.mLeftoversHandler = player.dispatchTransition(mixed.mTransition, everythingElse,
-                    otherStartT, finishTransaction, finishCB, mixedHandler);
+                    otherStartT, otherFinishT, finishCB, mixedHandler);
         } else {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  Not leaving split, so just "
                     + "forward animation to Pip-Handler.");
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp b/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp
index 29a9f10..7412c1d 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/Android.bp
@@ -15,7 +15,7 @@
 //
 
 package {
-    default_team: "trendy_team_app_compat",
+    default_team: "trendy_team_lse_app_compat",
     // See: http://go/android-license-faq
     // A large-scale-change added 'default_applicable_licenses' to import
     // all of the 'license_kinds' from "frameworks_base_license"
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
index d03d779..d1bf6ac 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
@@ -183,12 +183,6 @@
     }
 
     /** {@inheritDoc} */
-    @FlakyTest(bugId = 312446524)
-    @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
-
-    /** {@inheritDoc} */
     @Test
     @FlakyTest(bugId = 336510055)
     override fun entireScreenCovered() {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index f8f0db9..0373bbd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -26,6 +26,8 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -34,6 +36,12 @@
 
 import android.graphics.Insets;
 import android.graphics.Point;
+import android.os.Looper;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.IWindowManager;
 import android.view.InsetsSource;
 import android.view.InsetsSourceControl;
 import android.view.InsetsState;
@@ -47,6 +55,7 @@
 import com.android.wm.shell.sysui.ShellInit;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -61,11 +70,16 @@
  */
 @SmallTest
 public class DisplayImeControllerTest extends ShellTestCase {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
     @Mock
     private SurfaceControl.Transaction mT;
     @Mock
     private ShellInit mShellInit;
+    @Mock
+    private IWindowManager mWm;
+    private DisplayImeController mDisplayImeController;
     private DisplayImeController.PerDisplay mPerDisplay;
     private Executor mExecutor;
 
@@ -73,7 +87,8 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         mExecutor = spy(Runnable::run);
-        mPerDisplay = new DisplayImeController(null, mShellInit, null, null, new TransactionPool() {
+        mDisplayImeController = new DisplayImeController(mWm, mShellInit, null, null,
+                new TransactionPool() {
             @Override
             public SurfaceControl.Transaction acquire() {
                 return mT;
@@ -84,8 +99,10 @@
             }
         }, mExecutor) {
             @Override
-            void removeImeSurface(int displayId) { }
-        }.new PerDisplay(DEFAULT_DISPLAY, ROTATION_0);
+            void removeImeSurface(int displayId) {
+            }
+        };
+        mPerDisplay = mDisplayImeController.new PerDisplay(DEFAULT_DISPLAY, ROTATION_0);
     }
 
     @Test
@@ -95,12 +112,14 @@
 
     @Test
     public void insetsControlChanged_schedulesNoWorkOnExecutor() {
+        Looper.prepare();
         mPerDisplay.insetsControlChanged(insetsStateWithIme(false), insetsSourceControl());
         verifyZeroInteractions(mExecutor);
     }
 
     @Test
     public void insetsChanged_schedulesNoWorkOnExecutor() {
+        Looper.prepare();
         mPerDisplay.insetsChanged(insetsStateWithIme(false));
         verifyZeroInteractions(mExecutor);
     }
@@ -117,7 +136,10 @@
         verifyZeroInteractions(mExecutor);
     }
 
+    // With the refactor, the control's isInitiallyVisible is used to apply to the IME, therefore
+    // this test is obsolete
     @Test
+    @RequiresFlagsDisabled(android.view.inputmethod.Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
     public void reappliesVisibilityToChangedLeash() {
         verifyZeroInteractions(mT);
         mPerDisplay.mImeShowing = false;
@@ -136,6 +158,7 @@
 
     @Test
     public void insetsControlChanged_updateImeSourceControl() {
+        Looper.prepare();
         mPerDisplay.insetsControlChanged(insetsStateWithIme(false), insetsSourceControl());
         assertNotNull(mPerDisplay.mImeSourceControl);
 
@@ -143,6 +166,19 @@
         assertNull(mPerDisplay.mImeSourceControl);
     }
 
+    @Test
+    @RequiresFlagsEnabled(android.view.inputmethod.Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
+    public void setImeInputTargetRequestedVisibility_invokeOnImeRequested() {
+        var mockPp = mock(DisplayImeController.ImePositionProcessor.class);
+        mDisplayImeController.addPositionProcessor(mockPp);
+
+        mPerDisplay.setImeInputTargetRequestedVisibility(true);
+        verify(mockPp).onImeRequested(anyInt(), eq(true));
+
+        mPerDisplay.setImeInputTargetRequestedVisibility(false);
+        verify(mockPp).onImeRequested(anyInt(), eq(false));
+    }
+
     private InsetsSourceControl[] insetsSourceControl() {
         return new InsetsSourceControl[]{
                 new InsetsSourceControl(
diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig
index cc9a97c..6a7e693 100644
--- a/nfc/java/android/nfc/flags.aconfig
+++ b/nfc/java/android/nfc/flags.aconfig
@@ -157,3 +157,11 @@
     description: "Enable EUICC card emulation"
     bug: "321314635"
 }
+
+flag {
+    name: "nfc_state_change_security_log_event_enabled"
+    is_exported: true
+    namespace: "nfc"
+    description: "Enabling security log for nfc state change"
+    bug: "319934052"
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
index f5d01d7..907c39d 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
@@ -944,9 +944,26 @@
                 }
 
                 override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
-                    startController.onTransitionAnimationEnd(isExpandingFullyAbove)
-                    endController.onTransitionAnimationEnd(isExpandingFullyAbove)
-                    onLaunchAnimationEnd()
+                    // onLaunchAnimationEnd is called by an Animator at the end of the animation,
+                    // on a Choreographer animation tick. The following calls will move the animated
+                    // content from the dialog overlay back to its original position, and this
+                    // change must be reflected in the next frame given that we then sync the next
+                    // frame of both the content and dialog ViewRoots. However, in case that content
+                    // is rendered by Compose, whose compositions are also scheduled on a
+                    // Choreographer frame, any state change made *right now* won't be reflected in
+                    // the next frame given that a Choreographer frame can't schedule another and
+                    // have it happen in the same frame. So we post the forwarded calls to
+                    // [Controller.onLaunchAnimationEnd], leaving this Choreographer frame, ensuring
+                    // that the move of the content back to its original window will be reflected in
+                    // the next frame right after [onLaunchAnimationEnd] is called.
+                    //
+                    // TODO(b/330672236): Move this to TransitionAnimator.
+                    dialog.context.mainExecutor.execute {
+                        startController.onTransitionAnimationEnd(isExpandingFullyAbove)
+                        endController.onTransitionAnimationEnd(isExpandingFullyAbove)
+
+                        onLaunchAnimationEnd()
+                    }
                 }
 
                 override fun onTransitionAnimationProgress(
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index 859fc4e0..fc4cf1d 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -379,26 +379,13 @@
                         Log.d(TAG, "Animation ended")
                     }
 
-                    // onAnimationEnd is called at the end of the animation, on a Choreographer
-                    // animation tick. During dialog launches, the following calls will move the
-                    // animated content from the dialog overlay back to its original position, and
-                    // this change must be reflected in the next frame given that we then sync the
-                    // next frame of both the content and dialog ViewRoots. During SysUI activity
-                    // launches, we will instantly collapse the shade at the end of the transition.
-                    // However, if those are rendered by Compose, whose compositions are also
-                    // scheduled on a Choreographer frame, any state change made *right now* won't
-                    // be reflected in the next frame given that a Choreographer frame can't
-                    // schedule another and have it happen in the same frame. So we post the
-                    // forwarded calls to [Controller.onLaunchAnimationEnd] in the main executor,
-                    // leaving this Choreographer frame, ensuring that any state change applied by
-                    // onTransitionAnimationEnd() will be reflected in the same frame.
-                    mainExecutor.execute {
-                        controller.onTransitionAnimationEnd(isExpandingFullyAbove)
-                        transitionContainerOverlay.remove(windowBackgroundLayer)
+                    // TODO(b/330672236): Post this to the main thread instead so that it does not
+                    // flicker with Flexiglass enabled.
+                    controller.onTransitionAnimationEnd(isExpandingFullyAbove)
+                    transitionContainerOverlay.remove(windowBackgroundLayer)
 
-                        if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
-                            openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
-                        }
+                    if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
+                        openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
                     }
                 }
             }
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Bounceable.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Bounceable.kt
new file mode 100644
index 0000000..3f2f84b
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Bounceable.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.compose.animation
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import kotlin.math.roundToInt
+
+/** A component that can bounce in one dimension, for instance when it is tapped. */
+interface Bounceable {
+    val bounce: Dp
+}
+
+/**
+ * Bounce a composable in the given [orientation] when this [bounceable], the [previousBounceable]
+ * or [nextBounceable] is bouncing.
+ *
+ * Important: This modifier should be used on composables that have a fixed size in [orientation],
+ * i.e. they should be placed *after* modifiers like Modifier.fillMaxWidth() or Modifier.height().
+ *
+ * @param bounceable the [Bounceable] associated to the current composable that will make this
+ *   composable size grow when bouncing.
+ * @param previousBounceable the [Bounceable] associated to the previous composable in [orientation]
+ *   that will make this composable shrink when bouncing.
+ * @param nextBounceable the [Bounceable] associated to the next composable in [orientation] that
+ *   will make this composable shrink when bouncing.
+ * @param orientation the orientation in which this bounceable should grow/shrink.
+ * @param bounceEnd whether this bounceable should bounce on the end (right in LTR layouts, left in
+ *   RTL layouts) side. This can be used for grids for which the last item does not align perfectly
+ *   with the end of the grid.
+ */
+fun Modifier.bounceable(
+    bounceable: Bounceable,
+    previousBounceable: Bounceable?,
+    nextBounceable: Bounceable?,
+    orientation: Orientation,
+    bounceEnd: Boolean = nextBounceable != null,
+): Modifier {
+    return layout { measurable, constraints ->
+        // The constraints in the orientation should be fixed, otherwise there is no way to know
+        // what the size of our child node will be without this animation code.
+        checkFixedSize(constraints, orientation)
+
+        var sizePrevious = 0f
+        var sizeNext = 0f
+
+        if (previousBounceable != null) {
+            sizePrevious += bounceable.bounce.toPx() - previousBounceable.bounce.toPx()
+        }
+
+        if (nextBounceable != null) {
+            sizeNext += bounceable.bounce.toPx() - nextBounceable.bounce.toPx()
+        } else if (bounceEnd) {
+            sizeNext += bounceable.bounce.toPx()
+        }
+
+        when (orientation) {
+            Orientation.Horizontal -> {
+                val idleWidth = constraints.maxWidth
+                val animatedWidth = (idleWidth + sizePrevious + sizeNext).roundToInt()
+                val animatedConstraints =
+                    constraints.copy(minWidth = animatedWidth, maxWidth = animatedWidth)
+
+                val placeable = measurable.measure(animatedConstraints)
+
+                // Important: we still place the element using the idle size coming from the
+                // constraints, otherwise the parent will automatically center this node given the
+                // size that it expects us to be. This allows us to then place the element where we
+                // want it to be.
+                layout(idleWidth, placeable.height) {
+                    placeable.placeRelative(-sizePrevious.roundToInt(), 0)
+                }
+            }
+            Orientation.Vertical -> {
+                val idleHeight = constraints.maxHeight
+                val animatedHeight = (idleHeight + sizePrevious + sizeNext).roundToInt()
+                val animatedConstraints =
+                    constraints.copy(minHeight = animatedHeight, maxHeight = animatedHeight)
+
+                val placeable = measurable.measure(animatedConstraints)
+                layout(placeable.width, idleHeight) {
+                    placeable.placeRelative(0, -sizePrevious.roundToInt())
+                }
+            }
+        }
+    }
+}
+
+private fun checkFixedSize(constraints: Constraints, orientation: Orientation) {
+    when (orientation) {
+        Orientation.Horizontal -> {
+            check(constraints.hasFixedWidth) {
+                "Modifier.bounceable() should receive a fixed width from its parent. Make sure " +
+                    "that it is used *after* a fixed-width Modifier in the horizontal axis (like" +
+                    " Modifier.fillMaxWidth() or Modifier.width())."
+            }
+        }
+        Orientation.Vertical -> {
+            check(constraints.hasFixedHeight) {
+                "Modifier.bounceable() should receive a fixed height from its parent. Make sure " +
+                    "that it is used *after* a fixed-height Modifier in the vertical axis (like" +
+                    " Modifier.fillMaxHeight() or Modifier.height())."
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/BounceableTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/BounceableTest.kt
new file mode 100644
index 0000000..335e9f8
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/BounceableTest.kt
@@ -0,0 +1,212 @@
+/*
+ * 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.compose.animation
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.times
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class BounceableTest {
+    @get:Rule val rule = createComposeRule()
+
+    @Test
+    fun bounceable_horizontal() {
+        var bounceables by mutableStateOf(List(4) { bounceable(0.dp) })
+
+        rule.setContent {
+            Row(Modifier.size(100.dp, 50.dp)) {
+                repeat(bounceables.size) { i ->
+                    Box(
+                        Modifier.weight(1f)
+                            .fillMaxHeight()
+                            .bounceable(bounceables, i, orientation = Orientation.Horizontal)
+                    )
+                }
+            }
+        }
+
+        // All bounceables have a width of (100dp / bounceables.size) = 25dp and height of 50dp.
+        repeat(bounceables.size) { i ->
+            rule
+                .onNodeWithTag(bounceableTag(i))
+                .assertWidthIsEqualTo(25.dp)
+                .assertHeightIsEqualTo(50.dp)
+                .assertPositionInRootIsEqualTo(i * 25.dp, 0.dp)
+        }
+
+        // If all bounceables have the same bounce, it's the same as if they didn't have any.
+        bounceables = List(4) { bounceable(10.dp) }
+        repeat(bounceables.size) { i ->
+            rule
+                .onNodeWithTag(bounceableTag(i))
+                .assertWidthIsEqualTo(25.dp)
+                .assertHeightIsEqualTo(50.dp)
+                .assertPositionInRootIsEqualTo(i * 25.dp, 0.dp)
+        }
+
+        // Bounce the first and third one.
+        bounceables =
+            listOf(
+                bounceable(bounce = 5.dp),
+                bounceable(bounce = 0.dp),
+                bounceable(bounce = 10.dp),
+                bounceable(bounce = 0.dp),
+            )
+
+        // First one has a width of 25dp + 5dp, located in (0, 0).
+        rule
+            .onNodeWithTag(bounceableTag(0))
+            .assertWidthIsEqualTo(30.dp)
+            .assertHeightIsEqualTo(50.dp)
+            .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+
+        // Second one has a width of 25dp - 5dp - 10dp, located in (30, 0).
+        rule
+            .onNodeWithTag(bounceableTag(1))
+            .assertWidthIsEqualTo(10.dp)
+            .assertHeightIsEqualTo(50.dp)
+            .assertPositionInRootIsEqualTo(30.dp, 0.dp)
+
+        // Third one has a width of 25 + 2 * 10dp, located in (40, 0).
+        rule
+            .onNodeWithTag(bounceableTag(2))
+            .assertWidthIsEqualTo(45.dp)
+            .assertHeightIsEqualTo(50.dp)
+            .assertPositionInRootIsEqualTo(40.dp, 0.dp)
+
+        // First one has a width of 25dp - 10dp, located in (85, 0).
+        rule
+            .onNodeWithTag(bounceableTag(3))
+            .assertWidthIsEqualTo(15.dp)
+            .assertHeightIsEqualTo(50.dp)
+            .assertPositionInRootIsEqualTo(85.dp, 0.dp)
+    }
+
+    @Test
+    fun bounceable_vertical() {
+        var bounceables by mutableStateOf(List(4) { bounceable(0.dp) })
+
+        rule.setContent {
+            Column(Modifier.size(50.dp, 100.dp)) {
+                repeat(bounceables.size) { i ->
+                    Box(
+                        Modifier.weight(1f)
+                            .fillMaxWidth()
+                            .bounceable(bounceables, i, Orientation.Vertical)
+                    )
+                }
+            }
+        }
+
+        // All bounceables have a height of (100dp / bounceables.size) = 25dp and width of 50dp.
+        repeat(bounceables.size) { i ->
+            rule
+                .onNodeWithTag(bounceableTag(i))
+                .assertWidthIsEqualTo(50.dp)
+                .assertHeightIsEqualTo(25.dp)
+                .assertPositionInRootIsEqualTo(0.dp, i * 25.dp)
+        }
+
+        // If all bounceables have the same bounce, it's the same as if they didn't have any.
+        bounceables = List(4) { bounceable(10.dp) }
+        repeat(bounceables.size) { i ->
+            rule
+                .onNodeWithTag(bounceableTag(i))
+                .assertWidthIsEqualTo(50.dp)
+                .assertHeightIsEqualTo(25.dp)
+                .assertPositionInRootIsEqualTo(0.dp, i * 25.dp)
+        }
+
+        // Bounce the first and third one.
+        bounceables =
+            listOf(
+                bounceable(bounce = 5.dp),
+                bounceable(bounce = 0.dp),
+                bounceable(bounce = 10.dp),
+                bounceable(bounce = 0.dp),
+            )
+
+        // First one has a height of 25dp + 5dp, located in (0, 0).
+        rule
+            .onNodeWithTag(bounceableTag(0))
+            .assertWidthIsEqualTo(50.dp)
+            .assertHeightIsEqualTo(30.dp)
+            .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+
+        // Second one has a height of 25dp - 5dp - 10dp, located in (0, 30).
+        rule
+            .onNodeWithTag(bounceableTag(1))
+            .assertWidthIsEqualTo(50.dp)
+            .assertHeightIsEqualTo(10.dp)
+            .assertPositionInRootIsEqualTo(0.dp, 30.dp)
+
+        // Third one has a height of 25 + 2 * 10dp, located in (0, 40).
+        rule
+            .onNodeWithTag(bounceableTag(2))
+            .assertWidthIsEqualTo(50.dp)
+            .assertHeightIsEqualTo(45.dp)
+            .assertPositionInRootIsEqualTo(0.dp, 40.dp)
+
+        // First one has a height of 25dp - 10dp, located in (0, 85).
+        rule
+            .onNodeWithTag(bounceableTag(3))
+            .assertWidthIsEqualTo(50.dp)
+            .assertHeightIsEqualTo(15.dp)
+            .assertPositionInRootIsEqualTo(0.dp, 85.dp)
+    }
+
+    private fun bounceable(bounce: Dp): Bounceable {
+        return object : Bounceable {
+            override val bounce: Dp = bounce
+        }
+    }
+
+    private fun Modifier.bounceable(
+        bounceables: List<Bounceable>,
+        i: Int,
+        orientation: Orientation,
+    ): Modifier {
+        val previous = if (i > 0) bounceables[i - 1] else null
+        val next = if (i < bounceables.lastIndex) bounceables[i + 1] else null
+        return this.bounceable(bounceables[i], previous, next, orientation)
+            .testTag(bounceableTag(i))
+    }
+
+    private fun bounceableTag(i: Int) = "bounceable$i"
+}
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/DreamSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/DreamSceneModule.kt
new file mode 100644
index 0000000..2f82369
--- /dev/null
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/DreamSceneModule.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene
+
+import com.android.systemui.dream.ui.composable.DreamScene
+import com.android.systemui.scene.ui.composable.Scene
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+
+@Module
+interface DreamSceneModule {
+    @Binds @IntoSet fun dreamScene(scene: DreamScene): Scene
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
new file mode 100644
index 0000000..fda46b8
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResizeableItemFrame.kt
@@ -0,0 +1,242 @@
+/*
+ * 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.communal.ui.compose
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.gestures.AnchoredDraggableState
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.anchoredDraggable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastIsFinite
+import com.android.compose.theme.LocalAndroidColorScheme
+import com.android.systemui.communal.ui.viewmodel.DragHandle
+import com.android.systemui.communal.ui.viewmodel.ResizeInfo
+import com.android.systemui.communal.ui.viewmodel.ResizeableItemFrameViewModel
+import com.android.systemui.lifecycle.rememberViewModel
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNotNull
+
+@Composable
+private fun UpdateGridLayoutInfo(
+    viewModel: ResizeableItemFrameViewModel,
+    index: Int,
+    gridState: LazyGridState,
+    minItemSpan: Int,
+    gridContentPadding: PaddingValues,
+    verticalArrangement: Arrangement.Vertical,
+) {
+    val density = LocalDensity.current
+    LaunchedEffect(
+        density,
+        viewModel,
+        index,
+        gridState,
+        minItemSpan,
+        gridContentPadding,
+        verticalArrangement,
+    ) {
+        val verticalItemSpacingPx = with(density) { verticalArrangement.spacing.toPx() }
+        val verticalContentPaddingPx =
+            with(density) {
+                (gridContentPadding.calculateTopPadding() +
+                        gridContentPadding.calculateBottomPadding())
+                    .toPx()
+            }
+
+        combine(
+                snapshotFlow { gridState.layoutInfo.maxSpan },
+                snapshotFlow { gridState.layoutInfo.viewportSize.height },
+                snapshotFlow {
+                        gridState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }
+                    }
+                    .filterNotNull(),
+                ::Triple,
+            )
+            .collectLatest { (maxItemSpan, viewportHeightPx, itemInfo) ->
+                viewModel.setGridLayoutInfo(
+                    verticalItemSpacingPx,
+                    verticalContentPaddingPx,
+                    viewportHeightPx,
+                    maxItemSpan,
+                    minItemSpan,
+                    itemInfo.row,
+                    itemInfo.span,
+                )
+            }
+    }
+}
+
+@Composable
+private fun BoxScope.DragHandle(
+    handle: DragHandle,
+    dragState: AnchoredDraggableState<Int>,
+    outlinePadding: Dp,
+    brush: Brush,
+    alpha: () -> Float,
+    modifier: Modifier = Modifier,
+) {
+    val directionalModifier = if (handle == DragHandle.TOP) -1 else 1
+    val alignment = if (handle == DragHandle.TOP) Alignment.TopCenter else Alignment.BottomCenter
+    Box(
+        modifier
+            .align(alignment)
+            .graphicsLayer {
+                translationY =
+                    directionalModifier * (size.height / 2 + outlinePadding.toPx()) +
+                        (dragState.offset.takeIf { it.fastIsFinite() } ?: 0f)
+            }
+            .anchoredDraggable(dragState, Orientation.Vertical)
+    ) {
+        Canvas(modifier = Modifier.fillMaxSize()) {
+            if (dragState.anchors.size > 1) {
+                drawCircle(
+                    brush = brush,
+                    radius = outlinePadding.toPx(),
+                    center = Offset(size.width / 2, size.height / 2),
+                    alpha = alpha(),
+                )
+            }
+        }
+    }
+}
+
+/**
+ * Draws a frame around the content with drag handles on the top and bottom of the content.
+ *
+ * @param index The index of this item in the [LazyGridState].
+ * @param gridState The [LazyGridState] for the grid containing this item.
+ * @param minItemSpan The minimum span that an item may occupy. Items are resized in multiples of
+ *   this span.
+ * @param gridContentPadding The content padding used for the grid, needed for determining offsets.
+ * @param verticalArrangement The vertical arrangement of the grid items.
+ * @param modifier Optional modifier to apply to the frame.
+ * @param enabled Whether resizing is enabled.
+ * @param outlinePadding The padding to apply around the entire frame, in [Dp]
+ * @param outlineColor Optional color to make the outline around the content.
+ * @param cornerRadius Optional radius to give to the outline around the content.
+ * @param strokeWidth Optional stroke width to draw the outline with.
+ * @param alpha Optional function to provide an alpha value for the outline. Can be used to fade the
+ *   outline in and out. This is wrapped in a function for performance, as the value is only
+ *   accessed during the draw phase.
+ * @param onResize Optional callback which gets executed when the item is resized to a new span.
+ * @param content The content to draw inside the frame.
+ */
+@Composable
+fun ResizableItemFrame(
+    index: Int,
+    gridState: LazyGridState,
+    minItemSpan: Int,
+    gridContentPadding: PaddingValues,
+    verticalArrangement: Arrangement.Vertical,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    outlinePadding: Dp = 8.dp,
+    outlineColor: Color = LocalAndroidColorScheme.current.primary,
+    cornerRadius: Dp = 37.dp,
+    strokeWidth: Dp = 3.dp,
+    alpha: () -> Float = { 1f },
+    onResize: (info: ResizeInfo) -> Unit = {},
+    content: @Composable () -> Unit,
+) {
+    val brush = SolidColor(outlineColor)
+    val viewModel =
+        rememberViewModel(traceName = "ResizeableItemFrame.viewModel") {
+            ResizeableItemFrameViewModel()
+        }
+
+    val dragHandleHeight = verticalArrangement.spacing - outlinePadding * 2
+
+    // Draw content surrounded by drag handles at top and bottom. Allow drag handles
+    // to overlap content.
+    Box(modifier) {
+        content()
+
+        if (enabled) {
+            DragHandle(
+                handle = DragHandle.TOP,
+                dragState = viewModel.topDragState,
+                outlinePadding = outlinePadding,
+                brush = brush,
+                alpha = alpha,
+                modifier = Modifier.fillMaxWidth().height(dragHandleHeight),
+            )
+
+            DragHandle(
+                handle = DragHandle.BOTTOM,
+                dragState = viewModel.bottomDragState,
+                outlinePadding = outlinePadding,
+                brush = brush,
+                alpha = alpha,
+                modifier = Modifier.fillMaxWidth().height(dragHandleHeight),
+            )
+
+            // Draw outline around the element.
+            Canvas(modifier = Modifier.matchParentSize()) {
+                val paddingPx = outlinePadding.toPx()
+                val topOffset = viewModel.topDragState.offset.takeIf { it.fastIsFinite() } ?: 0f
+                val bottomOffset =
+                    viewModel.bottomDragState.offset.takeIf { it.fastIsFinite() } ?: 0f
+                drawRoundRect(
+                    brush,
+                    alpha = alpha(),
+                    topLeft = Offset(-paddingPx, topOffset + -paddingPx),
+                    size =
+                        Size(
+                            width = size.width + paddingPx * 2,
+                            height = -topOffset + bottomOffset + size.height + paddingPx * 2,
+                        ),
+                    cornerRadius = CornerRadius(cornerRadius.toPx()),
+                    style = Stroke(width = strokeWidth.toPx()),
+                )
+            }
+
+            UpdateGridLayoutInfo(
+                viewModel,
+                index,
+                gridState,
+                minItemSpan,
+                gridContentPadding,
+                verticalArrangement,
+            )
+            LaunchedEffect(viewModel) { viewModel.resizeInfo.collectLatest(onResize) }
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/dream/ui/composable/DreamScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/dream/ui/composable/DreamScene.kt
new file mode 100644
index 0000000..f4374c6
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/dream/ui/composable/DreamScene.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.dream.ui.composable
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dreams.ui.viewmodel.DreamUserActionsViewModel
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.composable.Scene
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** The dream scene shows when a dream activity is showing. */
+@SysUISingleton
+class DreamScene
+@Inject
+constructor(private val actionsViewModelFactory: DreamUserActionsViewModel.Factory) :
+    ExclusiveActivatable(), Scene {
+    override val key = Scenes.Dream
+
+    private val actionsViewModel: DreamUserActionsViewModel by lazy {
+        actionsViewModelFactory.create()
+    }
+
+    override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions
+
+    override suspend fun onActivated(): Nothing {
+        actionsViewModel.activate()
+    }
+
+    @Composable
+    override fun SceneScope.Content(modifier: Modifier) {
+        Box(modifier = modifier.fillMaxSize()) {
+            // Render a sleep emoji to make the scene appear visible.
+            Text(
+                modifier = Modifier.padding(16.dp).align(Alignment.BottomStart),
+                text = "\uD83D\uDCA4",
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index e27f46b..d75a776 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -417,17 +417,26 @@
                     .navigationBarsPadding()
                     .padding(horizontal = shadeHorizontalPadding),
         )
+
+        // The minimum possible value for the top of the notification stack. In other words: how
+        // high is the notification stack allowed to get when the scene is at rest. It may still be
+        // translated farther upwards by a transition animation but, at rest, the top edge of its
+        // bounds must be limited to be at or below this value.
+        //
+        // A 1 pixel is added to compensate for any kind of rounding errors to make sure 100% that
+        // the notification stack is entirely "below" the entire screen.
+        val minNotificationStackTop = screenHeight.roundToInt() + 1
         NotificationScrollingStack(
             shadeSession = shadeSession,
             stackScrollView = notificationStackScrollView,
             viewModel = notificationsPlaceholderViewModel,
-            maxScrimTop = { screenHeight },
+            maxScrimTop = { minNotificationStackTop.toFloat() },
             shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim,
             shouldIncludeHeadsUpSpace = false,
             supportNestedScrolling = true,
             modifier =
                 Modifier.fillMaxWidth()
-                    .offset { IntOffset(x = 0, y = screenHeight.roundToInt()) }
+                    .offset { IntOffset(x = 0, y = minNotificationStackTop) }
                     .padding(horizontal = shadeHorizontalPadding),
         )
         NotificationStackCutoffGuideline(
@@ -436,7 +445,7 @@
             modifier =
                 Modifier.align(Alignment.BottomCenter)
                     .navigationBarsPadding()
-                    .offset { IntOffset(x = 0, y = screenHeight.roundToInt()) }
+                    .offset { IntOffset(x = 0, y = minNotificationStackTop) }
                     .padding(horizontal = shadeHorizontalPadding),
         )
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index 340ac32..8728521 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -15,11 +15,13 @@
 import com.android.systemui.scene.ui.composable.transitions.bouncerToLockscreenPreview
 import com.android.systemui.scene.ui.composable.transitions.communalToBouncerTransition
 import com.android.systemui.scene.ui.composable.transitions.communalToShadeTransition
+import com.android.systemui.scene.ui.composable.transitions.dreamToGoneTransition
 import com.android.systemui.scene.ui.composable.transitions.goneToQuickSettingsTransition
 import com.android.systemui.scene.ui.composable.transitions.goneToShadeTransition
 import com.android.systemui.scene.ui.composable.transitions.goneToSplitShadeTransition
 import com.android.systemui.scene.ui.composable.transitions.lockscreenToBouncerTransition
 import com.android.systemui.scene.ui.composable.transitions.lockscreenToCommunalTransition
+import com.android.systemui.scene.ui.composable.transitions.lockscreenToDreamTransition
 import com.android.systemui.scene.ui.composable.transitions.lockscreenToGoneTransition
 import com.android.systemui.scene.ui.composable.transitions.lockscreenToQuickSettingsTransition
 import com.android.systemui.scene.ui.composable.transitions.lockscreenToShadeTransition
@@ -52,6 +54,7 @@
     // Scene transitions
 
     from(Scenes.Bouncer, to = Scenes.Gone) { bouncerToGoneTransition() }
+    from(Scenes.Dream, to = Scenes.Gone) { dreamToGoneTransition() }
     from(Scenes.Gone, to = Scenes.Shade) { goneToShadeTransition() }
     from(Scenes.Gone, to = Scenes.Shade, key = ToSplitShade) { goneToSplitShadeTransition() }
     from(Scenes.Gone, to = Scenes.Shade, key = SlightlyFasterShadeCollapse) {
@@ -72,6 +75,7 @@
         lockscreenToBouncerTransition()
     }
     from(Scenes.Lockscreen, to = Scenes.Communal) { lockscreenToCommunalTransition() }
+    from(Scenes.Lockscreen, to = Scenes.Dream) { lockscreenToDreamTransition() }
     from(Scenes.Lockscreen, to = Scenes.Shade) { lockscreenToShadeTransition() }
     from(Scenes.Lockscreen, to = Scenes.Shade, key = ToSplitShade) {
         lockscreenToSplitShadeTransition()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToGoneTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToGoneTransition.kt
new file mode 100644
index 0000000..60dc4c05
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromDreamToGoneTransition.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.tween
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.scene.shared.model.Scenes
+
+fun TransitionBuilder.dreamToGoneTransition() {
+    spec = tween(durationMillis = 1000)
+
+    fade(Scenes.Dream.rootElementKey)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToDreamTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToDreamTransition.kt
new file mode 100644
index 0000000..7092e3f
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToDreamTransition.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.tween
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.scene.shared.model.Scenes
+
+fun TransitionBuilder.lockscreenToDreamTransition() {
+    spec = tween(durationMillis = 1000)
+
+    fade(Scenes.Lockscreen.rootElementKey)
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 94d3b2c..176824f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -61,6 +61,7 @@
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.statusbar.policy.UserSwitcherController.UserSwitchCallback;
 import com.android.systemui.user.data.source.UserRecord;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.settings.GlobalSettings;
@@ -70,6 +71,8 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
@@ -96,6 +99,9 @@
     private UserSwitcherController mUserSwitcherController;
     @Mock
     private FalsingA11yDelegate mFalsingA11yDelegate;
+    @Captor
+    private ArgumentCaptor<UserSwitchCallback> mUserSwitchCallbackCaptor =
+            ArgumentCaptor.forClass(UserSwitchCallback.class);
 
     private KeyguardSecurityContainer mKeyguardSecurityContainer;
     private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
@@ -360,6 +366,18 @@
     }
 
     @Test
+    public void goingOutOfUserSwitcherRemovesCallback() {
+        // WHEN UserSwitcherViewMode is initialized
+        setupUserSwitcher();
+        verify(mUserSwitcherController).addUserSwitchCallback(mUserSwitchCallbackCaptor.capture());
+
+        // Back to default mode, as SIM PIN would be
+        initMode(MODE_DEFAULT);
+        verify(mUserSwitcherController).removeUserSwitchCallback(
+                mUserSwitchCallbackCaptor.getValue());
+    }
+
+    @Test
     public void testOnDensityOrFontScaleChanged() {
         setupUserSwitcher();
         View oldUserSwitcher = mKeyguardSecurityContainer.findViewById(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt
new file mode 100644
index 0000000..e1946fc
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt
@@ -0,0 +1,323 @@
+/*
+ * 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.communal.ui.viewmodel
+
+import androidx.compose.foundation.gestures.DraggableAnchors
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ResizeableItemFrameViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val underTest = kosmos.resizeableItemFrameViewModel
+
+    /** Total viewport height of the entire grid */
+    private val viewportHeightPx = 100
+    /** Total amount of vertical padding around the viewport */
+    private val verticalContentPaddingPx = 20f
+
+    private val singleSpanGrid =
+        GridLayout(
+            verticalItemSpacingPx = 10f,
+            verticalContentPaddingPx = verticalContentPaddingPx,
+            viewportHeightPx = viewportHeightPx,
+            maxItemSpan = 1,
+            minItemSpan = 1,
+            currentSpan = 1,
+            currentRow = 0,
+        )
+
+    @Before
+    fun setUp() {
+        underTest.activateIn(testScope)
+    }
+
+    @Test
+    fun testDefaultState() {
+        val topState = underTest.topDragState
+        assertThat(topState.currentValue).isEqualTo(0)
+        assertThat(topState.offset).isEqualTo(0f)
+        assertThat(topState.anchors.toList()).containsExactly(0 to 0f)
+
+        val bottomState = underTest.bottomDragState
+        assertThat(bottomState.currentValue).isEqualTo(0)
+        assertThat(bottomState.offset).isEqualTo(0f)
+        assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
+    }
+
+    @Test
+    fun testSingleSpanGrid() =
+        testScope.runTest(timeout = Duration.INFINITE) {
+            updateGridLayout(singleSpanGrid)
+
+            val topState = underTest.topDragState
+            assertThat(topState.currentValue).isEqualTo(0)
+            assertThat(topState.anchors.toList()).containsExactly(0 to 0f)
+
+            val bottomState = underTest.bottomDragState
+            assertThat(bottomState.currentValue).isEqualTo(0)
+            assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
+        }
+
+    /**
+     * Verifies element in first row which is already at the minimum size can only be expanded
+     * downwards.
+     */
+    @Test
+    fun testTwoSpanGrid_elementInFirstRow_sizeSingleSpan() =
+        testScope.runTest {
+            updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2))
+
+            val topState = underTest.topDragState
+            assertThat(topState.currentValue).isEqualTo(0)
+            assertThat(topState.anchors.toList()).containsExactly(0 to 0f)
+
+            val bottomState = underTest.bottomDragState
+            assertThat(bottomState.currentValue).isEqualTo(0)
+            assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, 1 to 45f)
+        }
+
+    /**
+     * Verifies element in second row which is already at the minimum size can only be expanded
+     * upwards.
+     */
+    @Test
+    fun testTwoSpanGrid_elementInSecondRow_sizeSingleSpan() =
+        testScope.runTest {
+            updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, currentRow = 1))
+
+            val topState = underTest.topDragState
+            assertThat(topState.currentValue).isEqualTo(0)
+            assertThat(topState.anchors.toList()).containsExactly(0 to 0f, -1 to -45f)
+
+            val bottomState = underTest.bottomDragState
+            assertThat(bottomState.currentValue).isEqualTo(0)
+            assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
+        }
+
+    /**
+     * Verifies element in first row which is already at full size (2 span) can only be shrunk from
+     * the bottom.
+     */
+    @Test
+    fun testTwoSpanGrid_elementInFirstRow_sizeTwoSpan() =
+        testScope.runTest {
+            updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, currentSpan = 2))
+
+            val topState = underTest.topDragState
+            assertThat(topState.currentValue).isEqualTo(0)
+            assertThat(topState.anchors.toList()).containsExactly(0 to 0f)
+
+            val bottomState = underTest.bottomDragState
+            assertThat(bottomState.currentValue).isEqualTo(0)
+            assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, -1 to -45f)
+        }
+
+    /**
+     * Verifies element in a middle row at minimum size can be expanded from either top or bottom.
+     */
+    @Test
+    fun testThreeSpanGrid_elementInMiddleRow_sizeOneSpan() =
+        testScope.runTest {
+            updateGridLayout(singleSpanGrid.copy(maxItemSpan = 3, currentRow = 1))
+
+            val topState = underTest.topDragState
+            assertThat(topState.currentValue).isEqualTo(0)
+            assertThat(topState.anchors.toList()).containsExactly(0 to 0f, -1 to -30f)
+
+            val bottomState = underTest.bottomDragState
+            assertThat(bottomState.currentValue).isEqualTo(0)
+            assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, 1 to 30f)
+        }
+
+    @Test
+    fun testThreeSpanGrid_elementInTopRow_sizeOneSpan() =
+        testScope.runTest {
+            updateGridLayout(singleSpanGrid.copy(maxItemSpan = 3))
+
+            val topState = underTest.topDragState
+            assertThat(topState.currentValue).isEqualTo(0)
+            assertThat(topState.anchors.toList()).containsExactly(0 to 0f)
+
+            val bottomState = underTest.bottomDragState
+            assertThat(bottomState.currentValue).isEqualTo(0)
+            assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, 1 to 30f, 2 to 60f)
+        }
+
+    @Test
+    fun testSixSpanGrid_minSpanThree_itemInThirdRow_sizeThreeSpans() =
+        testScope.runTest {
+            updateGridLayout(
+                singleSpanGrid.copy(
+                    maxItemSpan = 6,
+                    currentRow = 3,
+                    currentSpan = 3,
+                    minItemSpan = 3,
+                )
+            )
+
+            val topState = underTest.topDragState
+            assertThat(topState.currentValue).isEqualTo(0)
+            assertThat(topState.anchors.toList()).containsExactly(0 to 0f, -3 to -45f)
+
+            val bottomState = underTest.bottomDragState
+            assertThat(bottomState.currentValue).isEqualTo(0)
+            assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
+        }
+
+    @Test
+    fun testTwoSpanGrid_elementMovesFromFirstRowToSecondRow() =
+        testScope.runTest {
+            updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2))
+
+            val topState = underTest.topDragState
+            val bottomState = underTest.bottomDragState
+
+            assertThat(topState.anchors.toList()).containsExactly(0 to 0f)
+            assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, 1 to 45f)
+
+            updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, currentRow = 1))
+
+            assertThat(topState.anchors.toList()).containsExactly(0 to 0f, -1 to -45f)
+            assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f)
+        }
+
+    @Test
+    fun testTwoSpanGrid_expandElementFromBottom() = runTestWithSnapshots {
+        val resizeInfo by collectLastValue(underTest.resizeInfo)
+        updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2))
+
+        assertThat(resizeInfo).isNull()
+        underTest.bottomDragState.anchoredDrag { dragTo(45f) }
+        assertThat(resizeInfo).isEqualTo(ResizeInfo(1, DragHandle.BOTTOM))
+    }
+
+    @Test
+    fun testThreeSpanGrid_expandMiddleElementUpwards() = runTestWithSnapshots {
+        val resizeInfo by collectLastValue(underTest.resizeInfo)
+        updateGridLayout(singleSpanGrid.copy(maxItemSpan = 3, currentRow = 1))
+
+        assertThat(resizeInfo).isNull()
+        underTest.topDragState.anchoredDrag { dragTo(-30f) }
+        assertThat(resizeInfo).isEqualTo(ResizeInfo(1, DragHandle.TOP))
+    }
+
+    @Test
+    fun testThreeSpanGrid_expandTopElementDownBy2Spans() = runTestWithSnapshots {
+        val resizeInfo by collectLastValue(underTest.resizeInfo)
+        updateGridLayout(singleSpanGrid.copy(maxItemSpan = 3))
+
+        assertThat(resizeInfo).isNull()
+        underTest.bottomDragState.anchoredDrag { dragTo(60f) }
+        assertThat(resizeInfo).isEqualTo(ResizeInfo(2, DragHandle.BOTTOM))
+    }
+
+    @Test
+    fun testTwoSpanGrid_shrinkElementFromBottom() = runTestWithSnapshots {
+        val resizeInfo by collectLastValue(underTest.resizeInfo)
+        updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, currentSpan = 2))
+
+        assertThat(resizeInfo).isNull()
+        underTest.bottomDragState.anchoredDrag { dragTo(-45f) }
+        assertThat(resizeInfo).isEqualTo(ResizeInfo(-1, DragHandle.BOTTOM))
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun testIllegalState_maxSpanSmallerThanMinSpan() =
+        testScope.runTest {
+            updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, minItemSpan = 3))
+        }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun testIllegalState_minSpanOfZero() =
+        testScope.runTest {
+            updateGridLayout(singleSpanGrid.copy(maxItemSpan = 2, minItemSpan = 0))
+        }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun testIllegalState_maxSpanOfZero() =
+        testScope.runTest {
+            updateGridLayout(singleSpanGrid.copy(maxItemSpan = 0, minItemSpan = 0))
+        }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun testIllegalState_currentRowNotMultipleOfMinSpan() =
+        testScope.runTest {
+            updateGridLayout(singleSpanGrid.copy(maxItemSpan = 6, minItemSpan = 3, currentSpan = 2))
+        }
+
+    private fun TestScope.updateGridLayout(gridLayout: GridLayout) {
+        underTest.setGridLayoutInfo(
+            gridLayout.verticalItemSpacingPx,
+            gridLayout.verticalContentPaddingPx,
+            gridLayout.viewportHeightPx,
+            gridLayout.maxItemSpan,
+            gridLayout.minItemSpan,
+            gridLayout.currentRow,
+            gridLayout.currentSpan,
+        )
+        runCurrent()
+    }
+
+    private fun DraggableAnchors<Int>.toList() = buildList {
+        for (index in 0 until this@toList.size) {
+            add(anchorAt(index) to positionAt(index))
+        }
+    }
+
+    private fun runTestWithSnapshots(testBody: suspend TestScope.() -> Unit) {
+        val globalWriteObserverHandle =
+            Snapshot.registerGlobalWriteObserver {
+                // This is normally done by the compose runtime.
+                Snapshot.sendApplyNotifications()
+            }
+
+        try {
+            testScope.runTest(testBody = testBody)
+        } finally {
+            globalWriteObserverHandle.dispose()
+        }
+    }
+
+    private data class GridLayout(
+        val verticalItemSpacingPx: Float,
+        val verticalContentPaddingPx: Float,
+        val viewportHeightPx: Int,
+        val maxItemSpan: Int,
+        val minItemSpan: Int,
+        val currentRow: Int,
+        val currentSpan: Int,
+    )
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
index d7fe263..dd83702 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
@@ -71,7 +71,7 @@
     @Mock private lateinit var accessibilityManagerWrapper: AccessibilityManagerWrapper
     @get:Rule val mockitoRule = MockitoJUnit.rule()
     private var toastContent = ""
-    private val timeoutMillis = 3500L
+    private val timeoutMillis = 5000L
 
     @Before
     fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index 4ec0802..b632a8a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -176,8 +176,8 @@
             sceneContainerConfig.sceneKeys
                 .filter { it != currentScene }
                 .filter {
-                    // Moving to the Communal scene is not currently falsing protected.
-                    it != Scenes.Communal
+                    // Moving to the Communal and Dream scene is not currently falsing protected.
+                    it != Scenes.Communal && it != Scenes.Dream
                 }
                 .forEach { toScene ->
                     assertWithMessage("Protected scene $toScene not properly protected")
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index f40bfbd..8d678ef 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -506,7 +506,7 @@
     @EnableSceneContainer
     fun pinnedHeadsUpRows_filtersForPinnedItems() =
         testScope.runTest {
-            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRowKeys)
 
             // WHEN there are no pinned rows
             val rows =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt
new file mode 100644
index 0000000..7ce421a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorTest.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.volume.dialog.domain.interactor
+
+import android.app.ActivityManager
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.fakeVolumeDialogController
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.Events
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogVisibilityModel
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.days
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private val dialogTimeoutDuration = 3.seconds
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper()
+class VolumeDialogVisibilityInteractorTest : SysuiTestCase() {
+
+    private val kosmos: Kosmos = testKosmos()
+
+    private lateinit var underTest: VolumeDialogVisibilityInteractor
+
+    @Before
+    fun setUp() {
+        underTest = kosmos.volumeDialogVisibilityInteractor
+    }
+
+    @Test
+    fun testShowRequest_visible() =
+        with(kosmos) {
+            testScope.runTest {
+                runCurrent()
+                val visibilityModel by collectLastValue(underTest.dialogVisibility)
+                fakeVolumeDialogController.onShowRequested(
+                    Events.SHOW_REASON_VOLUME_CHANGED,
+                    false,
+                    ActivityManager.LOCK_TASK_MODE_LOCKED,
+                )
+                runCurrent()
+
+                assertThat(visibilityModel!!)
+                    .isEqualTo(
+                        VolumeDialogVisibilityModel.Visible(
+                            Events.SHOW_REASON_VOLUME_CHANGED,
+                            false,
+                            ActivityManager.LOCK_TASK_MODE_LOCKED,
+                        )
+                    )
+            }
+        }
+
+    @Test
+    fun testDismissRequest_dismissed() =
+        with(kosmos) {
+            testScope.runTest {
+                runCurrent()
+                val visibilityModel by collectLastValue(underTest.dialogVisibility)
+                fakeVolumeDialogController.onShowRequested(
+                    Events.SHOW_REASON_VOLUME_CHANGED,
+                    false,
+                    ActivityManager.LOCK_TASK_MODE_LOCKED,
+                )
+                runCurrent()
+
+                fakeVolumeDialogController.onDismissRequested(Events.DISMISS_REASON_SCREEN_OFF)
+
+                assertThat(visibilityModel!!)
+                    .isEqualTo(
+                        VolumeDialogVisibilityModel.Dismissed(Events.DISMISS_REASON_SCREEN_OFF)
+                    )
+            }
+        }
+
+    @Test
+    fun testTimeout_dismissed() =
+        with(kosmos) {
+            testScope.runTest {
+                runCurrent()
+                underTest.resetDismissTimeout()
+                val visibilityModel by collectLastValue(underTest.dialogVisibility)
+                fakeVolumeDialogController.onShowRequested(
+                    Events.SHOW_REASON_VOLUME_CHANGED,
+                    false,
+                    ActivityManager.LOCK_TASK_MODE_LOCKED,
+                )
+                runCurrent()
+
+                advanceTimeBy(1.days)
+
+                assertThat(visibilityModel!!)
+                    .isEqualTo(VolumeDialogVisibilityModel.Dismissed(Events.DISMISS_REASON_TIMEOUT))
+            }
+        }
+
+    @Test
+    fun testResetTimeoutInterruptsEvents() =
+        with(kosmos) {
+            testScope.runTest {
+                runCurrent()
+                underTest.resetDismissTimeout()
+                val visibilityModel by collectLastValue(underTest.dialogVisibility)
+                fakeVolumeDialogController.onShowRequested(
+                    Events.SHOW_REASON_VOLUME_CHANGED,
+                    false,
+                    ActivityManager.LOCK_TASK_MODE_LOCKED,
+                )
+                runCurrent()
+
+                advanceTimeBy(dialogTimeoutDuration / 2)
+                underTest.resetDismissTimeout()
+                advanceTimeBy(dialogTimeoutDuration / 2)
+                underTest.resetDismissTimeout()
+                advanceTimeBy(dialogTimeoutDuration / 2)
+
+                assertThat(visibilityModel)
+                    .isInstanceOf(VolumeDialogVisibilityModel.Visible::class.java)
+            }
+        }
+}
diff --git a/packages/SystemUI/res/drawable/touchpad_tutorial_home_icon.xml b/packages/SystemUI/res/drawable/touchpad_tutorial_home_icon.xml
new file mode 100644
index 0000000..9f3d075
--- /dev/null
+++ b/packages/SystemUI/res/drawable/touchpad_tutorial_home_icon.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="960"
+    android:viewportWidth="960">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M240,760L360,760L360,520L600,520L600,760L720,760L720,400L480,220L240,400L240,760ZM160,840L160,360L480,120L800,360L800,840L520,840L520,600L440,600L440,840L160,840ZM480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490Z" />
+</vector>
diff --git a/packages/SystemUI/res/drawable/touchpad_tutorial_recents_icon.xml b/packages/SystemUI/res/drawable/touchpad_tutorial_recents_icon.xml
new file mode 100644
index 0000000..113908a
--- /dev/null
+++ b/packages/SystemUI/res/drawable/touchpad_tutorial_recents_icon.xml
@@ -0,0 +1,27 @@
+<!--
+  ~ 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:autoMirrored="true"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="960"
+    android:viewportWidth="960">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M120,800Q87,800 63.5,776.5Q40,753 40,720L40,240Q40,207 63.5,183.5Q87,160 120,160L200,160Q233,160 256.5,183.5Q280,207 280,240L280,720Q280,753 256.5,776.5Q233,800 200,800L120,800ZM120,721L200,721Q200,721 200,721Q200,721 200,721L200,239Q200,239 200,239Q200,239 200,239L120,239Q120,239 120,239Q120,239 120,239L120,721Q120,721 120,721Q120,721 120,721ZM440,800Q407,800 383.5,776.5Q360,753 360,720L360,240Q360,207 383.5,183.5Q407,160 440,160L840,160Q873,160 896.5,183.5Q920,207 920,240L920,720Q920,753 896.5,776.5Q873,800 840,800L440,800ZM440,721L840,721Q840,721 840,721Q840,721 840,721L840,239Q840,239 840,239Q840,239 840,239L440,239Q440,239 440,239Q440,239 440,239L440,721Q440,721 440,721Q440,721 440,721ZM200,721Q200,721 200,721Q200,721 200,721L200,239Q200,239 200,239Q200,239 200,239L200,239Q200,239 200,239Q200,239 200,239L200,721Q200,721 200,721Q200,721 200,721ZM440,721Q440,721 440,721Q440,721 440,721L440,239Q440,239 440,239Q440,239 440,239L440,239Q440,239 440,239Q440,239 440,239L440,721Q440,721 440,721Q440,721 440,721Z" />
+</vector>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 2ddaa56..96a85d7 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3752,9 +3752,9 @@
 
     <!-- TOUCHPAD TUTORIAL-->
     <!-- Label for button opening tutorial for back gesture on touchpad [CHAR LIMIT=NONE] -->
-    <string name="touchpad_tutorial_back_gesture_button">Back gesture</string>
+    <string name="touchpad_tutorial_back_gesture_button">Go back</string>
     <!-- Label for button opening tutorial for back gesture on touchpad [CHAR LIMIT=NONE] -->
-    <string name="touchpad_tutorial_home_gesture_button">Home gesture</string>
+    <string name="touchpad_tutorial_home_gesture_button">Go home</string>
     <!-- Label for button opening tutorial for "view recent apps" gesture on touchpad [CHAR LIMIT=NONE] -->
     <string name="touchpad_tutorial_recent_apps_gesture_button">View recent apps</string>
     <!-- Label for button finishing touchpad tutorial [CHAR LIMIT=NONE] -->
@@ -3763,26 +3763,25 @@
     <!-- Touchpad back gesture action name in tutorial [CHAR LIMIT=NONE] -->
     <string name="touchpad_back_gesture_action_title">Go back</string>
     <!-- Touchpad back gesture guidance in gestures tutorial [CHAR LIMIT=NONE] -->
-    <string name="touchpad_back_gesture_guidance">To go back, swipe left or right using three fingers anywhere on the touchpad.\n\nYou can also use the keyboard shortcut
-Action + ESC for this.</string>
+    <string name="touchpad_back_gesture_guidance">Swipe left or right using three fingers on your touchpad</string>
     <!-- Screen title after back gesture was done successfully [CHAR LIMIT=NONE] -->
-    <string name="touchpad_back_gesture_success_title">Great job!</string>
+    <string name="touchpad_back_gesture_success_title">Nice!</string>
     <!-- Text shown to the user after they complete back gesture tutorial [CHAR LIMIT=NONE] -->
     <string name="touchpad_back_gesture_success_body">You completed the go back gesture.</string>
     <!-- HOME GESTURE -->
     <!-- Touchpad home gesture action name in tutorial [CHAR LIMIT=NONE] -->
     <string name="touchpad_home_gesture_action_title">Go home</string>
     <!-- Touchpad home gesture guidance in gestures tutorial [CHAR LIMIT=NONE] -->
-    <string name="touchpad_home_gesture_guidance">To go to your home screen at any time, swipe up with three fingers from the bottom of your screen.</string>
+    <string name="touchpad_home_gesture_guidance">Swipe up with three fingers on your touchpad</string>
     <!-- Screen title after home gesture was done successfully [CHAR LIMIT=NONE] -->
-    <string name="touchpad_home_gesture_success_title">Nice!</string>
+    <string name="touchpad_home_gesture_success_title">Great job!</string>
     <!-- Text shown to the user after they complete home gesture tutorial [CHAR LIMIT=NONE] -->
-    <string name="touchpad_home_gesture_success_body">You completed the go home gesture.</string>
+    <string name="touchpad_home_gesture_success_body">You completed the go home gesture</string>
     <!-- RECENT APPS GESTURE -->
     <!-- Touchpad recent apps gesture action name in tutorial [CHAR LIMIT=NONE] -->
     <string name="touchpad_recent_apps_gesture_action_title">View recent apps</string>
     <!-- Touchpad recent apps gesture guidance in gestures tutorial [CHAR LIMIT=NONE] -->
-    <string name="touchpad_recent_apps_gesture_guidance">Swipe up and hold using three fingers on your touchpad.</string>
+    <string name="touchpad_recent_apps_gesture_guidance">Swipe up and hold using three fingers on your touchpad</string>
     <!-- Screen title after recent apps gesture was done successfully [CHAR LIMIT=NONE] -->
     <string name="touchpad_recent_apps_gesture_success_title">Great job!</string>
     <!-- Text shown to the user after they complete recent apps gesture tutorial [CHAR LIMIT=NONE] -->
@@ -3790,13 +3789,13 @@
 
     <!-- KEYBOARD TUTORIAL-->
     <!-- Action key tutorial title [CHAR LIMIT=NONE] -->
-    <string name="tutorial_action_key_title">Action key</string>
+    <string name="tutorial_action_key_title">View all apps</string>
     <!-- Action key tutorial guidance[CHAR LIMIT=NONE] -->
-    <string name="tutorial_action_key_guidance">To access your apps, press the action key on your keyboard.</string>
+    <string name="tutorial_action_key_guidance">Press the action key on your keyboard</string>
     <!-- Screen title after action key pressed successfully [CHAR LIMIT=NONE] -->
-    <string name="tutorial_action_key_success_title">Congratulations!</string>
+    <string name="tutorial_action_key_success_title">Well done!</string>
     <!-- Text shown to the user after they complete action key tutorial [CHAR LIMIT=NONE] -->
-    <string name="tutorial_action_key_success_body">You completed the action key gesture.\n\nAction + / shows all the shortcuts you have available.</string>
+    <string name="tutorial_action_key_success_body">You completed the view all apps gesture</string>
 
     <!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] -->
     <string name="keyboard_backlight_dialog_title">Keyboard backlight</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 1c09f84..94b0b5f 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -540,6 +540,7 @@
     <!-- Overridden by values-television/styles.xml with tv-specific settings -->
     <style name="volume_dialog_theme" parent="Theme.SystemUI">
         <item name="android:windowIsFloating">true</item>
+        <item name="android:showWhenLocked">true</item>
     </style>
 
     <style name="Theme.SystemUI.DayNightDialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog"/>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index f05cbf4..2d27f1c0 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -1067,6 +1067,8 @@
 
         @Override
         public void onDestroy() {
+            mUserSwitcherController.removeUserSwitchCallback(mUserSwitchCallback);
+
             ConstraintSet constraintSet = new ConstraintSet();
             constraintSet.clone(mView);
             constraintSet.clear(mUserSwitcherViewGroup.getId());
@@ -1075,6 +1077,8 @@
 
             mView.removeView(mUserSwitcherViewGroup);
             mView.removeView(mUserSwitcher);
+            mUserSwitcher = null;
+            mUserSwitcherViewGroup = null;
         }
 
         private void findLargeUserIcon(int userId, Consumer<Drawable> consumer) {
@@ -1102,6 +1106,10 @@
                 return;
             }
 
+            if (mUserSwitcherViewGroup == null) {
+                return;
+            }
+
             mUserSwitcherViewGroup.setAlpha(0f);
             ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
             int yTrans = mView.getResources().getDimensionPixelSize(R.dimen.pin_view_trans_y_entry);
@@ -1110,14 +1118,18 @@
             animator.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
-                    mUserSwitcherViewGroup.setAlpha(1f);
-                    mUserSwitcherViewGroup.setTranslationY(0f);
+                    if (mUserSwitcherViewGroup != null) {
+                        mUserSwitcherViewGroup.setAlpha(1f);
+                        mUserSwitcherViewGroup.setTranslationY(0f);
+                    }
                 }
             });
             animator.addUpdateListener(animation -> {
-                float value = (float) animation.getAnimatedValue();
-                mUserSwitcherViewGroup.setAlpha(value);
-                mUserSwitcherViewGroup.setTranslationY(yTrans - yTrans * value);
+                if (mUserSwitcherViewGroup != null) {
+                    float value = (float) animation.getAnimatedValue();
+                    mUserSwitcherViewGroup.setAlpha(value);
+                    mUserSwitcherViewGroup.setTranslationY(yTrans - yTrans * value);
+                }
             });
             animator.start();
         }
@@ -1148,6 +1160,10 @@
                 Log.e(TAG, "Current user in user switcher is null.");
                 return;
             }
+            if (mUserSwitcher == null) {
+                Log.w(TAG, "User switcher is not inflated, cannot setupUserSwitcher");
+                return;
+            }
             final String currentUserName = mUserSwitcherController.getCurrentUserName();
             findLargeUserIcon(currentUser.info.id,
                     (Drawable userIcon) -> {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
index 60edaae..158623f 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -280,6 +280,14 @@
         if (mLocalBluetoothManager == null) {
             return;
         }
+
+        // Remove the default padding of the system ui dialog
+        View container = dialog.findViewById(android.R.id.custom);
+        if (container != null && container.getParent() != null) {
+            View containerParent = (View) container.getParent();
+            containerParent.setPadding(0, 0, 0, 0);
+        }
+
         mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_DIALOG_SHOW, mLaunchSourceId);
         mPairButton = dialog.requireViewById(R.id.pair_new_device_button);
         mDeviceList = dialog.requireViewById(R.id.device_list);
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java
index 664f3f8..9367cb5 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java
@@ -108,6 +108,7 @@
         private final ImageView mIconView;
         private final ImageView mGearIcon;
         private final View mGearView;
+        private final View mDividerView;
 
         DeviceItemViewHolder(@NonNull View itemView, Context context) {
             super(itemView);
@@ -118,6 +119,7 @@
             mIconView = itemView.requireViewById(R.id.bluetooth_device_icon);
             mGearIcon = itemView.requireViewById(R.id.gear_icon_image);
             mGearView = itemView.requireViewById(R.id.gear_icon);
+            mDividerView = itemView.requireViewById(R.id.divider);
         }
 
         public void bindView(DeviceItem item, HearingDeviceItemCallback callback) {
@@ -153,6 +155,7 @@
 
             mGearIcon.getDrawable().mutate().setTint(tintColor);
             mGearView.setOnClickListener(view -> callback.onDeviceItemGearClicked(item, view));
+            mDividerView.setBackgroundColor(tintColor);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt
new file mode 100644
index 0000000..7aad33d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt
@@ -0,0 +1,204 @@
+/*
+ * 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.communal.ui.viewmodel
+
+import androidx.compose.foundation.gestures.AnchoredDraggableState
+import androidx.compose.foundation.gestures.DraggableAnchors
+import androidx.compose.runtime.snapshotFlow
+import com.android.app.tracing.coroutines.coroutineScope
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.dropWhile
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
+
+enum class DragHandle {
+    TOP,
+    BOTTOM,
+}
+
+data class ResizeInfo(
+    /**
+     * The number of spans to resize by. A positive number indicates expansion, whereas a negative
+     * number indicates shrinking.
+     */
+    val spans: Int,
+    /** The drag handle which was used to resize the element. */
+    val fromHandle: DragHandle,
+)
+
+class ResizeableItemFrameViewModel : ExclusiveActivatable() {
+    private data class GridLayoutInfo(
+        val minSpan: Int,
+        val maxSpan: Int,
+        val heightPerSpanPx: Float,
+        val verticalItemSpacingPx: Float,
+        val currentRow: Int,
+        val currentSpan: Int,
+    )
+
+    /**
+     * The layout information necessary in order to calculate the pixel offsets of the drag anchor
+     * points.
+     */
+    private val gridLayoutInfo = MutableStateFlow<GridLayoutInfo?>(null)
+
+    val topDragState = AnchoredDraggableState(0, DraggableAnchors { 0 at 0f })
+    val bottomDragState = AnchoredDraggableState(0, DraggableAnchors { 0 at 0f })
+
+    /** Emits a [ResizeInfo] when the element is resized using a drag gesture. */
+    val resizeInfo: Flow<ResizeInfo> =
+        merge(
+                snapshotFlow { topDragState.settledValue }.map { ResizeInfo(-it, DragHandle.TOP) },
+                snapshotFlow { bottomDragState.settledValue }
+                    .map { ResizeInfo(it, DragHandle.BOTTOM) },
+            )
+            .dropWhile { it.spans == 0 }
+            .distinctUntilChanged()
+
+    /**
+     * Sets the necessary grid layout information needed for calculating the pixel offsets of the
+     * drag anchors.
+     */
+    fun setGridLayoutInfo(
+        verticalItemSpacingPx: Float,
+        verticalContentPaddingPx: Float,
+        viewportHeightPx: Int,
+        maxItemSpan: Int,
+        minItemSpan: Int,
+        currentRow: Int,
+        currentSpan: Int,
+    ) {
+        require(maxItemSpan >= minItemSpan) {
+            "Maximum item span of $maxItemSpan cannot be less than the minimum span of $minItemSpan"
+        }
+        require(minItemSpan in 1..maxItemSpan) {
+            "Minimum span must be between 1 and $maxItemSpan, but was $minItemSpan"
+        }
+        require(currentSpan % minItemSpan == 0) {
+            "Current span of $currentSpan is not a multiple of the minimum span of $minItemSpan"
+        }
+        val availableHeight = viewportHeightPx - verticalContentPaddingPx
+        val totalSpacing = verticalItemSpacingPx * ((maxItemSpan / minItemSpan) - 1)
+        val heightPerSpanPx = (availableHeight - totalSpacing) / maxItemSpan
+        gridLayoutInfo.value =
+            GridLayoutInfo(
+                minSpan = minItemSpan,
+                maxSpan = maxItemSpan,
+                heightPerSpanPx = heightPerSpanPx,
+                verticalItemSpacingPx = verticalItemSpacingPx,
+                currentRow = currentRow,
+                currentSpan = currentSpan,
+            )
+    }
+
+    private fun calculateAnchorsForHandle(
+        handle: DragHandle,
+        layoutInfo: GridLayoutInfo,
+    ): DraggableAnchors<Int> {
+
+        if (!isDragAllowed(handle, layoutInfo)) {
+            return DraggableAnchors { 0 at 0f }
+        }
+
+        val (
+            minItemSpan,
+            maxItemSpan,
+            heightPerSpanPx,
+            verticalSpacingPx,
+            currentRow,
+            currentSpan,
+        ) = layoutInfo
+
+        // The maximum row this handle can be dragged to.
+        val maxRow =
+            if (handle == DragHandle.TOP) {
+                (currentRow + currentSpan - minItemSpan).coerceAtLeast(0)
+            } else {
+                maxItemSpan
+            }
+
+        // The minimum row this handle can be dragged to.
+        val minRow =
+            if (handle == DragHandle.TOP) {
+                0
+            } else {
+                (currentRow + minItemSpan).coerceAtMost(maxItemSpan)
+            }
+
+        // The current row position of this handle
+        val currentPosition = if (handle == DragHandle.TOP) currentRow else currentRow + currentSpan
+
+        return DraggableAnchors {
+            for (targetRow in minRow..maxRow step minItemSpan) {
+                val diff = targetRow - currentPosition
+                val spacing = diff / minItemSpan * verticalSpacingPx
+                diff at diff * heightPerSpanPx + spacing
+            }
+        }
+    }
+
+    private fun isDragAllowed(handle: DragHandle, layoutInfo: GridLayoutInfo): Boolean {
+        val minItemSpan = layoutInfo.minSpan
+        val maxItemSpan = layoutInfo.maxSpan
+        val currentRow = layoutInfo.currentRow
+        val currentSpan = layoutInfo.currentSpan
+        val atMinSize = currentSpan == minItemSpan
+
+        // If already at the minimum size and in the first row, item cannot be expanded from the top
+        if (handle == DragHandle.TOP && currentRow == 0 && atMinSize) {
+            return false
+        }
+
+        // If already at the minimum size and occupying the last row, item cannot be expanded from
+        // the
+        // bottom
+        if (handle == DragHandle.BOTTOM && (currentRow + currentSpan) == maxItemSpan && atMinSize) {
+            return false
+        }
+
+        // If at maximum size, item can only be shrunk from the bottom and not the top.
+        if (handle == DragHandle.TOP && currentSpan == maxItemSpan) {
+            return false
+        }
+
+        return true
+    }
+
+    override suspend fun onActivated(): Nothing {
+        coroutineScope("ResizeableItemFrameViewModel.onActivated") {
+            gridLayoutInfo
+                .filterNotNull()
+                .onEach { layoutInfo ->
+                    topDragState.updateAnchors(
+                        calculateAnchorsForHandle(DragHandle.TOP, layoutInfo)
+                    )
+                    bottomDragState.updateAnchors(
+                        calculateAnchorsForHandle(DragHandle.BOTTOM, layoutInfo)
+                    )
+                }
+                .launchIn(this)
+            awaitCancellation()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
new file mode 100644
index 0000000..8b6cc8c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.dreams.ui.viewmodel
+
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/** Handles user input for the dream scene. */
+class DreamUserActionsViewModel @AssistedInject constructor() : UserActionsViewModel() {
+
+    override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
+        setActions(emptyMap())
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun create(): DreamUserActionsViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
index 32e7f41..5563969 100644
--- a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
@@ -48,7 +48,7 @@
 ) {
 
     companion object {
-        const val DEFAULT_DIALOG_TIMEOUT_MILLIS = 3500
+        const val DEFAULT_DIALOG_TIMEOUT_MILLIS = 5000
     }
 
     private val timeoutMillis: Long
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 51d2329..65c29b8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -52,8 +52,8 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.approachLayout
 import androidx.compose.ui.layout.onPlaced
-import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.res.dimensionResource
@@ -66,6 +66,9 @@
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.ElementMatcher
 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneScope
@@ -288,7 +291,7 @@
                 transitions =
                     transitions {
                         from(QuickQuickSettings, QuickSettings) {
-                            quickQuickSettingsToQuickSettings()
+                            quickQuickSettingsToQuickSettings(viewModel::inFirstPage::get)
                         }
                     },
             )
@@ -533,6 +536,10 @@
 
             onDispose { qqsVisible.value = false }
         }
+        val squishiness by
+            viewModel.containerViewModel.quickQuickSettingsViewModel.squishinessViewModel
+                .squishiness
+                .collectAsStateWithLifecycle()
         Column(modifier = Modifier.sysuiResTag("quick_qs_panel")) {
             Box(
                 modifier =
@@ -546,7 +553,16 @@
                                 topFromRoot + coordinates.size.height,
                             )
                         }
-                        .onSizeChanged { size -> qqsHeight.value = size.height }
+                        // Use an approach layout to determien the height without squishiness, as
+                        // that's the value that NPVC and QuickSettingsController care about
+                        // (measured height).
+                        .approachLayout(isMeasurementApproachInProgress = { squishiness < 1f }) {
+                            measurable,
+                            constraints ->
+                            qqsHeight.value = lookaheadSize.height
+                            val placeable = measurable.measure(constraints)
+                            layout(placeable.width, placeable.height) { placeable.place(0, 0) }
+                        }
                         .padding(top = { qqsPadding }, bottom = { bottomPadding.roundToPx() })
             ) {
                 val qsEnabled by viewModel.qsEnabled.collectAsStateWithLifecycle()
@@ -704,6 +720,14 @@
             else -> QuickSettings
         }
     }
+
+    val QqsTileElementMatcher =
+        object : ElementMatcher {
+            override fun matches(key: ElementKey, content: ContentKey): Boolean {
+                return content == SceneKeys.QuickQuickSettings &&
+                    ElementKeys.TileElementMatcher.matches(key, content)
+            }
+        }
 }
 
 suspend fun synchronizeQsState(state: MutableSceneTransitionLayoutState, expansion: Flow<Float>) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt
index 1514986..9e3945e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/FromQuickQuickSettingsToQuickSettings.kt
@@ -17,13 +17,23 @@
 package com.android.systemui.qs.composefragment.ui
 
 import com.android.compose.animation.scene.TransitionBuilder
+import com.android.systemui.qs.composefragment.SceneKeys
 import com.android.systemui.qs.shared.ui.ElementKeys
 
-fun TransitionBuilder.quickQuickSettingsToQuickSettings() {
+fun TransitionBuilder.quickQuickSettingsToQuickSettings(inFirstPage: () -> Boolean = { true }) {
 
     fractionRange(start = 0.5f) { fade(ElementKeys.QuickSettingsContent) }
 
     fractionRange(start = 0.9f) { fade(ElementKeys.FooterActions) }
 
     anchoredTranslate(ElementKeys.QuickSettingsContent, ElementKeys.GridAnchor)
+
+    sharedElement(ElementKeys.TileElementMatcher, enabled = inFirstPage())
+
+    // This will animate between 0f (QQS) and 0.6, fading in the QQS tiles when coming back
+    // from non first page QS. The QS content ends fading out at 0.5f, so there's a brief
+    // overlap, but because they are really faint, it looks better than complete black without
+    // overlap.
+    fractionRange(end = 0.6f) { fade(SceneKeys.QqsTileElementMatcher) }
+    anchoredTranslate(SceneKeys.QqsTileElementMatcher, ElementKeys.GridAnchor)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
index 2d4e358..7a8b2c2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
@@ -31,6 +31,7 @@
 import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel.QSExpansionState
 import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
 import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor
+import com.android.systemui.qs.panels.ui.viewmodel.PaginatedGridViewModel
 import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel
 import com.android.systemui.shade.LargeScreenHeaderHelper
 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
@@ -71,6 +72,7 @@
     private val configurationInteractor: ConfigurationInteractor,
     private val largeScreenHeaderHelper: LargeScreenHeaderHelper,
     private val squishinessInteractor: TileSquishinessInteractor,
+    private val paginatedGridViewModel: PaginatedGridViewModel,
     @Assisted private val lifecycleScope: LifecycleCoroutineScope,
 ) : Dumpable, ExclusiveActivatable() {
     val footerActionsViewModel =
@@ -292,6 +294,9 @@
      */
     var collapseExpandAccessibilityAction: Runnable? = null
 
+    val inFirstPage: Boolean
+        get() = paginatedGridViewModel.inFirstPage
+
     override suspend fun onActivated(): Nothing {
         hydrateSquishinessInteractor()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
index 083f529..e749475 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
@@ -31,8 +31,10 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -76,6 +78,11 @@
 
         val pagerState = rememberPagerState(0) { pages.size }
 
+        // Used to track if this is currently in the first page or not, for animations
+        LaunchedEffect(key1 = pagerState) {
+            snapshotFlow { pagerState.currentPage == 0 }.collect { viewModel.inFirstPage = it }
+        }
+
         Column {
             HorizontalPager(
                 state = pagerState,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.kt
index ada1ef4..91f7641 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/SquishTile.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.qs.panels.ui.compose.infinitegrid
 
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.approachLayout
 import kotlin.math.roundToInt
 
 /**
@@ -27,17 +27,22 @@
  * [squishiness] on the measure/layout pass.
  *
  * The squished composable will be center aligned.
+ *
+ * Use an [approachLayout] to indicate that this should be measured in the lookahead step without
+ * using squishiness. If a parent of this node needs to determine unsquished height, they should
+ * also use an approachLayout tracking the squishiness.
  */
 fun Modifier.verticalSquish(squishiness: () -> Float): Modifier {
-    return layout { measurable, constraints ->
-        val placeable = measurable.measure(constraints)
-        val actualHeight = placeable.height
-        val squishedHeight = actualHeight * squishiness()
-        // Center the content by moving it UP (squishedHeight < actualHeight)
-        val scroll = (squishedHeight - actualHeight) / 2
+    return approachLayout(isMeasurementApproachInProgress = { squishiness() < 1 }) { measurable, _
+        ->
+        val squishinessValue = squishiness()
+        val expectedHeight = lookaheadSize.height
 
-        layout(placeable.width, squishedHeight.roundToInt()) {
-            placeable.place(0, scroll.roundToInt())
-        }
+        val placeable = measurable.measure(lookaheadConstraints)
+        val squishedHeight = (expectedHeight * squishinessValue).roundToInt()
+        // Center the content by moving it UP (squishedHeight < actualHeight)
+        val scroll = (squishedHeight - expectedHeight) / 2
+
+        layout(placeable.width, squishedHeight) { placeable.place(0, scroll) }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
index 28bf474..d4f8298 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
@@ -43,4 +43,10 @@
             SharingStarted.WhileSubscribed(),
             paginatedGridInteractor.defaultRows,
         )
+
+    /*
+     * Tracks whether the current HorizontalPager (using this viewmodel) is in the first page.
+     * This requires it to be a `@SysUISingleton` to be shared between viewmodels.
+     */
+    var inFirstPage = true
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt b/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt
index 625459d..2425f13 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/shared/ui/ElementKeys.kt
@@ -25,7 +25,10 @@
     val GridAnchor = ElementKey("QuickSettingsGridAnchor")
     val FooterActions = ElementKey("FooterActions")
 
-    class TileElementKey(spec: TileSpec, val position: Int) : ElementKey(spec.spec, spec.spec)
+    fun TileSpec.toElementKey(positionInGrid: Int) =
+        ElementKey(this.spec, TileIdentity(this, positionInGrid))
 
-    fun TileSpec.toElementKey(positionInGrid: Int) = TileElementKey(this, positionInGrid)
+    val TileElementMatcher = ElementKey.withIdentity { it is TileIdentity }
 }
+
+private data class TileIdentity(val spec: TileSpec, val position: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index a89f752..4beec10 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -44,6 +44,7 @@
         [
             BouncerSceneModule::class,
             CommunalSceneModule::class,
+            DreamSceneModule::class,
             EmptySceneModule::class,
             GoneSceneModule::class,
             LockscreenSceneModule::class,
@@ -98,6 +99,7 @@
                     listOfNotNull(
                         Scenes.Gone,
                         Scenes.Communal,
+                        Scenes.Dream,
                         Scenes.Lockscreen,
                         Scenes.Bouncer,
                         Scenes.QuickSettings.takeUnless { DualShade.isEnabled },
@@ -114,9 +116,10 @@
                             Scenes.Gone to 0,
                             Scenes.Lockscreen to 0,
                             Scenes.Communal to 1,
-                            Scenes.Shade to 2.takeUnless { DualShade.isEnabled },
-                            Scenes.QuickSettings to 3.takeUnless { DualShade.isEnabled },
-                            Scenes.Bouncer to 4,
+                            Scenes.Dream to 2,
+                            Scenes.Shade to 3.takeUnless { DualShade.isEnabled },
+                            Scenes.QuickSettings to 4.takeUnless { DualShade.isEnabled },
+                            Scenes.Bouncer to 5,
                         )
                         .filterValues { it != null }
                         .mapValues { checkNotNull(it.value) },
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt
index 82b4b1c..16492ef 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt
@@ -33,6 +33,9 @@
     /** The communal scene shows the glanceable hub when device is locked and docked. */
     @JvmField val Communal = SceneKey("communal")
 
+    /** The dream scene shows up when a dream activity is showing. */
+    @JvmField val Dream = SceneKey("dream")
+
     /**
      * "Gone" is not a real scene but rather the absence of scenes when we want to skip showing any
      * content from the scene framework.
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt b/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt
index 6730d2d..7b56688 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt
@@ -49,7 +49,7 @@
     override fun handleScreenshot(
         screenshot: ScreenshotData,
         finisher: Consumer<Uri?>,
-        requestCallback: TakeScreenshotService.RequestCallback
+        requestCallback: TakeScreenshotService.RequestCallback,
     ) {
         if (screenshot.type == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) {
             screenshot.bitmap = imageCapture.captureDisplay(screenshot.displayId, crop = null)
@@ -69,8 +69,8 @@
                 Executors.newSingleThreadExecutor(),
                 UUID.randomUUID(),
                 screenshot.bitmap,
-                screenshot.getUserOrDefault(),
-                screenshot.displayId
+                screenshot.userHandle,
+                screenshot.displayId,
             )
         future.addListener(
             {
@@ -86,7 +86,7 @@
                     requestCallback.reportError()
                 }
             },
-            mainExecutor
+            mainExecutor,
         )
     }
 
@@ -98,11 +98,11 @@
                 .notifyScreenshotError(R.string.screenshot_failed_to_save_text)
         } else {
             uiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, screenshot.packageNameString)
-            if (userManager.isManagedProfile(screenshot.getUserOrDefault().identifier)) {
+            if (userManager.isManagedProfile(screenshot.userHandle.identifier)) {
                 uiEventLogger.log(
                     ScreenshotEvent.SCREENSHOT_SAVED_TO_WORK_PROFILE,
                     0,
-                    screenshot.packageNameString
+                    screenshot.packageNameString,
                 )
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
index 7724abd..e589600 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotController.java
@@ -301,7 +301,7 @@
         saveScreenshotInBackground(screenshot, requestId, finisher, result -> {
             if (result.uri != null) {
                 ScreenshotSavedResult savedScreenshot = new ScreenshotSavedResult(
-                        result.uri, screenshot.getUserOrDefault(), result.timestamp);
+                        result.uri, screenshot.getUserHandle(), result.timestamp);
                 mActionsController.setCompletedScreenshot(requestId, savedScreenshot);
             }
         });
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt
index 29208f8..0806be8 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.kt
@@ -214,11 +214,7 @@
         saveScreenshotInBackground(screenshot, requestId, finisher) { result ->
             if (result.uri != null) {
                 val savedScreenshot =
-                    ScreenshotSavedResult(
-                        result.uri,
-                        screenshot.getUserOrDefault(),
-                        result.timestamp,
-                    )
+                    ScreenshotSavedResult(result.uri, screenshot.userHandle, result.timestamp)
                 actionsController.setCompletedScreenshot(requestId, savedScreenshot)
             }
         }
@@ -235,7 +231,7 @@
         window.setFocusable(true)
         viewProxy.requestFocus()
 
-        enqueueScrollCaptureRequest(requestId, screenshot.userHandle!!)
+        enqueueScrollCaptureRequest(requestId, screenshot.userHandle)
 
         window.attachWindow()
 
@@ -267,7 +263,7 @@
 
     private fun prepareViewForNewScreenshot(screenshot: ScreenshotData, oldPackageName: String?) {
         window.whenWindowAttached {
-            announcementResolver.getScreenshotAnnouncement(screenshot.userHandle!!.identifier) {
+            announcementResolver.getScreenshotAnnouncement(screenshot.userHandle.identifier) {
                 viewProxy.announceForAccessibility(it)
             }
         }
@@ -517,7 +513,7 @@
                 bgExecutor,
                 requestId,
                 screenshot.bitmap,
-                screenshot.getUserOrDefault(),
+                screenshot.userHandle,
                 display.displayId,
             )
         future.addListener(
@@ -525,7 +521,7 @@
                 try {
                     val result = future.get()
                     Log.d(TAG, "Saved screenshot: $result")
-                    logScreenshotResultStatus(result.uri, screenshot.userHandle!!)
+                    logScreenshotResultStatus(result.uri, screenshot.userHandle)
                     onResult.accept(result)
                     if (LogConfig.DEBUG_CALLBACK) {
                         Log.d(TAG, "finished bg processing, calling back with uri: ${result.uri}")
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
index fb7c34f..2df1e8a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotData.kt
@@ -18,7 +18,7 @@
     @ScreenshotType val type: Int,
     @ScreenshotSource val source: Int,
     /** UserHandle for the owner of the app being screenshotted, if known. */
-    val userHandle: UserHandle?,
+    val userHandle: UserHandle,
     /** ComponentName of the top-most app in the screenshot. */
     val topComponent: ComponentName?,
     var screenBounds: Rect?,
@@ -40,7 +40,7 @@
             ScreenshotData(
                 type = request.type,
                 source = request.source,
-                userHandle = if (request.userId >= 0) UserHandle.of(request.userId) else null,
+                userHandle = UserHandle.of(request.userId),
                 topComponent = request.topComponent,
                 screenBounds = request.boundsInScreen,
                 taskId = request.taskId,
@@ -51,7 +51,7 @@
 
         @VisibleForTesting
         fun forTesting(
-            userHandle: UserHandle? = null,
+            userHandle: UserHandle = UserHandle.CURRENT,
             source: Int = ScreenshotSource.SCREENSHOT_KEY_CHORD,
             topComponent: ComponentName? = null,
             bitmap: Bitmap? = null,
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt
index b3d5c9e..b67ad8a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/PolicyRequestProcessor.kt
@@ -92,7 +92,7 @@
                         updates.component,
                         updates.owner,
                         type.taskId,
-                        type.taskBounds
+                        type.taskBounds,
                     )
                 is FullScreen ->
                     replaceWithScreenshot(
@@ -122,7 +122,7 @@
             componentName = topMainRootTask?.topActivity ?: defaultComponent,
             taskId = topMainRootTask?.taskId,
             owner = defaultOwner,
-            displayId = original.displayId
+            displayId = original.displayId,
         )
     }
 
@@ -141,14 +141,14 @@
             userHandle = owner,
             taskId = taskId,
             topComponent = componentName,
-            screenBounds = taskBounds
+            screenBounds = taskBounds,
         )
     }
 
     private suspend fun replaceWithScreenshot(
         original: ScreenshotData,
         componentName: ComponentName?,
-        owner: UserHandle?,
+        owner: UserHandle,
         displayId: Int,
         taskId: Int? = null,
     ): ScreenshotData {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS b/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
index 5558ab1..0a7f08d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
@@ -2,11 +2,13 @@
 
 # Bug component: 78010
 
-aioana@google.com
-aroederer@google.com
-iyz@google.com
 jeffdq@google.com
 juliacr@google.com
+
+aioana@google.com
+aroederer@google.com
+asc@google.com
+iyz@google.com
 juliatuttle@google.com
 kurucz@google.com
 liuyining@google.com
@@ -15,4 +17,4 @@
 valiiftime@google.com
 yurilin@google.com
 
-per-file MediaNotificationProcessor.java = ethibodeau@google.com, asc@google.com
+per-file MediaNotificationProcessor.java = ethibodeau@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
index aa203d7..e25127e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
@@ -39,10 +39,10 @@
 @Inject
 constructor(
     private val headsUpRepository: HeadsUpRepository,
-    private val faceAuthInteractor: DeviceEntryFaceAuthInteractor,
-    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-    private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
-    private val shadeInteractor: ShadeInteractor,
+    faceAuthInteractor: DeviceEntryFaceAuthInteractor,
+    keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
+    shadeInteractor: ShadeInteractor,
 ) {
 
     /** The top-ranked heads up row, regardless of pinned state */
@@ -56,8 +56,7 @@
             }
             .distinctUntilChanged()
 
-    /** Set of currently pinned top-level heads up rows to be displayed. */
-    val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy {
+    private val activeHeadsUpRows: Flow<Set<Pair<HeadsUpRowKey, Boolean>>> by lazy {
         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
             flowOf(emptySet())
         } else {
@@ -67,9 +66,7 @@
                         repositories.map { repo ->
                             repo.isPinned.map { isPinned -> repo to isPinned }
                         }
-                    combine(toCombine) { pairs ->
-                        pairs.filter { (_, isPinned) -> isPinned }.map { (repo, _) -> repo }.toSet()
-                    }
+                    combine(toCombine) { pairs -> pairs.toSet() }
                 } else {
                     // if the set is empty, there are no flows to combine
                     flowOf(emptySet())
@@ -78,6 +75,26 @@
         }
     }
 
+    /** Set of currently active top-level heads up rows to be displayed. */
+    val activeHeadsUpRowKeys: Flow<Set<HeadsUpRowKey>> by lazy {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
+            flowOf(emptySet())
+        } else {
+            activeHeadsUpRows.map { it.map { (repo, _) -> repo }.toSet() }
+        }
+    }
+
+    /** Set of currently pinned top-level heads up rows to be displayed. */
+    val pinnedHeadsUpRowKeys: Flow<Set<HeadsUpRowKey>> by lazy {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
+            flowOf(emptySet())
+        } else {
+            activeHeadsUpRows.map {
+                it.filter { (_, isPinned) -> isPinned }.map { (repo, _) -> repo }.toSet()
+            }
+        }
+    }
+
     /** Are there any pinned heads up rows to display? */
     val hasPinnedRows: Flow<Boolean> by lazy {
         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index 935e2a3..38390e7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -356,11 +356,23 @@
         }
     }
 
-    val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy {
+    val activeHeadsUpRowKeys: Flow<Set<HeadsUpRowKey>> by lazy {
         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
             flowOf(emptySet())
         } else {
-            headsUpNotificationInteractor.pinnedHeadsUpRows.dumpWhileCollecting("pinnedHeadsUpRows")
+            headsUpNotificationInteractor.activeHeadsUpRowKeys.dumpWhileCollecting(
+                "pinnedHeadsUpRows"
+            )
+        }
+    }
+
+    val pinnedHeadsUpRowKeys: Flow<Set<HeadsUpRowKey>> by lazy {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
+            flowOf(emptySet())
+        } else {
+            headsUpNotificationInteractor.pinnedHeadsUpRowKeys.dumpWhileCollecting(
+                "pinnedHeadsUpRows"
+            )
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt
index dc15970..e2e5c59 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt
@@ -26,6 +26,7 @@
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
 
 class HeadsUpNotificationViewBinder
@@ -35,18 +36,21 @@
         coroutineScope {
             launch {
                 var previousKeys = emptySet<HeadsUpRowKey>()
-                viewModel.pinnedHeadsUpRows
+                combine(viewModel.pinnedHeadsUpRowKeys, viewModel.activeHeadsUpRowKeys, ::Pair)
                     .sample(viewModel.headsUpAnimationsEnabled, ::Pair)
                     .collect { (newKeys, animationsEnabled) ->
-                        val added = newKeys - previousKeys
-                        val removed = previousKeys - newKeys
-                        previousKeys = newKeys
+                        val pinned = newKeys.first
+                        val all = newKeys.second
+                        val added = all.union(pinned) - previousKeys
+                        val removed = previousKeys - pinned
+                        previousKeys = pinned
+                        Pair(added, removed)
 
                         if (animationsEnabled) {
                             added.forEach { key ->
                                 parentView.generateHeadsUpAnimation(
                                     obtainView(key),
-                                    /* isHeadsUp = */ true
+                                    /* isHeadsUp = */ true,
                                 )
                             }
                             removed.forEach { key ->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerExt.kt
new file mode 100644
index 0000000..441cbb3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerExt.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy
+
+import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.onStart
+
+/** [DevicePostureController.getDevicePosture] as a [Flow]. */
+@DevicePostureInt
+fun DevicePostureController.devicePosture(): Flow<Int> =
+    conflatedCallbackFlow {
+            val callback = DevicePostureController.Callback { posture -> trySend(posture) }
+            addCallback(callback)
+            awaitClose { removeCallback(callback) }
+        }
+        .onStart { emit(devicePosture) }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
index 6acc891..94e19de 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt
@@ -20,19 +20,27 @@
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.ArrowBack
 import androidx.compose.material3.Button
 import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
 import androidx.compose.ui.unit.dp
 import com.android.systemui.inputdevice.tutorial.ui.composable.DoneButton
 import com.android.systemui.res.R
@@ -47,20 +55,17 @@
     Column(
         verticalArrangement = Arrangement.Center,
         modifier =
-            Modifier.background(
-                    color = MaterialTheme.colorScheme.surfaceContainer,
-                )
-                .fillMaxSize()
+            Modifier.background(color = MaterialTheme.colorScheme.surfaceContainer).fillMaxSize(),
     ) {
         TutorialSelectionButtons(
             onBackTutorialClicked = onBackTutorialClicked,
             onHomeTutorialClicked = onHomeTutorialClicked,
             onRecentAppsTutorialClicked = onRecentAppsTutorialClicked,
-            modifier = Modifier.padding(60.dp)
+            modifier = Modifier.padding(60.dp),
         )
         DoneButton(
             onDoneButtonClicked = onDoneButtonClicked,
-            modifier = Modifier.padding(horizontal = 60.dp)
+            modifier = Modifier.padding(horizontal = 60.dp),
         )
     }
 }
@@ -70,30 +75,36 @@
     onBackTutorialClicked: () -> Unit,
     onHomeTutorialClicked: () -> Unit,
     onRecentAppsTutorialClicked: () -> Unit,
-    modifier: Modifier = Modifier
+    modifier: Modifier = Modifier,
 ) {
     Row(
         horizontalArrangement = Arrangement.spacedBy(20.dp),
         verticalAlignment = Alignment.CenterVertically,
-        modifier = modifier
+        modifier = modifier,
     ) {
         TutorialButton(
             text = stringResource(R.string.touchpad_tutorial_home_gesture_button),
+            icon = Icons.AutoMirrored.Outlined.ArrowBack,
+            iconColor = MaterialTheme.colorScheme.onPrimary,
             onClick = onHomeTutorialClicked,
-            color = MaterialTheme.colorScheme.primary,
-            modifier = Modifier.weight(1f)
+            backgroundColor = MaterialTheme.colorScheme.primary,
+            modifier = Modifier.weight(1f),
         )
         TutorialButton(
             text = stringResource(R.string.touchpad_tutorial_back_gesture_button),
+            icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_home_icon),
+            iconColor = MaterialTheme.colorScheme.onTertiary,
             onClick = onBackTutorialClicked,
-            color = MaterialTheme.colorScheme.tertiary,
-            modifier = Modifier.weight(1f)
+            backgroundColor = MaterialTheme.colorScheme.tertiary,
+            modifier = Modifier.weight(1f),
         )
         TutorialButton(
             text = stringResource(R.string.touchpad_tutorial_recent_apps_gesture_button),
+            icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_recents_icon),
+            iconColor = MaterialTheme.colorScheme.onSecondary,
             onClick = onRecentAppsTutorialClicked,
-            color = MaterialTheme.colorScheme.secondary,
-            modifier = Modifier.weight(1f)
+            backgroundColor = MaterialTheme.colorScheme.secondary,
+            modifier = Modifier.weight(1f),
         )
     }
 }
@@ -101,16 +112,30 @@
 @Composable
 private fun TutorialButton(
     text: String,
+    icon: ImageVector,
+    iconColor: Color,
     onClick: () -> Unit,
-    color: Color,
-    modifier: Modifier = Modifier
+    backgroundColor: Color,
+    modifier: Modifier = Modifier,
 ) {
     Button(
         onClick = onClick,
         shape = RoundedCornerShape(16.dp),
-        colors = ButtonDefaults.buttonColors(containerColor = color),
-        modifier = modifier.aspectRatio(0.66f)
+        colors = ButtonDefaults.buttonColors(containerColor = backgroundColor),
+        modifier = modifier.aspectRatio(0.66f),
     ) {
-        Text(text = text, style = MaterialTheme.typography.headlineLarge)
+        Column(
+            verticalArrangement = Arrangement.Center,
+            horizontalAlignment = Alignment.CenterHorizontally,
+        ) {
+            Icon(
+                imageVector = icon,
+                contentDescription = text,
+                modifier = Modifier.width(30.dp).height(30.dp),
+                tint = iconColor,
+            )
+            Spacer(modifier = Modifier.height(16.dp))
+            Text(text = text, style = MaterialTheme.typography.headlineLarge)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt
deleted file mode 100644
index 869b3c6..0000000
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.volume.dialog
-
-import android.app.Dialog
-import android.content.Context
-import android.os.Bundle
-import android.view.ContextThemeWrapper
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.res.R
-import javax.inject.Inject
-
-class NewVolumeDialog @Inject constructor(@Application context: Context) :
-    Dialog(ContextThemeWrapper(context, R.style.volume_dialog_theme)) {
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        setContentView(R.layout.volume_dialog)
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt
deleted file mode 100644
index b93714a..0000000
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.volume.dialog
-
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.plugins.VolumeDialog
-import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
-import com.android.systemui.volume.dialog.dagger.VolumeDialogPluginComponent
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-
-class NewVolumeDialogPlugin
-@Inject
-constructor(
-    @Application private val applicationCoroutineScope: CoroutineScope,
-    private val volumeDialogPluginComponentFactory: VolumeDialogPluginComponent.Factory,
-) : VolumeDialog {
-
-    private var volumeDialogPluginComponent: VolumeDialogPluginComponent? = null
-    private var job: Job? = null
-
-    override fun init(windowType: Int, callback: VolumeDialog.Callback?) {
-        job =
-            applicationCoroutineScope.launch {
-                coroutineScope {
-                    volumeDialogPluginComponent = volumeDialogPluginComponentFactory.create(this)
-                }
-            }
-    }
-
-    private fun showDialog() {
-        val volumeDialogPluginComponent =
-            volumeDialogPluginComponent ?: error("Creating dialog before init was called")
-        volumeDialogPluginComponent.coroutineScope().launch {
-            coroutineScope {
-                val volumeDialogComponent: VolumeDialogComponent =
-                    volumeDialogPluginComponent.volumeDialogComponentFactory().create(this)
-                with(volumeDialogComponent.volumeDialog()) {
-                    setOnDismissListener { volumeDialogComponent.coroutineScope().cancel() }
-                    show()
-                }
-            }
-        }
-    }
-
-    override fun destroy() {
-        job?.cancel()
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
index 74e823e..7476c6a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
@@ -20,15 +20,39 @@
 import android.content.Context
 import android.os.Bundle
 import android.view.ContextThemeWrapper
+import android.view.MotionEvent
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.res.R
+import com.android.systemui.volume.Events
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.ui.binder.VolumeDialogBinder
 import javax.inject.Inject
 
-class VolumeDialog @Inject constructor(@Application context: Context) :
-    Dialog(ContextThemeWrapper(context, R.style.volume_dialog_theme)) {
+class VolumeDialog
+@Inject
+constructor(
+    @Application context: Context,
+    private val dialogBinder: VolumeDialogBinder,
+    private val visibilityInteractor: VolumeDialogVisibilityInteractor,
+) : Dialog(ContextThemeWrapper(context, R.style.volume_dialog_theme)) {
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        setContentView(R.layout.volume_dialog)
+        dialogBinder.bind(this)
+    }
+
+    /**
+     * NOTE: This will be called with ACTION_OUTSIDE MotionEvents for touches that occur outside of
+     * the touchable region of the volume dialog (as returned by [.onComputeInternalInsets]) even if
+     * those touches occurred within the bounds of the volume dialog.
+     */
+    override fun onTouchEvent(event: MotionEvent): Boolean {
+        if (isShowing) {
+            if (event.action == MotionEvent.ACTION_OUTSIDE) {
+                visibilityInteractor.dismissDialog(Events.DISMISS_REASON_TOUCH_OUTSIDE)
+                return true
+            }
+        }
+        return false
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
index a2e81d9..4b7a978 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
@@ -18,12 +18,10 @@
 
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.plugins.VolumeDialog
-import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
 import com.android.systemui.volume.dialog.dagger.VolumeDialogPluginComponent
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancel
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 
@@ -34,31 +32,17 @@
     private val volumeDialogPluginComponentFactory: VolumeDialogPluginComponent.Factory,
 ) : VolumeDialog {
 
-    private var volumeDialogPluginComponent: VolumeDialogPluginComponent? = null
     private var job: Job? = null
 
     override fun init(windowType: Int, callback: VolumeDialog.Callback?) {
         job =
             applicationCoroutineScope.launch {
                 coroutineScope {
-                    volumeDialogPluginComponent = volumeDialogPluginComponentFactory.create(this)
-                }
-            }
-    }
+                    val component = volumeDialogPluginComponentFactory.create(this)
 
-    private fun showDialog() {
-        val volumeDialogPluginComponent =
-            volumeDialogPluginComponent ?: error("Creating dialog before init was called")
-        volumeDialogPluginComponent.coroutineScope().launch {
-            coroutineScope {
-                val volumeDialogComponent: VolumeDialogComponent =
-                    volumeDialogPluginComponent.volumeDialogComponentFactory().create(this)
-                with(volumeDialogComponent.volumeDialog()) {
-                    setOnDismissListener { volumeDialogComponent.coroutineScope().cancel() }
-                    show()
+                    component.viewModel().activate()
                 }
             }
-        }
     }
 
     override fun destroy() {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt
index 82612a7..4e0098c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/VolumeDialogPluginComponent.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.volume.dialog.dagger.module.VolumeDialogPluginModule
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogPluginViewModel
 import dagger.BindsInstance
 import dagger.Subcomponent
 import kotlinx.coroutines.CoroutineScope
@@ -31,15 +32,7 @@
 @Subcomponent(modules = [VolumeDialogPluginModule::class])
 interface VolumeDialogPluginComponent {
 
-    /**
-     * Provides a coroutine scope to use inside [VolumeDialogPluginScope].
-     * [com.android.systemui.volume.dialog.VolumeDialogPlugin] manages the lifecycle of this scope.
-     * It's cancelled when the dialog is disposed. This helps to free occupied resources when volume
-     * dialog is not shown.
-     */
-    @VolumeDialogPlugin fun coroutineScope(): CoroutineScope
-
-    fun volumeDialogComponentFactory(): VolumeDialogComponent.Factory
+    fun viewModel(): VolumeDialogPluginViewModel
 
     @Subcomponent.Factory
     interface Factory {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
index ec7c6ce..2e26fd6 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt
@@ -20,7 +20,8 @@
 import android.os.Handler
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.plugins.VolumeDialogController
-import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
 import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
 import com.android.systemui.volume.dialog.domain.model.VolumeDialogStateModel
 import javax.inject.Inject
@@ -40,12 +41,12 @@
  *
  * @see VolumeDialogController.Callbacks
  */
-@VolumeDialog
+@VolumeDialogPluginScope
 class VolumeDialogCallbacksInteractor
 @Inject
 constructor(
     private val volumeDialogController: VolumeDialogController,
-    @VolumeDialog private val coroutineScope: CoroutineScope,
+    @VolumeDialogPlugin private val coroutineScope: CoroutineScope,
     @Background private val bgHandler: Handler,
 ) {
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
index dd51108..4a709a44b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
@@ -17,7 +17,8 @@
 package com.android.systemui.volume.dialog.domain.interactor
 
 import com.android.systemui.plugins.VolumeDialogController
-import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
 import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
 import com.android.systemui.volume.dialog.domain.model.VolumeDialogStateModel
 import javax.inject.Inject
@@ -35,13 +36,13 @@
  *
  * @see [VolumeDialogController]
  */
-@VolumeDialog
+@VolumeDialogPluginScope
 class VolumeDialogStateInteractor
 @Inject
 constructor(
     volumeDialogCallbacksInteractor: VolumeDialogCallbacksInteractor,
     private val volumeDialogController: VolumeDialogController,
-    @VolumeDialog private val coroutineScope: CoroutineScope,
+    @VolumeDialogPlugin private val coroutineScope: CoroutineScope,
 ) {
 
     val volumeDialogState: Flow<VolumeDialogStateModel> =
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt
new file mode 100644
index 0000000..6c92754
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.volume.dialog.domain.interactor
+
+import android.annotation.SuppressLint
+import com.android.systemui.volume.Events
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogVisibilityModel
+import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.update
+
+private val maxDialogShowTime: Duration = 3.seconds
+
+/**
+ * Handles Volume Dialog visibility state. It might change from several sources:
+ * - [com.android.systemui.plugins.VolumeDialogController] requests visibility change;
+ * - it might be dismissed by the inactivity timeout;
+ * - it can be dismissed by the user;
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@VolumeDialogPluginScope
+class VolumeDialogVisibilityInteractor
+@Inject
+constructor(
+    @VolumeDialogPlugin coroutineScope: CoroutineScope,
+    callbacksInteractor: VolumeDialogCallbacksInteractor,
+) {
+
+    @SuppressLint("SharedFlowCreation")
+    private val mutableDismissDialogEvents = MutableSharedFlow<Unit>()
+    private val mutableDialogVisibility =
+        MutableStateFlow<VolumeDialogVisibilityModel>(VolumeDialogVisibilityModel.Invisible)
+
+    val dialogVisibility: Flow<VolumeDialogVisibilityModel> = mutableDialogVisibility.asStateFlow()
+
+    init {
+        merge(
+                mutableDismissDialogEvents.mapLatest {
+                    delay(maxDialogShowTime)
+                    VolumeDialogEventModel.DismissRequested(Events.DISMISS_REASON_TIMEOUT)
+                },
+                callbacksInteractor.event,
+            )
+            .onEach { event ->
+                VolumeDialogVisibilityModel.fromEvent(event)?.let { model ->
+                    mutableDialogVisibility.value = model
+                    if (model is VolumeDialogVisibilityModel.Visible) {
+                        resetDismissTimeout()
+                    }
+                }
+            }
+            .launchIn(coroutineScope)
+    }
+
+    /**
+     * Dismisses the dialog with a given [reason]. The new state will be emitted in the
+     * [dialogVisibility].
+     */
+    fun dismissDialog(reason: Int) {
+        mutableDialogVisibility.update {
+            if (it is VolumeDialogVisibilityModel.Dismissed) {
+                it
+            } else {
+                VolumeDialogVisibilityModel.Dismissed(reason)
+            }
+        }
+    }
+
+    /** Resets current dialog timeout. */
+    suspend fun resetDismissTimeout() {
+        mutableDismissDialogEvents.emit(Unit)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogVisibilityModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogVisibilityModel.kt
new file mode 100644
index 0000000..646445d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogVisibilityModel.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.volume.dialog.domain.model
+
+/** Models current Volume Dialog visibility state. */
+sealed interface VolumeDialogVisibilityModel {
+
+    /** Dialog is currently visible. */
+    data class Visible(val reason: Int, val keyguardLocked: Boolean, val lockTaskModeState: Int) :
+        VolumeDialogVisibilityModel
+
+    /** Dialog has never been shown. So it's just invisible. */
+    interface Invisible : VolumeDialogVisibilityModel {
+        companion object : Invisible
+    }
+
+    /** Dialog has been shown and then dismissed. */
+    data class Dismissed(val reason: Int) : Invisible
+
+    companion object {
+
+        /**
+         * Creates [VolumeDialogVisibilityModel] from appropriate events and returns null otherwise.
+         */
+        fun fromEvent(event: VolumeDialogEventModel): VolumeDialogVisibilityModel? {
+            return when (event) {
+                is VolumeDialogEventModel.DismissRequested -> Dismissed(event.reason)
+                is VolumeDialogEventModel.ShowRequested ->
+                    Visible(event.reason, event.keyguardLocked, event.lockTaskModeState)
+                else -> null
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt
new file mode 100644
index 0000000..59c38c0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.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.volume.dialog.shared
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.VolumeLog
+import com.android.systemui.volume.Events
+import javax.inject.Inject
+
+private const val TAG = "SysUI_VolumeDialog"
+
+/** Logs events related to the Volume Panel. */
+class VolumeDialogLogger @Inject constructor(@VolumeLog private val logBuffer: LogBuffer) {
+
+    fun onShow(reason: Int) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { int1 = reason },
+            { "Show: ${Events.SHOW_REASONS[int1]}" },
+        )
+    }
+
+    fun onDismiss(reason: Int) {
+        logBuffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { int1 = reason },
+            { "Dismiss: ${Events.DISMISS_REASONS[int1]}" },
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt
new file mode 100644
index 0000000..3f2c39b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.volume.dialog.ui.binder
+
+import android.app.Dialog
+import android.graphics.Color
+import android.graphics.PixelFormat
+import android.graphics.drawable.ColorDrawable
+import android.view.View
+import android.view.ViewGroup
+import android.view.Window
+import android.view.WindowManager
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogGravityViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+@VolumeDialogScope
+class VolumeDialogBinder
+@Inject
+constructor(
+    @VolumeDialog private val coroutineScope: CoroutineScope,
+    private val volumeDialogViewBinder: VolumeDialogViewBinder,
+    private val gravityViewModel: VolumeDialogGravityViewModel,
+) {
+
+    fun bind(dialog: Dialog) {
+        with(dialog) {
+            setupWindow(window!!)
+            dialog.setContentView(R.layout.volume_dialog)
+
+            val volumeDialogView: View = dialog.requireViewById(R.id.volume_dialog_container)
+            volumeDialogView.repeatWhenAttached {
+                lifecycleScope.launch { volumeDialogViewBinder.bind(volumeDialogView) }
+            }
+        }
+    }
+
+    /** Configures [Window] for the [Dialog]. */
+    private fun setupWindow(window: Window) =
+        with(window) {
+            clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+            addFlags(
+                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
+                    WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
+                    WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or
+                    WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
+            )
+            addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY)
+
+            requestFeature(Window.FEATURE_NO_TITLE)
+            setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
+            setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY)
+            setWindowAnimations(-1)
+            setFormat(PixelFormat.TRANSLUCENT)
+
+            attributes =
+                attributes.apply {
+                    title = "VolumeDialog" // Not the same as Window#setTitle
+                }
+            setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+
+            gravityViewModel.dialogGravity.onEach { window.setGravity(it) }.launchIn(coroutineScope)
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt
new file mode 100644
index 0000000..df6523c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.volume.dialog.ui.viewmodel
+
+import android.content.Context
+import android.content.res.Configuration
+import android.view.Gravity
+import androidx.annotation.GravityInt
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.UiBackground
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.DevicePostureController
+import com.android.systemui.statusbar.policy.devicePosture
+import com.android.systemui.statusbar.policy.onConfigChanged
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+@VolumeDialogScope
+class VolumeDialogGravityViewModel
+@Inject
+constructor(
+    @Application private val context: Context,
+    @VolumeDialog private val coroutineScope: CoroutineScope,
+    @UiBackground private val uiBackgroundCoroutineContext: CoroutineContext,
+    configurationController: ConfigurationController,
+    private val devicePostureController: DevicePostureController,
+) {
+
+    @GravityInt private var originalGravity: Int = context.getAbsoluteGravity()
+
+    val dialogGravity: Flow<Int> =
+        combine(
+                devicePostureController.devicePosture(),
+                configurationController.onConfigChanged.onEach { onConfigurationChanged() },
+            ) { devicePosture, configuration ->
+                context.calculateGravity(devicePosture, configuration)
+            }
+            .stateIn(
+                scope = coroutineScope,
+                started = SharingStarted.Eagerly,
+                context.calculateGravity(),
+            )
+
+    private suspend fun onConfigurationChanged() {
+        withContext(uiBackgroundCoroutineContext) { originalGravity = context.getAbsoluteGravity() }
+    }
+
+    @GravityInt
+    private fun Context.calculateGravity(
+        devicePosture: Int = devicePostureController.devicePosture,
+        config: Configuration = resources.configuration,
+    ): Int {
+        val isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE
+        val isHalfOpen = devicePosture == DevicePostureController.DEVICE_POSTURE_HALF_OPENED
+        val gravity =
+            if (isLandscape && isHalfOpen) {
+                originalGravity or Gravity.TOP
+            } else {
+                originalGravity
+            }
+        return getAbsoluteGravity(gravity)
+    }
+}
+
+@GravityInt
+private fun Context.getAbsoluteGravity(
+    gravity: Int = resources.getInteger(R.integer.volume_dialog_gravity)
+): Int = with(resources) { Gravity.getAbsoluteGravity(gravity, configuration.layoutDirection) }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt
new file mode 100644
index 0000000..329a947
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.volume.dialog.ui.viewmodel
+
+import android.app.Dialog
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.plugins.VolumeDialogController
+import com.android.systemui.volume.Events
+import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogVisibilityModel
+import com.android.systemui.volume.dialog.shared.VolumeDialogLogger
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@VolumeDialogPluginScope
+class VolumeDialogPluginViewModel
+@Inject
+constructor(
+    private val componentFactory: VolumeDialogComponent.Factory,
+    private val dialogVisibilityInteractor: VolumeDialogVisibilityInteractor,
+    private val controller: VolumeDialogController,
+    private val logger: VolumeDialogLogger,
+) : ExclusiveActivatable() {
+
+    override suspend fun onActivated(): Nothing {
+        coroutineScope {
+            dialogVisibilityInteractor.dialogVisibility
+                .mapLatest { visibilityModel ->
+                    with(visibilityModel) {
+                        if (this is VolumeDialogVisibilityModel.Visible) {
+                            showDialog(reason, keyguardLocked, lockTaskModeState)
+                        }
+                        if (this is VolumeDialogVisibilityModel.Dismissed) {
+                            Events.writeEvent(Events.EVENT_DISMISS_DIALOG, reason)
+                            logger.onDismiss(reason)
+                        }
+                    }
+                }
+                .launchIn(this)
+        }
+        awaitCancellation()
+    }
+
+    suspend fun showDialog(reason: Int, keyguardLocked: Boolean, lockTaskModeState: Int): Unit =
+        coroutineScope {
+            logger.onShow(reason)
+
+            controller.notifyVisible(true)
+
+            val volumeDialogComponent: VolumeDialogComponent = componentFactory.create(this)
+            val dialog =
+                volumeDialogComponent.volumeDialog().apply {
+                    setOnDismissListener {
+                        volumeDialogComponent.coroutineScope().cancel()
+                        dialogVisibilityInteractor.dismissDialog(Events.DISMISS_REASON_UNKNOWN)
+                    }
+                }
+            launch { dialog.awaitShow() }
+
+            Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, keyguardLocked)
+        }
+}
+
+/** Shows [Dialog] until suspend function is cancelled. */
+private suspend fun Dialog.awaitShow() =
+    suspendCancellableCoroutine<Unit> {
+        show()
+        it.invokeOnCancellation { dismiss() }
+    }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
index f9e91ae..30c8c15 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
@@ -19,11 +19,12 @@
 import com.android.systemui.lifecycle.ExclusiveActivatable
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
 
 class VolumeDialogViewModel @AssistedInject constructor() : ExclusiveActivatable() {
 
     override suspend fun onActivated(): Nothing {
-        TODO("Not yet implemented")
+        awaitCancellation()
     }
 
     @AssistedFactory
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
index 3ed0977..1d74e8b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
@@ -17,12 +17,12 @@
 package com.android.systemui.screenshot
 
 import android.content.ComponentName
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import android.graphics.Insets
 import android.graphics.Rect
 import android.os.UserHandle
 import android.view.Display
 import android.view.WindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.internal.util.ScreenshotRequest
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -71,15 +71,6 @@
     }
 
     @Test
-    fun testNegativeUserId() {
-        val request = ScreenshotRequest.Builder(type, source).setUserId(-1).build()
-
-        val data = ScreenshotData.fromRequest(request)
-
-        assertThat(data.userHandle).isNull()
-    }
-
-    @Test
     fun testPackageNameAsString() {
         val request = ScreenshotRequest.Builder(type, source).setTopComponent(component).build()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt
index bab9bbb..2fcacb9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/policy/PolicyRequestProcessorTest.kt
@@ -58,13 +58,13 @@
             ScreenshotData(
                 TAKE_SCREENSHOT_FULLSCREEN,
                 SCREENSHOT_KEY_CHORD,
-                null,
+                UserHandle.CURRENT,
                 topComponent = null,
                 screenBounds = Rect(0, 0, 1, 1),
                 taskId = -1,
                 insets = Insets.NONE,
                 bitmap = null,
-                displayId = DEFAULT_DISPLAY
+                displayId = DEFAULT_DISPLAY,
             )
 
         /* Create a policy request processor with no capture policies */
@@ -75,7 +75,7 @@
                 policies = emptyList(),
                 defaultOwner = UserHandle.of(PERSONAL),
                 defaultComponent = ComponentName("default", "Component"),
-                displayTasks = fullScreenWork
+                displayTasks = fullScreenWork,
             )
 
         val result = runBlocking { requestProcessor.process(request) }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
index 7d5278e..eb1bcc7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
@@ -146,9 +146,17 @@
         }
 
     @Test
+    fun activeRows_noRows_isEmpty() =
+        testScope.runTest {
+            val activeHeadsUpRows by collectLastValue(underTest.activeHeadsUpRowKeys)
+
+            assertThat(activeHeadsUpRows).isEmpty()
+        }
+
+    @Test
     fun pinnedRows_noRows_isEmpty() =
         testScope.runTest {
-            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRowKeys)
 
             assertThat(pinnedHeadsUpRows).isEmpty()
         }
@@ -156,7 +164,7 @@
     @Test
     fun pinnedRows_noPinnedRows_isEmpty() =
         testScope.runTest {
-            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRowKeys)
             // WHEN no rows are pinned
             headsUpRepository.setNotifications(
                 fakeHeadsUpRowRepository("key 0"),
@@ -170,9 +178,27 @@
         }
 
     @Test
+    fun activeRows_noPinnedRows_containsAllRows() =
+        testScope.runTest {
+            val activeHeadsUpRows by collectLastValue(underTest.activeHeadsUpRowKeys)
+            // WHEN no rows are pinned
+            val rows =
+                arrayListOf(
+                    fakeHeadsUpRowRepository("key 0"),
+                    fakeHeadsUpRowRepository("key 1"),
+                    fakeHeadsUpRowRepository("key 2"),
+                )
+            headsUpRepository.setNotifications(rows)
+            runCurrent()
+
+            // THEN all rows are present
+            assertThat(activeHeadsUpRows).containsExactly(rows[0], rows[1], rows[2])
+        }
+
+    @Test
     fun pinnedRows_hasPinnedRows_containsPinnedRows() =
         testScope.runTest {
-            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRowKeys)
             // WHEN some rows are pinned
             val rows =
                 arrayListOf(
@@ -188,9 +214,27 @@
         }
 
     @Test
+    fun pinnedRows_hasPinnedRows_containsAllRows() =
+        testScope.runTest {
+            val activeHeadsUpRows by collectLastValue(underTest.activeHeadsUpRowKeys)
+            // WHEN no rows are pinned
+            val rows =
+                arrayListOf(
+                    fakeHeadsUpRowRepository("key 0", isPinned = true),
+                    fakeHeadsUpRowRepository("key 1", isPinned = true),
+                    fakeHeadsUpRowRepository("key 2"),
+                )
+            headsUpRepository.setNotifications(rows)
+            runCurrent()
+
+            // THEN all rows are present
+            assertThat(activeHeadsUpRows).containsExactly(rows[0], rows[1], rows[2])
+        }
+
+    @Test
     fun pinnedRows_rowGetsPinned_containsPinnedRows() =
         testScope.runTest {
-            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRowKeys)
             // GIVEN some rows are pinned
             val rows =
                 arrayListOf(
@@ -210,9 +254,34 @@
         }
 
     @Test
+    fun activeRows_rowGetsPinned_containsAllRows() =
+        testScope.runTest {
+            val activeHeadsUpRows by collectLastValue(underTest.activeHeadsUpRowKeys)
+            // GIVEN some rows are pinned
+            val rows =
+                arrayListOf(
+                    fakeHeadsUpRowRepository("key 0", isPinned = true),
+                    fakeHeadsUpRowRepository("key 1", isPinned = true),
+                    fakeHeadsUpRowRepository("key 2"),
+                )
+            headsUpRepository.setNotifications(rows)
+            runCurrent()
+
+            // THEN all rows are present
+            assertThat(activeHeadsUpRows).containsExactly(rows[0], rows[1], rows[2])
+
+            // WHEN all rows gets pinned
+            rows[2].isPinned.value = true
+            runCurrent()
+
+            // THEN no change
+            assertThat(activeHeadsUpRows).containsExactly(rows[0], rows[1], rows[2])
+        }
+
+    @Test
     fun pinnedRows_allRowsPinned_containsAllRows() =
         testScope.runTest {
-            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRowKeys)
             // WHEN all rows are pinned
             val rows =
                 arrayListOf(
@@ -228,9 +297,27 @@
         }
 
     @Test
+    fun activeRows_allRowsPinned_containsAllRows() =
+        testScope.runTest {
+            val activeHeadsUpRows by collectLastValue(underTest.activeHeadsUpRowKeys)
+            // WHEN all rows are pinned
+            val rows =
+                arrayListOf(
+                    fakeHeadsUpRowRepository("key 0", isPinned = true),
+                    fakeHeadsUpRowRepository("key 1", isPinned = true),
+                    fakeHeadsUpRowRepository("key 2", isPinned = true),
+                )
+            headsUpRepository.setNotifications(rows)
+            runCurrent()
+
+            // THEN no rows are filtered
+            assertThat(activeHeadsUpRows).containsExactly(rows[0], rows[1], rows[2])
+        }
+
+    @Test
     fun pinnedRows_rowGetsUnPinned_containsPinnedRows() =
         testScope.runTest {
-            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRowKeys)
             // GIVEN all rows are pinned
             val rows =
                 arrayListOf(
@@ -250,9 +337,31 @@
         }
 
     @Test
+    fun activeRows_rowGetsUnPinned_containsAllRows() =
+        testScope.runTest {
+            val activeHeadsUpRows by collectLastValue(underTest.activeHeadsUpRowKeys)
+            // GIVEN all rows are pinned
+            val rows =
+                arrayListOf(
+                    fakeHeadsUpRowRepository("key 0", isPinned = true),
+                    fakeHeadsUpRowRepository("key 1", isPinned = true),
+                    fakeHeadsUpRowRepository("key 2", isPinned = true),
+                )
+            headsUpRepository.setNotifications(rows)
+            runCurrent()
+
+            // WHEN a row gets unpinned
+            rows[0].isPinned.value = false
+            runCurrent()
+
+            // THEN all rows are still present
+            assertThat(activeHeadsUpRows).containsExactly(rows[0], rows[1], rows[2])
+        }
+
+    @Test
     fun pinnedRows_rowGetsPinnedAndUnPinned_containsTheSameInstance() =
         testScope.runTest {
-            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows)
+            val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRowKeys)
 
             val rows =
                 arrayListOf(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelKosmos.kt
new file mode 100644
index 0000000..8422942
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.communal.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.resizeableItemFrameViewModel by Kosmos.Fixture { ResizeableItemFrameViewModel() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt
new file mode 100644
index 0000000..e4a2a87
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt
@@ -0,0 +1,195 @@
+/*
+ * 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.plugins
+
+import android.media.AudioManager
+import android.media.AudioManager.CsdWarning
+import android.os.Handler
+import android.os.VibrationEffect
+import androidx.core.util.getOrElse
+import java.util.concurrent.CopyOnWriteArraySet
+
+class FakeVolumeDialogController(private val audioManager: AudioManager) : VolumeDialogController {
+
+    var isVisible: Boolean = false
+        private set
+
+    var hasScheduledTouchFeedback: Boolean = false
+        private set
+
+    var vibrationEffect: VibrationEffect? = null
+        private set
+
+    var hasUserActivity: Boolean = false
+        private set
+
+    private var hasVibrator: Boolean = true
+
+    private val state = VolumeDialogController.State()
+    private val callbacks = CopyOnWriteArraySet<VolumeDialogController.Callbacks>()
+
+    override fun setActiveStream(stream: Int) {
+        // ensure streamState existence for the active stream
+        state.states.getOrElse(stream) {
+            VolumeDialogController.StreamState().also { streamState ->
+                state.states.put(stream, streamState)
+            }
+        }
+        state.activeStream = stream
+    }
+
+    override fun setStreamVolume(stream: Int, userLevel: Int) {
+        val streamState =
+            state.states.getOrElse(stream) {
+                VolumeDialogController.StreamState().also { streamState ->
+                    state.states.put(stream, streamState)
+                }
+            }
+        streamState.level = userLevel.coerceIn(streamState.levelMin, streamState.levelMax)
+    }
+
+    override fun setRingerMode(ringerModeNormal: Int, external: Boolean) {
+        if (external) {
+            state.ringerModeExternal = ringerModeNormal
+        } else {
+            state.ringerModeInternal = ringerModeNormal
+        }
+    }
+
+    fun setHasVibrator(hasVibrator: Boolean) {
+        this.hasVibrator = hasVibrator
+    }
+
+    override fun hasVibrator(): Boolean = hasVibrator
+
+    override fun vibrate(effect: VibrationEffect) {
+        vibrationEffect = effect
+    }
+
+    override fun scheduleTouchFeedback() {
+        hasScheduledTouchFeedback = true
+    }
+
+    fun resetScheduledTouchFeedback() {
+        hasScheduledTouchFeedback = false
+    }
+
+    override fun getAudioManager(): AudioManager = audioManager
+
+    override fun notifyVisible(visible: Boolean) {
+        isVisible = visible
+    }
+
+    override fun addCallback(callbacks: VolumeDialogController.Callbacks?, handler: Handler?) {
+        this.callbacks.add(callbacks)
+    }
+
+    override fun removeCallback(callbacks: VolumeDialogController.Callbacks?) {
+        this.callbacks.remove(callbacks)
+    }
+
+    override fun userActivity() {
+        hasUserActivity = true
+    }
+
+    fun resetUserActivity() {
+        hasUserActivity = false
+    }
+
+    override fun getState() {
+        callbacks.sendEvent { it.onStateChanged(state) }
+    }
+
+    /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onShowRequested */
+    fun onShowRequested(reason: Int, keyguardLocked: Boolean, lockTaskModeState: Int) {
+        callbacks.sendEvent { it.onShowRequested(reason, keyguardLocked, lockTaskModeState) }
+    }
+
+    /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onDismissRequested */
+    fun onDismissRequested(reason: Int) {
+        callbacks.sendEvent { it.onDismissRequested(reason) }
+    }
+
+    /**
+     * @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onLayoutDirectionChanged
+     */
+    fun onLayoutDirectionChanged(layoutDirection: Int) {
+        callbacks.sendEvent { it.onLayoutDirectionChanged(layoutDirection) }
+    }
+
+    /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onConfigurationChanged */
+    fun onConfigurationChanged() {
+        callbacks.sendEvent { it.onConfigurationChanged() }
+    }
+
+    /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onShowVibrateHint */
+    fun onShowVibrateHint() {
+        callbacks.sendEvent { it.onShowVibrateHint() }
+    }
+
+    /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onShowSilentHint */
+    fun onShowSilentHint() {
+        callbacks.sendEvent { it.onShowSilentHint() }
+    }
+
+    /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onScreenOff */
+    fun onScreenOff() {
+        callbacks.sendEvent { it.onScreenOff() }
+    }
+
+    /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onShowSafetyWarning */
+    fun onShowSafetyWarning(flags: Int) {
+        callbacks.sendEvent { it.onShowSafetyWarning(flags) }
+    }
+
+    /**
+     * @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onAccessibilityModeChanged
+     */
+    fun onAccessibilityModeChanged(showA11yStream: Boolean?) {
+        callbacks.sendEvent { it.onAccessibilityModeChanged(showA11yStream) }
+    }
+
+    /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onShowCsdWarning */
+    fun onShowCsdWarning(@CsdWarning csdWarning: Int, durationMs: Int) {
+        callbacks.sendEvent { it.onShowCsdWarning(csdWarning, durationMs) }
+    }
+
+    /** @see com.android.systemui.plugins.VolumeDialogController.Callbacks.onVolumeChangedFromKey */
+    fun onVolumeChangedFromKey() {
+        callbacks.sendEvent { it.onVolumeChangedFromKey() }
+    }
+
+    override fun getCaptionsEnabledState(checkForSwitchState: Boolean) {
+        error("Unsupported for the new Volume Dialog")
+    }
+
+    override fun setCaptionsEnabledState(enabled: Boolean) {
+        error("Unsupported for the new Volume Dialog")
+    }
+
+    override fun getCaptionsComponentState(fromTooltip: Boolean) {
+        error("Unsupported for the new Volume Dialog")
+    }
+}
+
+private inline fun CopyOnWriteArraySet<VolumeDialogController.Callbacks>.sendEvent(
+    event: (callback: VolumeDialogController.Callbacks) -> Unit
+) {
+    for (callback in this) {
+        event(callback)
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/VolumeDialogControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/VolumeDialogControllerKosmos.kt
new file mode 100644
index 0000000..2f6d4fa
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/VolumeDialogControllerKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.plugins
+
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.kotlin.mock
+
+val Kosmos.fakeVolumeDialogController by Kosmos.Fixture { FakeVolumeDialogController(mock {}) }
+var Kosmos.volumeDialogController: VolumeDialogController by
+    Kosmos.Fixture { fakeVolumeDialogController }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
index dbb3e38..c218ff6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.qs.footerActionsController
 import com.android.systemui.qs.footerActionsViewModelFactory
 import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor
+import com.android.systemui.qs.panels.ui.viewmodel.paginatedGridViewModel
 import com.android.systemui.qs.ui.viewmodel.quickSettingsContainerViewModel
 import com.android.systemui.shade.largeScreenHeaderHelper
 import com.android.systemui.shade.transition.largeScreenShadeInterpolator
@@ -48,6 +49,7 @@
                     configurationInteractor,
                     largeScreenHeaderHelper,
                     tileSquishinessInteractor,
+                    paginatedGridViewModel,
                     lifecycleScope,
                 )
             }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
index 020f0a6..4f414d9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
@@ -28,6 +28,7 @@
         Scenes.Bouncer,
         Scenes.Gone,
         Scenes.Communal,
+        Scenes.Dream,
     )
 }
 
@@ -49,9 +50,10 @@
             Scenes.Gone to 0,
             Scenes.Lockscreen to 0,
             Scenes.Communal to 1,
-            Scenes.Shade to 2,
-            Scenes.QuickSettings to 3,
-            Scenes.Bouncer to 4,
+            Scenes.Dream to 2,
+            Scenes.Shade to 3,
+            Scenes.QuickSettings to 4,
+            Scenes.Bouncer to 5,
         )
 
     SceneContainerConfig(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt
new file mode 100644
index 0000000..db9c48d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractorKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.volume.dialog.domain.interactor
+
+import android.os.Handler
+import android.os.looper
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.plugins.volumeDialogController
+
+val Kosmos.volumeDialogCallbacksInteractor: VolumeDialogCallbacksInteractor by
+    Kosmos.Fixture {
+        VolumeDialogCallbacksInteractor(
+            volumeDialogController = volumeDialogController,
+            coroutineScope = applicationCoroutineScope,
+            bgHandler = Handler(looper),
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
new file mode 100644
index 0000000..e73539e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractorKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.volume.dialog.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+
+val Kosmos.volumeDialogVisibilityInteractor by
+    Kosmos.Fixture {
+        VolumeDialogVisibilityInteractor(applicationCoroutineScope, volumeDialogCallbacksInteractor)
+    }
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
index ef795c6..520f050 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
@@ -37,8 +37,6 @@
     private RavenwoodCommonUtils() {
     }
 
-    private static final Object sLock = new Object();
-
     /**
      * If set to "1", we enable the verbose logging.
      *
@@ -68,9 +66,6 @@
 
     public static final String RAVENWOOD_VERSION_JAVA_SYSPROP = "android.ravenwood.version";
 
-    // @GuardedBy("sLock")
-    private static boolean sIntegrityChecked = false;
-
     /**
      * @return if we're running on Ravenwood.
      */
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
index 4e7dc5d..ad86135 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
@@ -32,25 +32,20 @@
 
     public static NativeAllocationRegistry createNonmalloced(
             ClassLoader classLoader, long freeFunction, long size) {
-        return new NativeAllocationRegistry(classLoader, freeFunction, size, false);
+        return new NativeAllocationRegistry(classLoader, freeFunction, size);
     }
 
     public static NativeAllocationRegistry createMalloced(
             ClassLoader classLoader, long freeFunction, long size) {
-        return new NativeAllocationRegistry(classLoader, freeFunction, size, true);
+        return new NativeAllocationRegistry(classLoader, freeFunction, size);
     }
 
     public static NativeAllocationRegistry createMalloced(
             ClassLoader classLoader, long freeFunction) {
-        return new NativeAllocationRegistry(classLoader, freeFunction, 0, true);
+        return new NativeAllocationRegistry(classLoader, freeFunction, 0);
     }
 
     public NativeAllocationRegistry(ClassLoader classLoader, long freeFunction, long size) {
-        this(classLoader, freeFunction, size, size == 0);
-    }
-
-    private NativeAllocationRegistry(ClassLoader classLoader, long freeFunction, long size,
-            boolean mallocAllocation) {
         if (size < 0) {
             throw new IllegalArgumentException("Invalid native allocation size: " + size);
         }
diff --git a/ravenwood/bivalenttest/Android.bp b/ravenwood/tests/bivalenttest/Android.bp
similarity index 63%
rename from ravenwood/bivalenttest/Android.bp
rename to ravenwood/tests/bivalenttest/Android.bp
index e897735..ac499b9 100644
--- a/ravenwood/bivalenttest/Android.bp
+++ b/ravenwood/tests/bivalenttest/Android.bp
@@ -54,32 +54,34 @@
     auto_gen_config: true,
 }
 
-android_test {
-    name: "RavenwoodBivalentTest_device",
+// TODO(b/371215487): migrate bivalenttest.ravenizer tests to another architecture
 
-    srcs: [
-        "test/**/*.java",
-    ],
-    static_libs: [
-        "junit",
-        "truth",
-
-        "androidx.annotation_annotation",
-        "androidx.test.ext.junit",
-        "androidx.test.rules",
-
-        "junit-params",
-        "platform-parametric-runner-lib",
-
-        "ravenwood-junit",
-    ],
-    jni_libs: [
-        "libravenwoodbivalenttest_jni",
-    ],
-    test_suites: [
-        "device-tests",
-    ],
-    optimize: {
-        enabled: false,
-    },
-}
+// android_test {
+//     name: "RavenwoodBivalentTest_device",
+//
+//     srcs: [
+//         "test/**/*.java",
+//     ],
+//     static_libs: [
+//         "junit",
+//         "truth",
+//
+//         "androidx.annotation_annotation",
+//         "androidx.test.ext.junit",
+//         "androidx.test.rules",
+//
+//         "junit-params",
+//         "platform-parametric-runner-lib",
+//
+//         "ravenwood-junit",
+//     ],
+//     jni_libs: [
+//         "libravenwoodbivalenttest_jni",
+//     ],
+//     test_suites: [
+//         "device-tests",
+//     ],
+//     optimize: {
+//         enabled: false,
+//     },
+// }
diff --git a/ravenwood/bivalenttest/AndroidManifest.xml b/ravenwood/tests/bivalenttest/AndroidManifest.xml
similarity index 100%
rename from ravenwood/bivalenttest/AndroidManifest.xml
rename to ravenwood/tests/bivalenttest/AndroidManifest.xml
diff --git a/ravenwood/bivalenttest/AndroidTest.xml b/ravenwood/tests/bivalenttest/AndroidTest.xml
similarity index 100%
rename from ravenwood/bivalenttest/AndroidTest.xml
rename to ravenwood/tests/bivalenttest/AndroidTest.xml
diff --git a/ravenwood/bivalenttest/README.md b/ravenwood/tests/bivalenttest/README.md
similarity index 100%
rename from ravenwood/bivalenttest/README.md
rename to ravenwood/tests/bivalenttest/README.md
diff --git a/ravenwood/bivalenttest/jni/ravenwood_core_test_jni.cpp b/ravenwood/tests/bivalenttest/jni/ravenwood_core_test_jni.cpp
similarity index 100%
rename from ravenwood/bivalenttest/jni/ravenwood_core_test_jni.cpp
rename to ravenwood/tests/bivalenttest/jni/ravenwood_core_test_jni.cpp
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodAndroidApiTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodAndroidApiTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodAndroidApiTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodAndroidApiTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodConfigTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodConfigTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodConfigTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodConfigTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodJniTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodJniTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodJniTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodJniTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodMultipleRuleTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodMultipleRuleTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodMultipleRuleTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodMultipleRuleTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodNativeAllocationRegistryTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodNativeAllocationRegistryTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodNativeAllocationRegistryTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodNativeAllocationRegistryTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodNoConfigNoRuleTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodNoConfigNoRuleTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodNoConfigNoRuleTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodNoConfigNoRuleTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodRuleTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodRuleTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodRuleTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodRuleTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/CallTracker.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/CallTracker.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/CallTracker.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/CallTracker.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleOrderRewriteTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleOrderRewriteTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleOrderRewriteTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleOrderRewriteTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTestBase.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTestBase.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTestBase.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTestBase.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodNoRavenizerTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodNoRavenizerTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodNoRavenizerTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodNoRavenizerTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsReallyDisabledTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsReallyDisabledTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsReallyDisabledTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsReallyDisabledTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithAndroidXRunnerTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithAndroidXRunnerTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithAndroidXRunnerTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithAndroidXRunnerTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithJUnitParamsRunnerTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithJUnitParamsRunnerTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithJUnitParamsRunnerTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithJUnitParamsRunnerTest.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithParameterizedAndroidJunit4Test.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithParameterizedAndroidJunit4Test.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithParameterizedAndroidJunit4Test.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithParameterizedAndroidJunit4Test.java
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodSuiteTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodSuiteTest.java
similarity index 100%
rename from ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodSuiteTest.java
rename to ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodSuiteTest.java
diff --git a/ravenwood/tests/coretest/Android.bp b/ravenwood/tests/coretest/Android.bp
index d94475c..85f1baf 100644
--- a/ravenwood/tests/coretest/Android.bp
+++ b/ravenwood/tests/coretest/Android.bp
@@ -17,9 +17,15 @@
         "junit-params",
         "platform-parametric-runner-lib",
         "truth",
+
+        // This library should be removed by Ravenizer
+        "mockito-target-minus-junit4",
     ],
     srcs: [
         "test/**/*.java",
     ],
+    ravenizer: {
+        strip_mockito: true,
+    },
     auto_gen_config: true,
 }
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMockitoTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMockitoTest.java
new file mode 100644
index 0000000..dd6d259
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMockitoTest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwoodtest.coretest;
+
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Test;
+
+public class RavenwoodMockitoTest {
+
+    @Test
+    public void checkMockitoClasses() {
+        // DexMaker should not exist
+        assertThrows(
+                ClassNotFoundException.class,
+                () -> Class.forName("com.android.dx.DexMaker"));
+        // Mockito 2 should not exist
+        assertThrows(
+                ClassNotFoundException.class,
+                () -> Class.forName("org.mockito.Matchers"));
+    }
+}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
index f7f9a85..49f0b59 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
@@ -85,18 +85,17 @@
 /**
  * Main class.
  */
-class Ravenizer(val options: RavenizerOptions) {
-    fun run() {
+class Ravenizer {
+    fun run(options: RavenizerOptions) {
         val stats = RavenizerStats()
 
-        val fatalValidation = options.fatalValidation.get
-
         stats.totalTime = log.nTime {
             process(
                 options.inJar.get,
                 options.outJar.get,
                 options.enableValidation.get,
-                fatalValidation,
+                options.fatalValidation.get,
+                options.stripMockito.get,
                 stats,
             )
         }
@@ -108,6 +107,7 @@
         outJar: String,
         enableValidation: Boolean,
         fatalValidation: Boolean,
+        stripMockito: Boolean,
         stats: RavenizerStats,
     ) {
         var allClasses = ClassNodes.loadClassStructures(inJar) {
@@ -126,6 +126,9 @@
                 }
             }
         }
+        if (includeUnsupportedMockito(allClasses)) {
+            log.w("Unsupported Mockito detected in $inJar}!")
+        }
 
         stats.totalProcessTime = log.vTime("$executableName processing $inJar") {
             ZipFile(inJar).use { inZip ->
@@ -145,6 +148,11 @@
                             )
                         }
 
+                        if (stripMockito && entry.name.isMockitoFile()) {
+                            // Skip this entry
+                            continue
+                        }
+
                         val className = zipEntryNameToClassName(entry.name)
 
                         if (className != null) {
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
index ff41818..aee4530 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
@@ -36,6 +36,6 @@
         log.v("Options: $options")
 
         // Run.
-        Ravenizer(options).run()
+        Ravenizer().run(options)
     }
 }
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
index 10fe0a3..32dcbe5 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
@@ -47,6 +47,9 @@
 
     /** Whether the validation failure is fatal or not. */
     var fatalValidation: SetOnce<Boolean> = SetOnce(false),
+
+    /** Whether to remove mockito and dexmaker classes. */
+    var stripMockito: SetOnce<Boolean> = SetOnce(false),
 ) {
     companion object {
 
@@ -85,6 +88,9 @@
                         "--fatal-validation" -> ret.fatalValidation.set(true)
                         "--no-fatal-validation" -> ret.fatalValidation.set(false)
 
+                        "--strip-mockito" -> ret.stripMockito.set(true)
+                        "--no-strip-mockito" -> ret.stripMockito.set(false)
+
                         else -> throw ArgumentsException("Unknown option: $arg")
                     }
                 } catch (e: SetOnce.SetMoreThanOnceException) {
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
index 1aa70c08..37a7975 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
@@ -100,3 +100,19 @@
         // TODO -- anything else?
     )
 }
+
+/**
+ * Files that should be removed when "--strip-mockito" is set.
+ */
+fun String.isMockitoFile(): Boolean {
+    return this.startsWithAny(
+        "org/mockito/", // Mockito
+        "com/android/dx/", // DexMaker
+        "mockito-extensions/", // DexMaker overrides
+    )
+}
+
+fun includeUnsupportedMockito(classes: ClassNodes): Boolean {
+    return classes.findClass("com/android/dx/DexMaker") != null
+            || classes.findClass("org/mockito/Matchers") != null
+}
diff --git a/services/appfunctions/TEST_MAPPING b/services/appfunctions/TEST_MAPPING
index 91cfa06..851d754 100644
--- a/services/appfunctions/TEST_MAPPING
+++ b/services/appfunctions/TEST_MAPPING
@@ -2,11 +2,6 @@
   "presubmit": [
     {
       "name": "FrameworksAppFunctionsTests"
-    }
-  ],
-  "postsubmit": [
-    {
-      "name": "FrameworksAppFunctionsTests"
     },
     {
       "name": "CtsAppFunctionTestCases"
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index d0c3daf..d31ced3 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -16,8 +16,6 @@
 
 package com.android.server.appfunctions;
 
-import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DISABLED;
-import static android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_ENABLED;
 import static android.app.appfunctions.AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_METADATA_DB;
 import static android.app.appfunctions.AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_NAMESPACE;
 
@@ -363,26 +361,14 @@
                                     callingPackage,
                                     functionIdentifier,
                                     runtimeMetadataSearchSession));
-            AppFunctionRuntimeMetadata.Builder newMetadata =
-                    new AppFunctionRuntimeMetadata.Builder(existingMetadata);
-            switch (enabledState) {
-                case AppFunctionManager.APP_FUNCTION_STATE_DEFAULT -> {
-                    newMetadata.setEnabled(null);
-                }
-                case APP_FUNCTION_STATE_ENABLED -> {
-                    newMetadata.setEnabled(true);
-                }
-                case APP_FUNCTION_STATE_DISABLED -> {
-                    newMetadata.setEnabled(false);
-                }
-                default ->
-                        throw new IllegalArgumentException("Value of EnabledState is unsupported.");
-            }
+            AppFunctionRuntimeMetadata newMetadata =
+                    new AppFunctionRuntimeMetadata.Builder(existingMetadata)
+                            .setEnabled(enabledState).build();
             AppSearchBatchResult<String, Void> putDocumentBatchResult =
                     runtimeMetadataSearchSession
                             .put(
                                     new PutDocumentsRequest.Builder()
-                                            .addGenericDocuments(newMetadata.build())
+                                            .addGenericDocuments(newMetadata)
                                             .build())
                             .get();
             if (!putDocumentBatchResult.isSuccess()) {
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index f32031de..7daf158 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -17,6 +17,7 @@
 package com.android.server;
 
 import static android.app.Flags.modesApi;
+import static android.app.Flags.enableCurrentModeTypeBinderCache;
 import static android.app.Flags.enableNightModeBinderCache;
 import static android.app.UiModeManager.ContrastUtils.CONTRAST_DEFAULT_VALUE;
 import static android.app.UiModeManager.DEFAULT_PRIORITY;
@@ -138,7 +139,7 @@
 
     private int mLastBroadcastState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
 
-    private final NightMode mNightMode = new NightMode(){
+    private final IntProperty mNightMode = new IntProperty(){
         private int mNightModeValue = UiModeManager.MODE_NIGHT_NO;
 
         @Override
@@ -192,7 +193,22 @@
     // flag set by resource, whether to night mode change for normal all or not.
     private boolean mNightModeLocked = false;
 
-    int mCurUiMode = 0;
+    private final IntProperty mCurUiMode = new IntProperty(){
+        private int mCurrentModeTypeValue = 0;
+
+        @Override
+        public int get() {
+            return mCurrentModeTypeValue;
+        }
+
+        @Override
+        public void set(int mode) {
+            mCurrentModeTypeValue = mode;
+            if (enableCurrentModeTypeBinderCache()) {
+                UiModeManager.invalidateCurrentModeTypeCache();
+            }
+        }
+    };
     private int mSetUiMode = 0;
     private boolean mHoldingConfiguration = false;
     private int mCurrentUser;
@@ -810,7 +826,7 @@
             final long ident = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
-                    return mCurUiMode & Configuration.UI_MODE_TYPE_MASK;
+                    return mCurUiMode.get() & Configuration.UI_MODE_TYPE_MASK;
                 }
             } finally {
                 Binder.restoreCallingIdentity(ident);
@@ -1492,7 +1508,7 @@
             pw.print(" mCarModeEnableFlags="); pw.print(mCarModeEnableFlags);
             pw.print(" mEnableCarDockLaunch="); pw.println(mEnableCarDockLaunch);
 
-            pw.print("  mCurUiMode=0x"); pw.print(Integer.toHexString(mCurUiMode));
+            pw.print("  mCurUiMode=0x"); pw.print(Integer.toHexString(mCurUiMode.get()));
             pw.print(" mUiModeLocked="); pw.print(mUiModeLocked);
             pw.print(" mSetUiMode=0x"); pw.println(Integer.toHexString(mSetUiMode));
 
@@ -1745,7 +1761,7 @@
                     + "; uiMode=" + uiMode);
         }
 
-        mCurUiMode = uiMode;
+        mCurUiMode.set(uiMode);
         if (!mHoldingConfiguration && (!mWaitForDeviceInactive || mPowerSave)) {
             mConfiguration.uiMode = uiMode;
         }
@@ -1892,7 +1908,7 @@
         boolean keepScreenOn = mCharging &&
                 ((mCarModeEnabled && mCarModeKeepsScreenOn &&
                 (mCarModeEnableFlags & UiModeManager.ENABLE_CAR_MODE_ALLOW_SLEEP) == 0) ||
-                (mCurUiMode == Configuration.UI_MODE_TYPE_DESK && mDeskModeKeepsScreenOn));
+                (mCurUiMode.get() == Configuration.UI_MODE_TYPE_DESK && mDeskModeKeepsScreenOn));
         if (keepScreenOn != mWakeLock.isHeld()) {
             if (keepScreenOn) {
                 mWakeLock.acquire();
@@ -2319,11 +2335,12 @@
     }
 
     /**
-     * Interface to contain the value for system night mode. We make the night mode accessible
-     * through this class to ensure that the reassignment of this value invalidates the cache.
+     * Interface to contain the value for an integral property. We make the property
+     * accessible through this class to ensure that the reassignment of this value invalidates the
+     * cache.
      */
-    private interface NightMode {
+    private interface IntProperty {
         int get();
-        void set(int mode);
+        void set(int value);
     }
 }
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index cc66378..7873d34 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -198,13 +198,10 @@
 
 flag {
     name: "logcat_longer_timeout"
-    namespace: "backstage_power"
+    namespace: "stability"
     description: "Wait longer during the logcat gathering operation"
     bug: "292533246"
     is_fixed_read_only: true
-    metadata {
-        purpose: PURPOSE_BUGFIX
-    }
 }
 
 flag {
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index e8d14cb..9b9be4c 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -832,8 +832,9 @@
                                       FullyQualifiedGroupKey newGroup) { }
 
     /**
-     * Called when a notification channel is updated, so that this helper can adjust
-     * the aggregate groups by moving children if their section has changed.
+     * Called when a notification channel is updated (channel attributes have changed),
+     * so that this helper can adjust the aggregate groups by moving children
+     * if their section has changed.
      * see {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)}
      * @param userId the userId of the channel
      * @param pkgName the channel's package
@@ -853,24 +854,48 @@
                 }
             }
 
-            // The list of notification operations required after the channel update
-            final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>();
+            regroupNotifications(userId, pkgName, notificationsToCheck);
+        }
+    }
 
-            // Check any already auto-grouped notifications that may need to be re-grouped
-            // after the channel update
-            notificationsToMove.addAll(
-                    getAutogroupedNotificationsMoveOps(userId, pkgName,
-                        notificationsToCheck));
+    /**
+     * Called when an individuial notification's channel is updated (moved to a new channel),
+     * so that this helper can adjust the aggregate groups by moving children
+     * if their section has changed.
+     * see {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)}
+     *
+     * @param record the notification which had its channel updated
+     */
+    @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING)
+    public void onChannelUpdated(final NotificationRecord record) {
+        synchronized (mAggregatedNotifications) {
+            ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>();
+            notificationsToCheck.put(record.getKey(), record);
+            regroupNotifications(record.getUserId(), record.getSbn().getPackageName(),
+                    notificationsToCheck);
+        }
+    }
 
-            // Check any ungrouped notifications that may need to be auto-grouped
-            // after the channel update
-            notificationsToMove.addAll(
-                    getUngroupedNotificationsMoveOps(userId, pkgName, notificationsToCheck));
+    @GuardedBy("mAggregatedNotifications")
+    private void regroupNotifications(int userId, String pkgName,
+            ArrayMap<String, NotificationRecord> notificationsToCheck) {
+        // The list of notification operations required after the channel update
+        final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>();
 
-            // Batch move to new section
-            if (!notificationsToMove.isEmpty()) {
-                moveNotificationsToNewSection(userId, pkgName, notificationsToMove);
-            }
+        // Check any already auto-grouped notifications that may need to be re-grouped
+        // after the channel update
+        notificationsToMove.addAll(
+                getAutogroupedNotificationsMoveOps(userId, pkgName,
+                    notificationsToCheck));
+
+        // Check any ungrouped notifications that may need to be auto-grouped
+        // after the channel update
+        notificationsToMove.addAll(
+                getUngroupedNotificationsMoveOps(userId, pkgName, notificationsToCheck));
+
+        // Batch move to new section
+        if (!notificationsToMove.isEmpty()) {
+            moveNotificationsToNewSection(userId, pkgName, notificationsToMove);
         }
     }
 
diff --git a/services/core/java/com/android/server/notification/NotificationAdjustmentExtractor.java b/services/core/java/com/android/server/notification/NotificationAdjustmentExtractor.java
index 97bbc23..2dd4f83 100644
--- a/services/core/java/com/android/server/notification/NotificationAdjustmentExtractor.java
+++ b/services/core/java/com/android/server/notification/NotificationAdjustmentExtractor.java
@@ -15,6 +15,9 @@
 */
 package com.android.server.notification;
 
+import static android.service.notification.Adjustment.KEY_TYPE;
+import static android.service.notification.Flags.notificationForceGrouping;
+
 import android.content.Context;
 import android.util.Slog;
 
@@ -24,6 +27,7 @@
 public class NotificationAdjustmentExtractor implements NotificationSignalExtractor {
     private static final String TAG = "AdjustmentExtractor";
     private static final boolean DBG = false;
+    private GroupHelper mGroupHelper;
 
 
     public void initialize(Context ctx, NotificationUsageStats usageStats) {
@@ -35,8 +39,27 @@
             if (DBG) Slog.d(TAG, "skipping empty notification");
             return null;
         }
+
+        final boolean hasAdjustedClassification = record.hasAdjustment(KEY_TYPE);
         record.applyAdjustments();
 
+        if (notificationForceGrouping()
+                && android.service.notification.Flags.notificationClassification()) {
+            // Classification adjustments trigger regrouping
+            if (mGroupHelper != null && hasAdjustedClassification) {
+                return new RankingReconsideration(record.getKey(), 0) {
+                    @Override
+                    public void work() {
+                    }
+
+                    @Override
+                    public void applyChangesLocked(NotificationRecord record) {
+                        mGroupHelper.onChannelUpdated(record);
+                    }
+                };
+            }
+        }
+
         return null;
     }
 
@@ -49,4 +72,9 @@
     public void setZenHelper(ZenModeHelper helper) {
 
     }
+
+    @Override
+    public void setGroupHelper(GroupHelper groupHelper) {
+        mGroupHelper = groupHelper;
+    }
 }
diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
index 06f419a..ea4a6db 100644
--- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
+++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
@@ -17,6 +17,7 @@
 package com.android.server.notification;
 
 import static android.app.Flags.sortSectionByTime;
+import static android.app.Notification.CATEGORY_MESSAGE;
 import static android.app.Notification.FLAG_INSISTENT;
 import static android.app.Notification.FLAG_ONLY_ALERT_ONCE;
 import static android.app.NotificationManager.IMPORTANCE_MIN;
@@ -42,6 +43,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
+import android.content.pm.ShortcutInfo;
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.database.ContentObserver;
@@ -1641,7 +1643,7 @@
             }
 
             // recent conversation
-            if (record.isConversation()
+            if ((record.isConversation() || isConversationMessage(record))
                     && record.getNotification().getWhen() > mLastAvalancheTriggerTimestamp) {
                 return true;
             }
@@ -1656,6 +1658,21 @@
 
             return false;
         }
+
+        // Relaxed signals for conversations messages
+        private boolean isConversationMessage(final NotificationRecord record) {
+            if (!CATEGORY_MESSAGE.equals(record.getSbn().getNotification().category)) {
+                return false;
+            }
+            if (record.getChannel().isDemoted()) {
+                return false;
+            }
+            final ShortcutInfo shortcut = record.getShortcutInfo();
+            if (shortcut == null) {
+                return false;
+            }
+            return true;
+        }
     }
 
     //======================  Observers  =============================
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 56e0a89..99e66a2 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -519,6 +519,7 @@
 
     private static final long DELAY_FORCE_REGROUP_TIME = 3000;
 
+
     private static final String ACTION_NOTIFICATION_TIMEOUT =
             NotificationManagerService.class.getSimpleName() + ".TIMEOUT";
     private static final int REQUEST_CODE_TIMEOUT = 1;
@@ -2583,7 +2584,7 @@
                 mShowReviewPermissionsNotification,
                 Clock.systemUTC());
         mRankingHelper = new RankingHelper(getContext(), mRankingHandler, mPreferencesHelper,
-                mZenModeHelper, mUsageStats, extractorNames, mPlatformCompat);
+                mZenModeHelper, mUsageStats, extractorNames, mPlatformCompat, groupHelper);
         mSnoozeHelper = snoozeHelper;
         mGroupHelper = groupHelper;
         mHistoryManager = historyManager;
@@ -6871,22 +6872,9 @@
             }
             if (android.service.notification.Flags.notificationClassification()
                     && adjustments.containsKey(KEY_TYPE)) {
-                NotificationChannel newChannel = null;
-                int type = adjustments.getInt(KEY_TYPE);
-                if (TYPE_NEWS == type) {
-                    newChannel = mPreferencesHelper.getNotificationChannel(
-                            r.getSbn().getPackageName(), r.getUid(), NEWS_ID, false);
-                } else if (TYPE_PROMOTION == type) {
-                    newChannel = mPreferencesHelper.getNotificationChannel(
-                            r.getSbn().getPackageName(), r.getUid(), PROMOTIONS_ID, false);
-                } else if (TYPE_SOCIAL_MEDIA == type) {
-                    newChannel = mPreferencesHelper.getNotificationChannel(
-                            r.getSbn().getPackageName(), r.getUid(), SOCIAL_MEDIA_ID, false);
-                } else if (TYPE_CONTENT_RECOMMENDATION == type) {
-                    newChannel = mPreferencesHelper.getNotificationChannel(
-                            r.getSbn().getPackageName(), r.getUid(), RECS_ID, false);
-                }
-                if (newChannel == null) {
+                final NotificationChannel newChannel = getClassificationChannelLocked(r,
+                        adjustments);
+                if (newChannel == null || newChannel.getId().equals(r.getChannel().getId())) {
                     adjustments.remove(KEY_TYPE);
                 } else {
                     // swap app provided type with the real thing
@@ -6902,6 +6890,27 @@
         }
     }
 
+    @GuardedBy("mNotificationLock")
+    @Nullable
+    private NotificationChannel getClassificationChannelLocked(NotificationRecord r,
+            Bundle adjustments) {
+        int type = adjustments.getInt(KEY_TYPE);
+        if (TYPE_NEWS == type) {
+            return mPreferencesHelper.getNotificationChannel(
+                    r.getSbn().getPackageName(), r.getUid(), NEWS_ID, false);
+        } else if (TYPE_PROMOTION == type) {
+            return mPreferencesHelper.getNotificationChannel(
+                    r.getSbn().getPackageName(), r.getUid(), PROMOTIONS_ID, false);
+        } else if (TYPE_SOCIAL_MEDIA == type) {
+            return mPreferencesHelper.getNotificationChannel(
+                    r.getSbn().getPackageName(), r.getUid(), SOCIAL_MEDIA_ID, false);
+        } else if (TYPE_CONTENT_RECOMMENDATION == type) {
+            return mPreferencesHelper.getNotificationChannel(
+                    r.getSbn().getPackageName(), r.getUid(), RECS_ID, false);
+        }
+        return null;
+    }
+
     @SuppressWarnings("GuardedBy")
     @GuardedBy("mNotificationLock")
     void addAutogroupKeyLocked(String key, String groupName, boolean requestSort) {
diff --git a/services/core/java/com/android/server/notification/NotificationSignalExtractor.java b/services/core/java/com/android/server/notification/NotificationSignalExtractor.java
index f0358d1..be34bee 100644
--- a/services/core/java/com/android/server/notification/NotificationSignalExtractor.java
+++ b/services/core/java/com/android/server/notification/NotificationSignalExtractor.java
@@ -55,4 +55,9 @@
     void setZenHelper(ZenModeHelper helper);
 
     default void setCompatChangeLogger(IPlatformCompat platformCompat){};
+
+    /**
+     * @param groupHelper Helper for auto-grouping notifications
+     */
+    default void setGroupHelper(GroupHelper groupHelper){};
 }
diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java
index 03dd935..f06d6405 100644
--- a/services/core/java/com/android/server/notification/RankingHelper.java
+++ b/services/core/java/com/android/server/notification/RankingHelper.java
@@ -23,7 +23,6 @@
 import static android.text.TextUtils.formatSimple;
 
 import android.annotation.NonNull;
-import android.app.NotificationManager;
 import android.content.Context;
 import android.service.notification.RankingHelperProto;
 import android.util.ArrayMap;
@@ -61,7 +60,7 @@
             })
     public RankingHelper(Context context, RankingHandler rankingHandler, RankingConfig config,
             ZenModeHelper zenHelper, NotificationUsageStats usageStats, String[] extractorNames,
-            IPlatformCompat platformCompat) {
+            IPlatformCompat platformCompat, GroupHelper groupHelper) {
         mContext = context;
         mRankingHandler = rankingHandler;
         if (sortSectionByTime()) {
@@ -80,6 +79,7 @@
                 extractor.initialize(mContext, usageStats);
                 extractor.setConfig(config);
                 extractor.setZenHelper(zenHelper);
+                extractor.setGroupHelper(groupHelper);
                 if (restrictAudioAttributesAlarm() || restrictAudioAttributesMedia()
                         || restrictAudioAttributesCall()) {
                     extractor.setCompatChangeLogger(platformCompat);
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 68e781f..e47b4c2 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1141,9 +1141,9 @@
                         + mShortPressOnPowerBehavior);
 
         if (count == 2) {
-            powerMultiPressAction(displayId, eventTime, interactive, mDoublePressOnPowerBehavior);
+            powerMultiPressAction(eventTime, interactive, mDoublePressOnPowerBehavior);
         } else if (count == 3) {
-            powerMultiPressAction(displayId, eventTime, interactive, mTriplePressOnPowerBehavior);
+            powerMultiPressAction(eventTime, interactive, mTriplePressOnPowerBehavior);
         } else if (count > 3 && count <= getMaxMultiPressPowerCount()) {
             Slog.d(TAG, "No behavior defined for power press count " + count);
         } else if (count == 1 && shouldHandleShortPressPowerAction(interactive, eventTime)) {
@@ -1307,8 +1307,7 @@
         }
     }
 
-    private void powerMultiPressAction(int displayId, long eventTime, boolean interactive,
-            int behavior) {
+    private void powerMultiPressAction(long eventTime, boolean interactive, int behavior) {
         switch (behavior) {
             case MULTI_PRESS_POWER_NOTHING:
                 break;
@@ -1323,7 +1322,7 @@
                     Settings.Global.putInt(mContext.getContentResolver(),
                             Settings.Global.THEATER_MODE_ON, 0);
                     if (!interactive) {
-                        wakeUpFromWakeKey(displayId, eventTime, KEYCODE_POWER, /* isDown= */ false);
+                        wakeUpFromWakeKey(eventTime, KEYCODE_POWER, /* isDown= */ false);
                     }
                 } else {
                     Slog.i(TAG, "Toggling theater mode on.");
@@ -1339,7 +1338,7 @@
             case MULTI_PRESS_POWER_BRIGHTNESS_BOOST:
                 Slog.i(TAG, "Starting brightness boost.");
                 if (!interactive) {
-                    wakeUpFromWakeKey(displayId, eventTime, KEYCODE_POWER, /* isDown= */ false);
+                    wakeUpFromWakeKey(eventTime, KEYCODE_POWER, /* isDown= */ false);
                 }
                 mPowerManager.boostScreenBrightness(eventTime);
                 break;
@@ -5547,7 +5546,7 @@
         if (mRequestedOrSleepingDefaultDisplay) {
             mCameraGestureTriggeredDuringGoingToSleep = true;
             // Wake device up early to prevent display doing redundant turning off/on stuff.
-            mWindowWakeUpPolicy.wakeUpFromPowerKeyCameraGesture(event.getDisplayId());
+            mWindowWakeUpPolicy.wakeUpFromPowerKeyCameraGesture();
         }
         return true;
     }
@@ -5645,8 +5644,8 @@
     public int interceptMotionBeforeQueueingNonInteractive(int displayId, int source, int action,
             long whenNanos, int policyFlags) {
         if ((policyFlags & FLAG_WAKE) != 0) {
-            if (mWindowWakeUpPolicy.wakeUpFromMotion(displayId, whenNanos / 1000000, source,
-                    action == MotionEvent.ACTION_DOWN)) {
+            if (mWindowWakeUpPolicy.wakeUpFromMotion(
+                        whenNanos / 1000000, source, action == MotionEvent.ACTION_DOWN)) {
                 // Woke up. Pass motion events to user.
                 return ACTION_PASS_TO_USER;
             }
@@ -5660,8 +5659,8 @@
         // there will be no dream to intercept the touch and wake into ambient.  The device should
         // wake up in this case.
         if (isTheaterModeEnabled() && (policyFlags & FLAG_WAKE) != 0) {
-            if (mWindowWakeUpPolicy.wakeUpFromMotion(displayId, whenNanos / 1000000, source,
-                    action == MotionEvent.ACTION_DOWN)) {
+            if (mWindowWakeUpPolicy.wakeUpFromMotion(
+                        whenNanos / 1000000, source, action == MotionEvent.ACTION_DOWN)) {
                 // Woke up. Pass motion events to user.
                 return ACTION_PASS_TO_USER;
             }
@@ -6003,14 +6002,13 @@
             return;
         }
         wakeUpFromWakeKey(
-                event.getDisplayId(),
                 event.getEventTime(),
                 event.getKeyCode(),
                 event.getAction() == KeyEvent.ACTION_DOWN);
     }
 
-    private void wakeUpFromWakeKey(int displayId, long eventTime, int keyCode, boolean isDown) {
-        if (mWindowWakeUpPolicy.wakeUpFromKey(displayId, eventTime, keyCode, isDown)) {
+    private void wakeUpFromWakeKey(long eventTime, int keyCode, boolean isDown) {
+        if (mWindowWakeUpPolicy.wakeUpFromKey(eventTime, keyCode, isDown)) {
             final boolean keyCanLaunchHome = keyCode == KEYCODE_HOME || keyCode == KEYCODE_POWER;
             // Start HOME with "reason" extra if sleeping for more than mWakeUpToLastStateTimeout
             if (shouldWakeUpWithHomeIntent() &&  keyCanLaunchHome) {
diff --git a/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java b/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java
index 966d84f4..af1ad13 100644
--- a/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowWakeUpPolicy.java
@@ -25,7 +25,6 @@
 import static android.view.KeyEvent.KEYCODE_POWER;
 
 import static com.android.server.policy.Flags.supportInputWakeupDelegate;
-import static com.android.server.power.feature.flags.Flags.perDisplayWakeByTouch;
 
 import android.annotation.Nullable;
 import android.content.Context;
@@ -108,14 +107,13 @@
     /**
      * Wakes up from a key event.
      *
-     * @param displayId the id of the display to wake.
      * @param eventTime the timestamp of the event in {@link SystemClock#uptimeMillis()}.
      * @param keyCode the {@link android.view.KeyEvent} key code of the key event.
      * @param isDown {@code true} if the event's action is {@link KeyEvent#ACTION_DOWN}.
      * @return {@code true} if the policy allows the requested wake up and the request has been
      *      executed; {@code false} otherwise.
      */
-    boolean wakeUpFromKey(int displayId, long eventTime, int keyCode, boolean isDown) {
+    boolean wakeUpFromKey(long eventTime, int keyCode, boolean isDown) {
         final boolean wakeAllowedDuringTheaterMode =
                 keyCode == KEYCODE_POWER
                         ? mAllowTheaterModeWakeFromPowerKey
@@ -129,7 +127,6 @@
             return true;
         }
         wakeUp(
-                displayId,
                 eventTime,
                 keyCode == KEYCODE_POWER ? WAKE_REASON_POWER_BUTTON : WAKE_REASON_WAKE_KEY,
                 keyCode == KEYCODE_POWER ? "POWER" : "KEY");
@@ -139,13 +136,12 @@
     /**
      * Wakes up from a motion event.
      *
-     * @param displayId the id of the display to wake.
      * @param eventTime the timestamp of the event in {@link SystemClock#uptimeMillis()}.
      * @param isDown {@code true} if the event's action is {@link MotionEvent#ACTION_DOWN}.
      * @return {@code true} if the policy allows the requested wake up and the request has been
      *      executed; {@code false} otherwise.
      */
-    boolean wakeUpFromMotion(int displayId, long eventTime, int source, boolean isDown) {
+    boolean wakeUpFromMotion(long eventTime, int source, boolean isDown) {
         if (!canWakeUp(mAllowTheaterModeWakeFromMotion)) {
             if (DEBUG) Slog.d(TAG, "Unable to wake up from motion.");
             return false;
@@ -154,7 +150,7 @@
                 && mInputWakeUpDelegate.wakeUpFromMotion(eventTime, source, isDown)) {
             return true;
         }
-        wakeUp(displayId, eventTime, WAKE_REASON_WAKE_MOTION, "MOTION");
+        wakeUp(eventTime, WAKE_REASON_WAKE_MOTION, "MOTION");
         return true;
     }
 
@@ -170,7 +166,7 @@
             if (DEBUG) Slog.d(TAG, "Unable to wake up from camera cover.");
             return false;
         }
-        wakeUp(Display.DEFAULT_DISPLAY, eventTime, WAKE_REASON_CAMERA_LAUNCH, "CAMERA_COVER");
+        wakeUp(eventTime, WAKE_REASON_CAMERA_LAUNCH, "CAMERA_COVER");
         return true;
     }
 
@@ -185,24 +181,22 @@
             if (DEBUG) Slog.d(TAG, "Unable to wake up from lid.");
             return false;
         }
-        wakeUp(Display.DEFAULT_DISPLAY, mClock.uptimeMillis(), WAKE_REASON_LID, "LID");
+        wakeUp(mClock.uptimeMillis(), WAKE_REASON_LID, "LID");
         return true;
     }
 
     /**
      * Wakes up to prevent sleeping when opening camera through power button.
      *
-     * @param displayId the id of the display to wake.
      * @return {@code true} if the policy allows the requested wake up and the request has been
      *      executed; {@code false} otherwise.
      */
-    boolean wakeUpFromPowerKeyCameraGesture(int displayId) {
+    boolean wakeUpFromPowerKeyCameraGesture() {
         if (!canWakeUp(mAllowTheaterModeWakeFromPowerKey)) {
             if (DEBUG) Slog.d(TAG, "Unable to wake up from power key camera gesture.");
             return false;
         }
-        wakeUp(displayId, mClock.uptimeMillis(), WAKE_REASON_CAMERA_LAUNCH,
-                "CAMERA_GESTURE_PREVENT_LOCK");
+        wakeUp(mClock.uptimeMillis(), WAKE_REASON_CAMERA_LAUNCH, "CAMERA_GESTURE_PREVENT_LOCK");
         return true;
     }
 
@@ -217,7 +211,7 @@
             if (DEBUG) Slog.d(TAG, "Unable to wake up from gesture.");
             return false;
         }
-        wakeUp(Display.DEFAULT_DISPLAY, mClock.uptimeMillis(), WAKE_REASON_GESTURE, "GESTURE");
+        wakeUp(mClock.uptimeMillis(), WAKE_REASON_GESTURE, "GESTURE");
         return true;
     }
 
@@ -240,11 +234,7 @@
     }
 
     /** Wakes up {@link PowerManager}. */
-    private void wakeUp(int displayId, long wakeTime, @WakeReason int reason, String details) {
-        if (perDisplayWakeByTouch()) {
-            mPowerManager.wakeUp(wakeTime, reason, "android.policy:" + details, displayId);
-        } else {
-            mPowerManager.wakeUp(wakeTime, reason, "android.policy:" + details);
-        }
+    private void wakeUp(long wakeTime, @WakeReason int reason, String details) {
+        mPowerManager.wakeUp(wakeTime, reason, "android.policy:" + details);
     }
 }
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 4fae798..eb62b56 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -709,7 +709,7 @@
         SparseBooleanArray newDisplayInteractivities = new SparseBooleanArray();
         for (int i = 0; i < displaysByGroupId.size(); i++) {
             final int groupId = displaysByGroupId.keyAt(i);
-            for (int displayId : displaysByGroupId.get(i)) {
+            for (int displayId : displaysByGroupId.get(groupId)) {
                 // If we already know display interactivity, use that
                 if (mDisplayInteractivities.indexOfKey(displayId) > 0) {
                     newDisplayInteractivities.put(
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 2972771..0b36c7e 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -6429,11 +6429,7 @@
             // and the token could be null.
             return;
         }
-        final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy
-                .getAppCompatCameraPolicy(r);
-        if (cameraPolicy != null) {
-            cameraPolicy.onActivityRefreshed(r);
-        }
+        AppCompatCameraPolicy.onActivityRefreshed(r);
     }
 
     static void splashScreenAttachedLocked(IBinder token) {
@@ -9476,11 +9472,7 @@
             return;
         }
 
-        final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
-                this);
-        if (cameraPolicy != null) {
-            cameraPolicy.onActivityConfigurationChanging(this, newConfig, lastReportedConfig);
-        }
+        AppCompatCameraPolicy.onActivityConfigurationChanging(this, newConfig, lastReportedConfig);
     }
 
     /** Get process configuration, or global config if the process is not set. */
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
index 0e66629..d59046f 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
@@ -255,13 +255,10 @@
                 mActivityRecord.getOverrideOrientation());
         final AppCompatCameraOverrides cameraOverrides =
                 mActivityRecord.mAppCompatController.getAppCompatCameraOverrides();
-        final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
-                mActivityRecord);
         // Don't resize to split screen size when in book mode if letterbox position is centered
         return (isBookMode && isNotCenteredHorizontally || isTabletopMode && isLandscape)
                 || cameraOverrides.isCameraCompatSplitScreenAspectRatioAllowed()
-                && (cameraPolicy != null
-                    && cameraPolicy.isTreatmentEnabledForActivity(mActivityRecord));
+                && AppCompatCameraPolicy.isTreatmentEnabledForActivity(mActivityRecord);
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
index 3b023fe..548c0a3 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java
@@ -74,11 +74,9 @@
             @NonNull Rect parentBounds) {
         // If in camera compat mode, aspect ratio from the camera compat policy has priority over
         // default letterbox aspect ratio.
-        final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
-                mActivityRecord);
-        if (cameraPolicy != null && cameraPolicy.shouldCameraCompatControlAspectRatio(
+        if (AppCompatCameraPolicy.shouldCameraCompatControlAspectRatio(
                 mActivityRecord)) {
-            return cameraPolicy.getCameraCompatAspectRatio(mActivityRecord);
+            return AppCompatCameraPolicy.getCameraCompatAspectRatio(mActivityRecord);
         }
 
         final float letterboxAspectRatioOverride =
@@ -128,12 +126,8 @@
         if (aspectRatioOverrides.shouldApplyUserMinAspectRatioOverride()) {
             return aspectRatioOverrides.getUserMinAspectRatio();
         }
-        final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
-                mActivityRecord);
-        final boolean shouldOverrideMinAspectRatioForCamera = cameraPolicy != null
-                && cameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord);
         if (!aspectRatioOverrides.shouldOverrideMinAspectRatio()
-                && !shouldOverrideMinAspectRatioForCamera) {
+                && !AppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord)) {
             if (mActivityRecord.isUniversalResizeable()) {
                 return 0;
             }
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
index f6090eb..1d00136 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
@@ -70,9 +70,10 @@
         }
     }
 
-    void onActivityRefreshed(@NonNull ActivityRecord activity) {
-        if (mActivityRefresher != null) {
-            mActivityRefresher.onActivityRefreshed(activity);
+    static void onActivityRefreshed(@NonNull ActivityRecord activity) {
+        final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+        if (cameraPolicy != null && cameraPolicy.mActivityRefresher != null) {
+            cameraPolicy.mActivityRefresher.onActivityRefreshed(activity);
         }
     }
 
@@ -88,10 +89,11 @@
      * camera preview and can lead to sideways or stretching issues persisting even after force
      * rotation.
      */
-    void onActivityConfigurationChanging(@NonNull ActivityRecord activity,
+    static void onActivityConfigurationChanging(@NonNull ActivityRecord activity,
             @NonNull Configuration newConfig, @NonNull Configuration lastReportedConfig) {
-        if (mActivityRefresher != null) {
-            mActivityRefresher.onActivityConfigurationChanging(activity, newConfig,
+        final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+        if (cameraPolicy != null && cameraPolicy.mActivityRefresher != null) {
+            cameraPolicy.mActivityRefresher.onActivityConfigurationChanging(activity, newConfig,
                     lastReportedConfig);
         }
     }
@@ -108,11 +110,11 @@
         }
     }
 
-    boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord activity) {
-        if (mDisplayRotationCompatPolicy != null) {
-            return mDisplayRotationCompatPolicy.isActivityEligibleForOrientationOverride(activity);
-        }
-        return false;
+    static boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord activity) {
+        final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+        return cameraPolicy != null && cameraPolicy.mDisplayRotationCompatPolicy != null
+                && cameraPolicy.mDisplayRotationCompatPolicy
+                        .isActivityEligibleForOrientationOverride(activity);
     }
 
     /**
@@ -125,11 +127,11 @@
      *     <li>The activity has fixed orientation but not "locked" or "nosensor" one.
      * </ul>
      */
-    boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) {
-        if (mDisplayRotationCompatPolicy != null) {
-            return mDisplayRotationCompatPolicy.isTreatmentEnabledForActivity(activity);
-        }
-        return false;
+    static boolean isTreatmentEnabledForActivity(@NonNull ActivityRecord activity) {
+        final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+        return cameraPolicy != null && cameraPolicy.mDisplayRotationCompatPolicy != null
+                && cameraPolicy.mDisplayRotationCompatPolicy
+                        .isTreatmentEnabledForActivity(activity);
     }
 
     void start() {
@@ -176,23 +178,31 @@
     }
 
     // TODO(b/369070416): have policies implement the same interface.
-    boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) {
-        return (mDisplayRotationCompatPolicy != null
-                        && mDisplayRotationCompatPolicy.shouldCameraCompatControlOrientation(
-                                activity))
-                || (mCameraCompatFreeformPolicy != null
-                        && mCameraCompatFreeformPolicy.shouldCameraCompatControlOrientation(
-                                activity));
+    static boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) {
+        final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+        if (cameraPolicy == null) {
+            return false;
+        }
+        return (cameraPolicy.mDisplayRotationCompatPolicy != null
+                        && cameraPolicy.mDisplayRotationCompatPolicy
+                                .shouldCameraCompatControlOrientation(activity))
+                || (cameraPolicy.mCameraCompatFreeformPolicy != null
+                        && cameraPolicy.mCameraCompatFreeformPolicy
+                                .shouldCameraCompatControlOrientation(activity));
     }
 
     // TODO(b/369070416): have policies implement the same interface.
-    boolean shouldCameraCompatControlAspectRatio(@NonNull ActivityRecord activity) {
-        return (mDisplayRotationCompatPolicy != null
-                        && mDisplayRotationCompatPolicy.shouldCameraCompatControlAspectRatio(
-                                activity))
-                || (mCameraCompatFreeformPolicy != null
-                        && mCameraCompatFreeformPolicy.shouldCameraCompatControlAspectRatio(
-                                activity));
+    static boolean shouldCameraCompatControlAspectRatio(@NonNull ActivityRecord activity) {
+        final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+        if (cameraPolicy == null) {
+            return false;
+        }
+        return (cameraPolicy.mDisplayRotationCompatPolicy != null
+                        && cameraPolicy.mDisplayRotationCompatPolicy
+                                .shouldCameraCompatControlAspectRatio(activity))
+                || (cameraPolicy.mCameraCompatFreeformPolicy != null
+                        && cameraPolicy.mCameraCompatFreeformPolicy
+                                .shouldCameraCompatControlAspectRatio(activity));
     }
 
     // TODO(b/369070416): have policies implement the same interface.
@@ -200,29 +210,41 @@
      * @return {@code true} if the Camera is active for the provided {@link ActivityRecord} and
      * any camera compat treatment could be triggered for the current windowing mode.
      */
-    private boolean isCameraRunningAndWindowingModeEligible(@NonNull ActivityRecord activity) {
-        return (mDisplayRotationCompatPolicy != null
-                && mDisplayRotationCompatPolicy.isCameraRunningAndWindowingModeEligible(activity,
-                        /* mustBeFullscreen */ true))
-                || (mCameraCompatFreeformPolicy != null && mCameraCompatFreeformPolicy
-                        .isCameraRunningAndWindowingModeEligible(activity));
+    private static boolean isCameraRunningAndWindowingModeEligible(
+            @NonNull ActivityRecord activity) {
+        final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+        if (cameraPolicy == null) {
+            return false;
+        }
+        return (cameraPolicy.mDisplayRotationCompatPolicy != null
+                && cameraPolicy.mDisplayRotationCompatPolicy
+                        .isCameraRunningAndWindowingModeEligible(activity,
+                                /* mustBeFullscreen */ true))
+                || (cameraPolicy.mCameraCompatFreeformPolicy != null
+                        && cameraPolicy.mCameraCompatFreeformPolicy
+                                .isCameraRunningAndWindowingModeEligible(activity));
     }
 
     @Nullable
     String getSummaryForDisplayRotationHistoryRecord() {
-        if (mDisplayRotationCompatPolicy != null) {
-            return mDisplayRotationCompatPolicy.getSummaryForDisplayRotationHistoryRecord();
-        }
-        return null;
+        return mDisplayRotationCompatPolicy != null
+                ? mDisplayRotationCompatPolicy.getSummaryForDisplayRotationHistoryRecord()
+                : null;
     }
 
     // TODO(b/369070416): have policies implement the same interface.
-    float getCameraCompatAspectRatio(@NonNull ActivityRecord activity) {
-        float displayRotationCompatPolicyAspectRatio = mDisplayRotationCompatPolicy != null
-                ? mDisplayRotationCompatPolicy.getCameraCompatAspectRatio(activity)
+    static float getCameraCompatAspectRatio(@NonNull ActivityRecord activity) {
+        final AppCompatCameraPolicy cameraPolicy = getAppCompatCameraPolicy(activity);
+        if (cameraPolicy == null) {
+            return 1.0f;
+        }
+        float displayRotationCompatPolicyAspectRatio =
+                cameraPolicy.mDisplayRotationCompatPolicy != null
+                ? cameraPolicy.mDisplayRotationCompatPolicy.getCameraCompatAspectRatio(activity)
                 : MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
-        float cameraCompatFreeformPolicyAspectRatio = mCameraCompatFreeformPolicy != null
-                ? mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(activity)
+        float cameraCompatFreeformPolicyAspectRatio =
+                cameraPolicy.mCameraCompatFreeformPolicy != null
+                ? cameraPolicy.mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(activity)
                 : MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
         return Math.max(displayRotationCompatPolicyAspectRatio,
                 cameraCompatFreeformPolicyAspectRatio);
@@ -232,8 +254,8 @@
      * Whether we should apply the min aspect ratio per-app override only when an app is connected
      * to the camera.
      */
-    boolean shouldOverrideMinAspectRatioForCamera(@NonNull ActivityRecord activityRecord) {
-        return isCameraRunningAndWindowingModeEligible(activityRecord)
+    static boolean shouldOverrideMinAspectRatioForCamera(@NonNull ActivityRecord activityRecord) {
+        return AppCompatCameraPolicy.isCameraRunningAndWindowingModeEligible(activityRecord)
                 && activityRecord.mAppCompatController.getAppCompatCameraOverrides()
                         .isOverrideMinAspectRatioForCameraEnabled();
     }
diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
index 5bd4aeb..f5d58ea 100644
--- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java
@@ -58,10 +58,8 @@
                 && displayContent.getIgnoreOrientationRequest();
         final boolean shouldApplyUserFullscreenOverride = mAppCompatOverrides
                 .getAppCompatAspectRatioOverrides().shouldApplyUserFullscreenOverride();
-        final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy
-                .getAppCompatCameraPolicy(mActivityRecord);
-        final boolean shouldCameraCompatControlOrientation = cameraPolicy != null
-                && cameraPolicy.shouldCameraCompatControlOrientation(mActivityRecord);
+        final boolean shouldCameraCompatControlOrientation =
+                AppCompatCameraPolicy.shouldCameraCompatControlOrientation(mActivityRecord);
         if (shouldApplyUserFullscreenOverride && isIgnoreOrientationRequestEnabled
                 // Do not override orientation to fullscreen for camera activities.
                 // Fixed-orientation activities are rarely tested in other orientations, and it
@@ -98,7 +96,7 @@
         if (displayContent != null
                 && mAppCompatOverrides.getAppCompatCameraOverrides()
                     .isOverrideOrientationOnlyForCameraEnabled()
-                && !displayContent.mAppCompatCameraPolicy
+                && !AppCompatCameraPolicy
                     .isActivityEligibleForOrientationOverride(mActivityRecord)) {
             return candidate;
         }
@@ -213,5 +211,4 @@
         }
         return false;
     }
-
 }
diff --git a/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java
index 3b2f723..c8cb621 100644
--- a/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java
+++ b/services/core/java/com/android/server/wm/DesktopAppCompatAspectRatioPolicy.java
@@ -194,12 +194,8 @@
             return aspectRatioOverrides.getUserMinAspectRatio();
         }
 
-        final AppCompatCameraPolicy cameraPolicy = AppCompatCameraPolicy.getAppCompatCameraPolicy(
-                mActivityRecord);
-        final boolean shouldOverrideMinAspectRatioForCamera = cameraPolicy != null
-                && cameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord);
         if (!aspectRatioOverrides.shouldOverrideMinAspectRatio()
-                && !shouldOverrideMinAspectRatioForCamera) {
+                && !AppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera(mActivityRecord)) {
             if (mActivityRecord.isUniversalResizeable()) {
                 return 0;
             }
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index b414a862..61b13a8 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -47,6 +47,7 @@
 import android.view.WindowInsetsAnimation;
 import android.view.WindowInsetsAnimation.Bounds;
 import android.view.WindowManager;
+import android.view.inputmethod.Flags;
 import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputMethodManager;
 
@@ -412,6 +413,22 @@
                 state.addSource(imeSource);
                 return state;
             }
+        } else if (Flags.refactorInsetsController()
+                && (w.mMergedExcludeInsetsTypes & WindowInsets.Type.ime()) != 0) {
+            // In some cases (e.g. split screen from when the IME was requested and the animation
+            // actually starts) the insets should not be send, unless the flag is unset.
+            final InsetsSource originalImeSource = originalState.peekSource(ID_IME);
+            if (originalImeSource != null && originalImeSource.isVisible()) {
+                final InsetsState state = copyState
+                        ? new InsetsState(originalState)
+                        : originalState;
+                final InsetsSource imeSource = new InsetsSource(originalImeSource);
+                // Setting the height to zero, pretending we're in floating mode
+                imeSource.setFrame(0, 0, 0, 0);
+                imeSource.setVisibleFrame(imeSource.getFrame());
+                state.addSource(imeSource);
+                return state;
+            }
         }
         return originalState;
     }
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index ede587c..5dddf36 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -458,6 +458,12 @@
         mDisplayContent.notifyInsetsChanged(mDispatchInsetsChanged);
     }
 
+    void notifyInsetsChanged(ArraySet<WindowState> changedWindows) {
+        for (int i = changedWindows.size() - 1; i >= 0; i--) {
+            mDispatchInsetsChanged.accept(changedWindows.valueAt(i));
+        }
+    }
+
     /**
      * Checks if the control target has pending controls.
      *
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index da64a5f..af57c84 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -32,6 +32,7 @@
 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.os.UserHandle.USER_NULL;
 import static android.view.SurfaceControl.Transaction;
+import static android.view.WindowInsets.Type.InsetsType;
 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.window.TaskFragmentAnimationParams.DEFAULT_ANIMATION_BACKGROUND_COLOR;
@@ -173,6 +174,13 @@
      */
     protected SparseArray<InsetsSourceProvider> mInsetsSourceProviders = null;
 
+    /**
+     * The combined excluded insets types (combined mExcludeInsetsTypes and the
+     * mMergedExcludeInsetsTypes from its parent)
+     */
+    protected @InsetsType int mMergedExcludeInsetsTypes = 0;
+    private @InsetsType int mExcludeInsetsTypes = 0;
+
     @Nullable
     private ArrayMap<IBinder, DeathRecipient> mInsetsOwnerDeathRecipientMap;
 
@@ -555,6 +563,49 @@
         return mControllableInsetProvider;
     }
 
+    /**
+     * Sets the excludeInsetsTypes of this window and updates the mMergedExcludeInsetsTypes of
+     * all child nodes in the hierarchy.
+     *
+     * @param excludeInsetsTypes the excluded {@link InsetsType} that should be set on this
+     *                           WindowContainer
+     */
+    void setExcludeInsetsTypes(@InsetsType int excludeInsetsTypes) {
+        if (excludeInsetsTypes == mExcludeInsetsTypes) {
+            return;
+        }
+        mExcludeInsetsTypes = excludeInsetsTypes;
+        mergeExcludeInsetsTypesAndNotifyInsetsChanged(
+                mParent != null ? mParent.mMergedExcludeInsetsTypes : 0);
+    }
+
+    private void mergeExcludeInsetsTypesAndNotifyInsetsChanged(
+            @InsetsType int excludeInsetsTypesFromParent) {
+        final ArraySet<WindowState> changedWindows = new ArraySet<>();
+        updateMergedExcludeInsetsTypes(excludeInsetsTypesFromParent, changedWindows);
+        if (getDisplayContent() != null) {
+            getDisplayContent().getInsetsStateController().notifyInsetsChanged(changedWindows);
+        }
+    }
+
+    private void updateMergedExcludeInsetsTypes(
+            @InsetsType int excludeInsetsTypesFromParent, ArraySet<WindowState> changedWindows) {
+        final int newMergedExcludeInsetsTypes = mExcludeInsetsTypes | excludeInsetsTypesFromParent;
+        if (newMergedExcludeInsetsTypes == mMergedExcludeInsetsTypes) {
+            return;
+        }
+        mMergedExcludeInsetsTypes = newMergedExcludeInsetsTypes;
+
+        final WindowState win = asWindowState();
+        if (win != null) {
+            changedWindows.add(win);
+        }
+        // Apply to all children
+        for (int i = mChildren.size() - 1; i >= 0; i--) {
+            final WindowContainer<?> child = mChildren.get(i);
+            child.updateMergedExcludeInsetsTypes(mMergedExcludeInsetsTypes, changedWindows);
+        }
+    }
 
     @Override
     final protected WindowContainer getParent() {
@@ -653,6 +704,7 @@
     void onParentChanged(ConfigurationContainer newParent, ConfigurationContainer oldParent) {
         super.onParentChanged(newParent, oldParent);
         if (mParent == null) {
+            mergeExcludeInsetsTypesAndNotifyInsetsChanged(0);
             return;
         }
 
@@ -667,6 +719,7 @@
             // new parent.
             reparentSurfaceControl(getSyncTransaction(), mParent.mSurfaceControl);
         }
+        mergeExcludeInsetsTypesAndNotifyInsetsChanged(mParent.mMergedExcludeInsetsTypes);
 
         // Either way we need to ask the parent to assign us a Z-order.
         mParent.assignChildLayers();
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index e1e4714..ebf645d 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -6810,30 +6810,36 @@
      * @param logLevel  Determines the amount of data to be written to the Protobuf.
      */
     void dumpDebugLocked(ProtoOutputStream proto, @WindowTracingLogLevel int logLevel) {
-        mPolicy.dumpDebug(proto, POLICY);
-        mRoot.dumpDebug(proto, ROOT_WINDOW_CONTAINER, logLevel);
-        final DisplayContent topFocusedDisplayContent = mRoot.getTopFocusedDisplayContent();
-        if (topFocusedDisplayContent.mCurrentFocus != null) {
-            topFocusedDisplayContent.mCurrentFocus.writeIdentifierToProto(proto, FOCUSED_WINDOW);
-        }
-        if (topFocusedDisplayContent.mFocusedApp != null) {
-            topFocusedDisplayContent.mFocusedApp.writeNameToProto(proto, FOCUSED_APP);
-        }
-        final WindowState imeWindow = mRoot.getCurrentInputMethodWindow();
-        if (imeWindow != null) {
-            imeWindow.writeIdentifierToProto(proto, INPUT_METHOD_WINDOW);
-        }
-        proto.write(DISPLAY_FROZEN, mDisplayFrozen);
-        proto.write(FOCUSED_DISPLAY_ID, topFocusedDisplayContent.getDisplayId());
-        proto.write(HARD_KEYBOARD_AVAILABLE, mHardKeyboardAvailable);
+        Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "dumpDebugLocked");
+        try {
+            mPolicy.dumpDebug(proto, POLICY);
+            mRoot.dumpDebug(proto, ROOT_WINDOW_CONTAINER, logLevel);
+            final DisplayContent topFocusedDisplayContent = mRoot.getTopFocusedDisplayContent();
+            if (topFocusedDisplayContent.mCurrentFocus != null) {
+                topFocusedDisplayContent.mCurrentFocus
+                        .writeIdentifierToProto(proto, FOCUSED_WINDOW);
+            }
+            if (topFocusedDisplayContent.mFocusedApp != null) {
+                topFocusedDisplayContent.mFocusedApp.writeNameToProto(proto, FOCUSED_APP);
+            }
+            final WindowState imeWindow = mRoot.getCurrentInputMethodWindow();
+            if (imeWindow != null) {
+                imeWindow.writeIdentifierToProto(proto, INPUT_METHOD_WINDOW);
+            }
+            proto.write(DISPLAY_FROZEN, mDisplayFrozen);
+            proto.write(FOCUSED_DISPLAY_ID, topFocusedDisplayContent.getDisplayId());
+            proto.write(HARD_KEYBOARD_AVAILABLE, mHardKeyboardAvailable);
 
-        // This is always true for now since we still update the window frames at the server side.
-        // Once we move the window layout to the client side, this can be false when we are waiting
-        // for the frames.
-        proto.write(WINDOW_FRAMES_VALID, true);
+            // This is always true for now since we still update the window frames at the server
+            // side. Once we move the window layout to the client side, this can be false when we
+            // are waiting for the frames.
+            proto.write(WINDOW_FRAMES_VALID, true);
 
-        // Write the BackNavigationController's state into the protocol buffer
-        mAtmService.mBackNavigationController.dumpDebug(proto, BACK_NAVIGATION);
+            // Write the BackNavigationController's state into the protocol buffer
+            mAtmService.mBackNavigationController.dumpDebug(proto, BACK_NAVIGATION);
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
+        }
     }
 
     private void dumpWindowsLocked(PrintWriter pw, boolean dumpAll,
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 09a2bf9..f8d0bc2 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -67,6 +67,7 @@
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_EXCLUDE_INSETS_TYPES;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_IS_TRIMMABLE;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT;
@@ -1449,6 +1450,16 @@
                 }
                 break;
             }
+            case HIERARCHY_OP_TYPE_SET_EXCLUDE_INSETS_TYPES: {
+                final WindowContainer container = WindowContainer.fromBinder(hop.getContainer());
+                if (container == null) {
+                    Slog.e(TAG, "Attempt to operate on unknown or detached container: "
+                            + container);
+                    break;
+                }
+                container.setExcludeInsetsTypes(hop.getExcludeInsetsTypes());
+                break;
+            }
         }
         return effects;
     }
diff --git a/services/core/java/com/android/server/wm/WindowTracing.java b/services/core/java/com/android/server/wm/WindowTracing.java
index fe26726..6883437 100644
--- a/services/core/java/com/android/server/wm/WindowTracing.java
+++ b/services/core/java/com/android/server/wm/WindowTracing.java
@@ -167,12 +167,7 @@
 
             long token = os.start(WINDOW_MANAGER_SERVICE);
             synchronized (mGlobalLock) {
-                Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "dumpDebugLocked");
-                try {
-                    mService.dumpDebugLocked(os, logLevel);
-                } finally {
-                    Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
-                }
+                mService.dumpDebugLocked(os, logLevel);
             }
             os.end(token);
         } catch (Exception e) {
diff --git a/services/tests/appfunctions/Android.bp b/services/tests/appfunctions/Android.bp
index 9560ec9..c841643 100644
--- a/services/tests/appfunctions/Android.bp
+++ b/services/tests/appfunctions/Android.bp
@@ -36,6 +36,7 @@
         "androidx.test.core",
         "androidx.test.runner",
         "androidx.test.ext.truth",
+        "kotlin-test",
         "platform-test-annotations",
         "services.appfunctions",
         "servicestests-core-utils",
diff --git a/services/tests/appfunctions/src/android/app/appfunctions/AppFunctionRuntimeMetadataTest.kt b/services/tests/appfunctions/src/android/app/appfunctions/AppFunctionRuntimeMetadataTest.kt
index da3e94f..d326204 100644
--- a/services/tests/appfunctions/src/android/app/appfunctions/AppFunctionRuntimeMetadataTest.kt
+++ b/services/tests/appfunctions/src/android/app/appfunctions/AppFunctionRuntimeMetadataTest.kt
@@ -15,8 +15,12 @@
  */
 package android.app.appfunctions
 
+import android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DEFAULT
+import android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_DISABLED
+import android.app.appfunctions.AppFunctionManager.APP_FUNCTION_STATE_ENABLED
 import android.app.appsearch.AppSearchSchema
 import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -108,32 +112,43 @@
 
         assertThat(runtimeMetadata.packageName).isEqualTo("com.pkg")
         assertThat(runtimeMetadata.functionId).isEqualTo("funcId")
-        assertThat(runtimeMetadata.enabled).isNull()
+        assertThat(runtimeMetadata.enabled).isEqualTo(APP_FUNCTION_STATE_DEFAULT)
         assertThat(runtimeMetadata.appFunctionStaticMetadataQualifiedId)
             .isEqualTo("android\$apps-db/app_functions#com.pkg/funcId")
     }
 
     @Test
-    fun setEnabled_true() {
+    fun setEnabled_enabled() {
         val runtimeMetadata =
-            AppFunctionRuntimeMetadata.Builder("com.pkg", "funcId").setEnabled(true).build()
+            AppFunctionRuntimeMetadata.Builder("com.pkg", "funcId").setEnabled(APP_FUNCTION_STATE_ENABLED).build()
 
-        assertThat(runtimeMetadata.enabled).isTrue()
+        assertThat(runtimeMetadata.enabled).isEqualTo(APP_FUNCTION_STATE_ENABLED)
     }
 
     @Test
-    fun setEnabled_false() {
+    fun setEnabled_disabled() {
         val runtimeMetadata =
-            AppFunctionRuntimeMetadata.Builder("com.pkg", "funcId").setEnabled(false).build()
+            AppFunctionRuntimeMetadata.Builder("com.pkg", "funcId").setEnabled(
+                APP_FUNCTION_STATE_DISABLED).build()
 
-        assertThat(runtimeMetadata.enabled).isFalse()
+        assertThat(runtimeMetadata.enabled).isEqualTo(APP_FUNCTION_STATE_DISABLED)
     }
 
     @Test
-    fun setEnabled_null() {
+    fun setEnabled_default() {
         val runtimeMetadata =
-            AppFunctionRuntimeMetadata.Builder("com.pkg", "funcId").setEnabled(null).build()
+            AppFunctionRuntimeMetadata.Builder("com.pkg", "funcId").setEnabled(
+                APP_FUNCTION_STATE_DEFAULT).build()
 
-        assertThat(runtimeMetadata.enabled).isNull()
+        assertThat(runtimeMetadata.enabled).isEqualTo(APP_FUNCTION_STATE_DEFAULT)
+    }
+
+    @Test
+    fun setEnabled_illegalArgument() {
+        val runtimeMetadataBuilder =
+            AppFunctionRuntimeMetadata.Builder("com.pkg", "funcId")
+        assertFailsWith<IllegalArgumentException>("Value of EnabledState is unsupported.") {
+            runtimeMetadataBuilder.setEnabled(-1)
+        }
     }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java
index 18ca09b..bf0586c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java
@@ -18,11 +18,21 @@
 
 import static android.app.NotificationManager.IMPORTANCE_LOW;
 
+import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
+import static android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION;
+import static android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING;
+
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNull;
 import static junit.framework.Assert.assertTrue;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.PendingIntent;
@@ -30,12 +40,16 @@
 import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.Adjustment;
 import android.service.notification.SnoozeCriterion;
 import android.service.notification.StatusBarNotification;
 
 import com.android.server.UiServiceTestCase;
 
+import org.junit.Rule;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -43,6 +57,9 @@
 
 public class NotificationAdjustmentExtractorTest extends UiServiceTestCase {
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
+
     @Test
     public void testExtractsAdjustment() {
         NotificationAdjustmentExtractor extractor = new NotificationAdjustmentExtractor();
@@ -111,6 +128,44 @@
         assertEquals(snoozeCriteria, r.getSnoozeCriteria());
     }
 
+    @Test
+    @EnableFlags({FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NOTIFICATION_FORCE_GROUPING})
+    public void testClassificationAdjustments_triggerRegrouping() {
+        GroupHelper groupHelper = mock(GroupHelper.class);
+        NotificationAdjustmentExtractor extractor = new NotificationAdjustmentExtractor();
+        extractor.setGroupHelper(groupHelper);
+
+        NotificationRecord r = generateRecord();
+
+        Bundle classificationAdj = new Bundle();
+        classificationAdj.putParcelable(Adjustment.KEY_TYPE, mock(NotificationChannel.class));
+        Adjustment adjustment = new Adjustment("pkg", r.getKey(), classificationAdj, "", 0);
+        r.addAdjustment(adjustment);
+
+        RankingReconsideration regroupingTask = extractor.process(r);
+        assertThat(regroupingTask).isNotNull();
+        regroupingTask.applyChangesLocked(r);
+        verify(groupHelper, times(1)).onChannelUpdated(r);
+    }
+
+    @Test
+    @DisableFlags({FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NOTIFICATION_FORCE_GROUPING})
+    public void testClassificationAdjustments_notTriggerRegrouping_flagsDisabled() {
+        GroupHelper groupHelper = mock(GroupHelper.class);
+        NotificationAdjustmentExtractor extractor = new NotificationAdjustmentExtractor();
+        extractor.setGroupHelper(groupHelper);
+
+        NotificationRecord r = generateRecord();
+
+        Bundle classificationAdj = new Bundle();
+        classificationAdj.putParcelable(Adjustment.KEY_TYPE, mock(NotificationChannel.class));
+        Adjustment adjustment = new Adjustment("pkg", r.getKey(), classificationAdj, "", 0);
+        r.addAdjustment(adjustment);
+
+        RankingReconsideration regroupingTask = extractor.process(r);
+        assertThat(regroupingTask).isNull();
+    }
+
     private NotificationRecord generateRecord() {
         NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW);
         final Notification.Builder builder = new Notification.Builder(getContext())
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
index 45cd571..03cad24 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
@@ -2637,6 +2637,63 @@
     }
 
     @Test
+    public void testBeepVolume_politeNotif_AvalancheStrategy_exempt_msgCategory() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE);
+        TestableFlagResolver flagResolver = new TestableFlagResolver();
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+        initAttentionHelper(flagResolver);
+
+        // Trigger avalanche trigger intent
+        final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        intent.putExtra("state", false);
+        mAvalancheBroadcastReceiver.onReceive(getContext(), intent);
+
+        // Create a conversation group with GROUP_ALERT_SUMMARY behavior
+        // Where the summary is not MessagingStyle
+        final String groupKey = "grup_name";
+        final String shortcutId = "shortcut";
+        NotificationRecord summary = getBeepyNotificationRecord(groupKey, GROUP_ALERT_SUMMARY);
+        summary.getNotification().flags |= Notification.FLAG_GROUP_SUMMARY;
+        summary.getNotification().category = Notification.CATEGORY_MESSAGE;
+        ShortcutInfo.Builder sb = new ShortcutInfo.Builder(getContext());
+        summary.setShortcutInfo(sb.setId(shortcutId).build());
+
+        // Should beep at 100% volume
+        mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+        verifyBeepVolume(1.0f);
+        assertNotEquals(-1, summary.getLastAudiblyAlertedMs());
+        Mockito.reset(mRingtonePlayer);
+
+        // Post child notifications with GROUP_ALERT_SUMMARY
+        NotificationRecord child = getConversationNotificationRecord(mId, false /* insistent */,
+                false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true,
+                true, false, groupKey, Notification.GROUP_ALERT_SUMMARY, false, mUser, mPkg,
+                shortcutId);
+
+        // Should not beep
+        mAttentionHelper.buzzBeepBlinkLocked(child, DEFAULT_SIGNALS);
+        verifyNeverBeep();
+        assertFalse(child.isInterruptive());
+        assertEquals(-1, child.getLastAudiblyAlertedMs());
+        Mockito.reset(mRingtonePlayer);
+
+        // 2nd update for summary should beep at 50% volume
+        mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+        verifyBeepVolume(0.5f);
+        assertNotEquals(-1, summary.getLastAudiblyAlertedMs());
+        Mockito.reset(mRingtonePlayer);
+
+        // 3rd update for summary should not beep
+        mAttentionHelper.buzzBeepBlinkLocked(summary, DEFAULT_SIGNALS);
+        verifyNeverBeep();
+        assertFalse(summary.isInterruptive());
+        assertEquals(-1, summary.getLastAudiblyAlertedMs());
+    }
+
+    @Test
     public void testBeepVolume_politeNotif_exemptEmergency() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
         mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
index 9a6e818..5d4382a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
@@ -92,6 +92,7 @@
     @Mock ZenModeHelper mMockZenModeHelper;
     @Mock RankingConfig mConfig;
     @Mock Vibrator mVibrator;
+    @Mock GroupHelper mGroupHelper;
 
     private NotificationManager.Policy mTestNotificationPolicy;
     private Notification mNotiGroupGSortA;
@@ -157,7 +158,7 @@
         when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy);
         mHelper = new RankingHelper(getContext(), mHandler, mConfig, mMockZenModeHelper,
                 mUsageStats, new String[] {ImportanceExtractor.class.getName()},
-                mock(IPlatformCompat.class));
+                mock(IPlatformCompat.class), mGroupHelper);
 
         mNotiGroupGSortA = new Notification.Builder(mContext, TEST_CHANNEL_ID)
                 .setContentTitle("A")
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index 3dc893a..a85f866 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -700,7 +700,7 @@
     void assertPowerWakeUp() {
         mTestLooper.dispatchAll();
         verify(mWindowWakeUpPolicy)
-                .wakeUpFromKey(anyInt(), anyLong(), eq(KeyEvent.KEYCODE_POWER), anyBoolean());
+                .wakeUpFromKey(anyLong(), eq(KeyEvent.KEYCODE_POWER), anyBoolean());
     }
 
     void assertNoPowerSleep() {
diff --git a/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java b/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java
index 4119ad3..7322e5a 100644
--- a/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/WindowWakeUpPolicyTests.java
@@ -35,7 +35,6 @@
 import static com.android.internal.R.bool.config_allowTheaterModeWakeFromLidSwitch;
 import static com.android.internal.R.bool.config_allowTheaterModeWakeFromGesture;
 import static com.android.server.policy.Flags.FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE;
-import static com.android.server.power.feature.flags.Flags.FLAG_PER_DISPLAY_WAKE_BY_TOUCH;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
@@ -44,7 +43,6 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
@@ -54,8 +52,6 @@
 import android.content.ContextWrapper;
 import android.content.res.Resources;
 import android.os.PowerManager;
-import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.view.Display;
@@ -129,7 +125,6 @@
     }
 
     @Test
-    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testMotionWakeUpDelegation_wakePowerManagerIfDelegateDoesNotHandleWake() {
         setTheaterModeEnabled(false);
         mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE);
@@ -141,8 +136,7 @@
 
         // Verify the policy wake up call succeeds because of the call on the delegate, and not
         // because of a PowerManager wake up.
-        assertThat(mPolicy.wakeUpFromMotion(
-                mDefaultDisplay.getDisplayId(), 200, SOURCE_TOUCHSCREEN, true)).isTrue();
+        assertThat(mPolicy.wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true)).isTrue();
         verify(mInputWakeUpDelegate).wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true);
         verifyNoPowerManagerWakeUp();
 
@@ -150,14 +144,12 @@
 
         // Verify the policy wake up call succeeds because of the PowerManager wake up, since the
         // delegate would not handle the wake up request.
-        assertThat(mPolicy.wakeUpFromMotion(
-                mDefaultDisplay.getDisplayId(), 300, SOURCE_ROTARY_ENCODER, false)).isTrue();
+        assertThat(mPolicy.wakeUpFromMotion(300, SOURCE_ROTARY_ENCODER, false)).isTrue();
         verify(mInputWakeUpDelegate).wakeUpFromMotion(300, SOURCE_ROTARY_ENCODER, false);
         verify(mPowerManager).wakeUp(300, WAKE_REASON_WAKE_MOTION, "android.policy:MOTION");
     }
 
     @Test
-    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testKeyWakeUpDelegation_wakePowerManagerIfDelegateDoesNotHandleWake() {
         setTheaterModeEnabled(false);
         mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE);
@@ -169,8 +161,7 @@
 
         // Verify the policy wake up call succeeds because of the call on the delegate, and not
         // because of a PowerManager wake up.
-        assertThat(mPolicy.wakeUpFromKey(
-                mDefaultDisplay.getDisplayId(), 200, KEYCODE_POWER, true)).isTrue();
+        assertThat(mPolicy.wakeUpFromKey(200, KEYCODE_POWER, true)).isTrue();
         verify(mInputWakeUpDelegate).wakeUpFromKey(200, KEYCODE_POWER, true);
         verifyNoPowerManagerWakeUp();
 
@@ -178,8 +169,7 @@
 
         // Verify the policy wake up call succeeds because of the PowerManager wake up, since the
         // delegate would not handle the wake up request.
-        assertThat(mPolicy.wakeUpFromKey(
-                mDefaultDisplay.getDisplayId(), 300, KEYCODE_STEM_PRIMARY, false)).isTrue();
+        assertThat(mPolicy.wakeUpFromKey(300, KEYCODE_STEM_PRIMARY, false)).isTrue();
         verify(mInputWakeUpDelegate).wakeUpFromKey(300, KEYCODE_STEM_PRIMARY, false);
         verify(mPowerManager).wakeUp(300, WAKE_REASON_WAKE_KEY, "android.policy:KEY");
     }
@@ -196,8 +186,7 @@
                 .setInputWakeUpDelegate(mInputWakeUpDelegate);
 
         // Check that the wake up does not happen because the theater mode policy check fails.
-        assertThat(mPolicy.wakeUpFromKey(
-                mDefaultDisplay.getDisplayId(), 200, KEYCODE_POWER, true)).isFalse();
+        assertThat(mPolicy.wakeUpFromKey(200, KEYCODE_POWER, true)).isFalse();
         verify(mInputWakeUpDelegate, never()).wakeUpFromKey(anyLong(), anyInt(), anyBoolean());
     }
 
@@ -212,13 +201,11 @@
                 .setInputWakeUpDelegate(mInputWakeUpDelegate);
 
         // Check that the wake up does not happen because the theater mode policy check fails.
-        assertThat(mPolicy.wakeUpFromMotion(
-                mDefaultDisplay.getDisplayId(), 200, SOURCE_TOUCHSCREEN, true)).isFalse();
+        assertThat(mPolicy.wakeUpFromMotion(200, SOURCE_TOUCHSCREEN, true)).isFalse();
         verify(mInputWakeUpDelegate, never()).wakeUpFromMotion(anyLong(), anyInt(), anyBoolean());
     }
 
     @Test
-    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testTheaterModeChecksNotAppliedWhenScreenIsOn() {
         mSetFlagsRule.enableFlags(FLAG_SUPPORT_INPUT_WAKEUP_DELEGATE);
         setDefaultDisplayState(Display.STATE_ON);
@@ -226,69 +213,30 @@
         setBooleanRes(config_allowTheaterModeWakeFromMotion, false);
         mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
 
-        mPolicy.wakeUpFromMotion(mDefaultDisplay.getDisplayId(), 200L, SOURCE_TOUCHSCREEN, true);
+        mPolicy.wakeUpFromMotion(200L, SOURCE_TOUCHSCREEN, true);
 
         verify(mPowerManager).wakeUp(200L, WAKE_REASON_WAKE_MOTION, "android.policy:MOTION");
     }
 
     @Test
-    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testWakeUpFromMotion() {
         runPowerManagerUpChecks(
-                () -> mPolicy.wakeUpFromMotion(mDefaultDisplay.getDisplayId(),
-                        mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, true),
+                () -> mPolicy.wakeUpFromMotion(mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, true),
                 config_allowTheaterModeWakeFromMotion,
                 WAKE_REASON_WAKE_MOTION,
                 "android.policy:MOTION");
     }
 
     @Test
-    @EnableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
-    public void testWakeUpFromMotion_perDisplayWakeByTouchEnabled() {
-        setTheaterModeEnabled(false);
-        final int displayId = 555;
-        mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
-
-        boolean displayWokeUp = mPolicy.wakeUpFromMotion(
-                displayId, mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, /* isDown= */ true);
-
-        // Verify that display is woken up
-        assertThat(displayWokeUp).isTrue();
-        verify(mPowerManager).wakeUp(anyLong(), eq(WAKE_REASON_WAKE_MOTION),
-                eq("android.policy:MOTION"), eq(displayId));
-    }
-
-    @Test
-    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
-    public void testWakeUpFromMotion_perDisplayWakeByTouchDisabled() {
-        setTheaterModeEnabled(false);
-        final int displayId = 555;
-        mPolicy = new WindowWakeUpPolicy(mContextSpy, mClock);
-
-        boolean displayWokeUp = mPolicy.wakeUpFromMotion(
-                displayId, mClock.uptimeMillis(), SOURCE_TOUCHSCREEN, /* isDown= */ true);
-
-        // Verify that power is woken up and display isn't woken up individually
-        assertThat(displayWokeUp).isTrue();
-        verify(mPowerManager).wakeUp(
-                anyLong(), eq(WAKE_REASON_WAKE_MOTION), eq("android.policy:MOTION"));
-        verify(mPowerManager, never()).wakeUp(anyLong(), eq(WAKE_REASON_WAKE_MOTION),
-                eq("android.policy:MOTION"), eq(displayId));
-    }
-
-    @Test
-    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testWakeUpFromKey_nonPowerKey() {
         runPowerManagerUpChecks(
-                () -> mPolicy.wakeUpFromKey(
-                        mDefaultDisplay.getDisplayId(), mClock.uptimeMillis(), KEYCODE_HOME, true),
+                () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_HOME, true),
                 config_allowTheaterModeWakeFromKey,
                 WAKE_REASON_WAKE_KEY,
                 "android.policy:KEY");
     }
 
     @Test
-    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testWakeUpFromKey_powerKey() {
         // Disable the resource affecting all wake keys because it affects power key as well.
         // That way, power key wake during theater mode will solely be controlled by
@@ -297,8 +245,7 @@
 
         // Test with power key
         runPowerManagerUpChecks(
-                () -> mPolicy.wakeUpFromKey(
-                        mDefaultDisplay.getDisplayId(), mClock.uptimeMillis(), KEYCODE_POWER, true),
+                () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_POWER, true),
                 config_allowTheaterModeWakeFromPowerKey,
                 WAKE_REASON_POWER_BUTTON,
                 "android.policy:POWER");
@@ -307,15 +254,13 @@
         // even if the power-key specific theater mode config is disabled.
         setBooleanRes(config_allowTheaterModeWakeFromPowerKey, false);
         runPowerManagerUpChecks(
-                () -> mPolicy.wakeUpFromKey(mDefaultDisplay.getDisplayId(), mClock.uptimeMillis(),
-                        KEYCODE_POWER, false),
+                () -> mPolicy.wakeUpFromKey(mClock.uptimeMillis(), KEYCODE_POWER, false),
                 config_allowTheaterModeWakeFromKey,
                 WAKE_REASON_POWER_BUTTON,
                 "android.policy:POWER");
     }
 
     @Test
-    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testWakeUpFromLid() {
         runPowerManagerUpChecks(
                 () -> mPolicy.wakeUpFromLid(),
@@ -325,7 +270,6 @@
     }
 
     @Test
-    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testWakeUpFromWakeGesture() {
         runPowerManagerUpChecks(
                 () -> mPolicy.wakeUpFromWakeGesture(),
@@ -335,7 +279,6 @@
     }
 
     @Test
-    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testwakeUpFromCameraCover() {
         runPowerManagerUpChecks(
                 () -> mPolicy.wakeUpFromCameraCover(mClock.uptimeMillis()),
@@ -345,7 +288,6 @@
     }
 
     @Test
-    @DisableFlags({FLAG_PER_DISPLAY_WAKE_BY_TOUCH})
     public void testWakeUpFromPowerKeyCameraGesture() {
         // Disable the resource affecting all wake keys because it affects power key as well.
         // That way, power key wake during theater mode will solely be controlled by
@@ -353,7 +295,7 @@
         setBooleanRes(config_allowTheaterModeWakeFromKey, false);
 
         runPowerManagerUpChecks(
-                () -> mPolicy.wakeUpFromPowerKeyCameraGesture(mDefaultDisplay.getDisplayId()),
+                () -> mPolicy.wakeUpFromPowerKeyCameraGesture(),
                 config_allowTheaterModeWakeFromPowerKey,
                 WAKE_REASON_CAMERA_LAUNCH,
                 "android.policy:CAMERA_GESTURE_PREVENT_LOCK");
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 65736cb..c8a3559 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -179,17 +179,25 @@
                 .getAppCompatAspectRatioPolicy()).isLetterboxedForFixedOrientationAndAspectRatio();
     }
 
-    void enableTreatmentForTopActivity(boolean enabled) {
-        doReturn(enabled).when(mDisplayContent.mAppCompatCameraPolicy)
-                .isTreatmentEnabledForActivity(eq(mActivityStack.top()));
+    void enableFullscreenCameraCompatTreatmentForTopActivity(boolean enabled) {
+        if (mDisplayContent.mAppCompatCameraPolicy.hasDisplayRotationCompatPolicy()) {
+            doReturn(enabled).when(
+                    mDisplayContent.mAppCompatCameraPolicy.mDisplayRotationCompatPolicy)
+                        .isTreatmentEnabledForActivity(eq(mActivityStack.top()));
+        }
     }
 
-    void setTopActivityCameraActive(boolean enabled) {
+    void setIsCameraRunningAndWindowingModeEligibleFullscreen(boolean enabled) {
         doReturn(enabled).when(getTopDisplayRotationCompatPolicy())
                 .isCameraRunningAndWindowingModeEligible(eq(mActivityStack.top()),
                         /* mustBeFullscreen= */ eq(true));
     }
 
+    void setIsCameraRunningAndWindowingModeEligibleFreeform(boolean enabled) {
+        doReturn(enabled).when(getTopCameraCompatFreeformPolicy())
+                .isCameraRunningAndWindowingModeEligible(eq(mActivityStack.top()));
+    }
+
     void setTopActivityEligibleForOrientationOverride(boolean enabled) {
         doReturn(enabled).when(getTopDisplayRotationCompatPolicy())
                 .isActivityEligibleForOrientationOverride(eq(mActivityStack.top()));
@@ -508,8 +516,13 @@
     }
 
     private DisplayRotationCompatPolicy getTopDisplayRotationCompatPolicy() {
-        return mActivityStack.top().mDisplayContent
-                .mAppCompatCameraPolicy.mDisplayRotationCompatPolicy;
+        return mActivityStack.top().mDisplayContent.mAppCompatCameraPolicy
+                .mDisplayRotationCompatPolicy;
+    }
+
+    private CameraCompatFreeformPolicy getTopCameraCompatFreeformPolicy() {
+        return mActivityStack.top().mDisplayContent.mAppCompatCameraPolicy
+                .mCameraCompatFreeformPolicy;
     }
 
     // We add the activity to the stack and spyOn() on its properties.
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
index 1e40aa0..b839113 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java
@@ -282,7 +282,8 @@
             robot.activity().createActivityWithComponentInNewTaskAndDisplay();
             robot.checkFixedOrientationLetterboxAspectRatioForTopParent(/* expected */ 1.5f);
 
-            robot.activity().enableTreatmentForTopActivity(/* enabled */ true);
+            robot.activity().enableFullscreenCameraCompatTreatmentForTopActivity(
+                    /* enabled */ true);
             robot.checkAspectRatioForTopParentIsSplitScreenRatio(/* expected */ true);
         });
     }
@@ -308,6 +309,12 @@
         void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) {
             super.onPostDisplayContentCreation(displayContent);
             spyOn(displayContent.mAppCompatCameraPolicy);
+            if (displayContent.mAppCompatCameraPolicy.hasDisplayRotationCompatPolicy()) {
+                spyOn(displayContent.mAppCompatCameraPolicy.mDisplayRotationCompatPolicy);
+            }
+            if (displayContent.mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy()) {
+                spyOn(displayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy);
+            }
         }
 
         @Override
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
index 41102d6..9b9040b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
@@ -20,6 +20,8 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.wm.AppCompatCameraPolicy.isTreatmentEnabledForActivity;
+import static com.android.server.wm.AppCompatCameraPolicy.shouldOverrideMinAspectRatioForCamera;
 import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
 
 import static org.junit.Assert.assertEquals;
@@ -194,9 +196,10 @@
     @Test
     public void testIsCameraCompatTreatmentActive_whenTreatmentForTopActivityIsEnabled() {
         runTestScenario((robot) -> {
+            robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ true);
             robot.applyOnActivity((a)-> {
-                a.createActivityWithComponent();
-                a.enableTreatmentForTopActivity(/* enabled */ true);
+                a.createActivityWithComponentInNewTaskAndDisplay();
+                a.enableFullscreenCameraCompatTreatmentForTopActivity(/* enabled */ true);
             });
 
             robot.checkIsCameraCompatTreatmentActiveForTopActivity(/* active */ true);
@@ -206,9 +209,10 @@
     @Test
     public void testIsCameraCompatTreatmentNotActive_whenTreatmentForTopActivityIsDisabled() {
         runTestScenario((robot) -> {
+            robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ true);
             robot.applyOnActivity((a)-> {
                 a.createActivityWithComponent();
-                a.enableTreatmentForTopActivity(/* enabled */ false);
+                a.enableFullscreenCameraCompatTreatmentForTopActivity(/* enabled */ false);
             });
 
             robot.checkIsCameraCompatTreatmentActiveForTopActivity(/* active */ false);
@@ -220,9 +224,10 @@
     public void testShouldOverrideMinAspectRatioForCamera_whenCameraIsNotRunning() {
         runTestScenario((robot) -> {
             robot.applyOnActivity((a)-> {
+                robot.allowEnterDesktopMode(true);
                 robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ true);
                 a.createActivityWithComponentInNewTaskAndDisplay();
-                a.setTopActivityCameraActive(/* active */ false);
+                a.setIsCameraRunningAndWindowingModeEligibleFullscreen(/* enabled */ false);
             });
 
             robot.checkShouldOverrideMinAspectRatioForCamera(/* active */ false);
@@ -234,9 +239,10 @@
     public void testShouldOverrideMinAspectRatioForCamera_whenCameraIsRunning_overrideDisabled() {
         runTestScenario((robot) -> {
             robot.applyOnActivity((a)-> {
+                robot.allowEnterDesktopMode(true);
                 robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ true);
                 a.createActivityWithComponentInNewTaskAndDisplay();
-                a.setTopActivityCameraActive(/* active */ true);
+                a.setIsCameraRunningAndWindowingModeEligibleFullscreen(/* active */ true);
             });
 
             robot.checkShouldOverrideMinAspectRatioForCamera(/* active */ false);
@@ -245,12 +251,28 @@
 
     @Test
     @EnableCompatChanges(OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA)
-    public void testShouldOverrideMinAspectRatioForCamera_whenCameraIsRunning_overrideEnabled() {
+    public void testShouldOverrideMinAspectRatioForCameraFullscr_cameraIsRunning_overrideEnabled() {
         runTestScenario((robot) -> {
             robot.applyOnActivity((a)-> {
                 robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ true);
                 a.createActivityWithComponentInNewTaskAndDisplay();
-                a.setTopActivityCameraActive(/* active */ true);
+                a.setIsCameraRunningAndWindowingModeEligibleFullscreen(/* active */ true);
+            });
+
+            robot.checkShouldOverrideMinAspectRatioForCamera(/* active */ true);
+        });
+    }
+
+
+    @Test
+    @EnableCompatChanges(OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA)
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    public void testShouldOverrideMinAspectRatioForCameraFreeform_cameraRunning_overrideEnabled() {
+        runTestScenario((robot) -> {
+            robot.applyOnActivity((a)-> {
+                robot.allowEnterDesktopMode(true);
+                a.createActivityWithComponentInNewTaskAndDisplay();
+                a.setIsCameraRunningAndWindowingModeEligibleFreeform(/* active */ true);
             });
 
             robot.checkShouldOverrideMinAspectRatioForCamera(/* active */ true);
@@ -318,13 +340,11 @@
         }
 
         void checkIsCameraCompatTreatmentActiveForTopActivity(boolean active) {
-            assertEquals(getTopAppCompatCameraPolicy()
-                    .isTreatmentEnabledForActivity(activity().top()), active);
+            assertEquals(active, isTreatmentEnabledForActivity(activity().top()));
         }
 
         void checkShouldOverrideMinAspectRatioForCamera(boolean expected) {
-            assertEquals(getTopAppCompatCameraPolicy()
-                    .shouldOverrideMinAspectRatioForCamera(activity().top()), expected);
+            assertEquals(expected, shouldOverrideMinAspectRatioForCamera(activity().top()));
         }
 
         // TODO(b/350460645): Create Desktop Windowing Robot to reuse common functionalities.
@@ -332,9 +352,5 @@
             doReturn(isAllowed).when(() ->
                     DesktopModeHelper.canEnterDesktopMode(any()));
         }
-
-        private AppCompatCameraPolicy getTopAppCompatCameraPolicy() {
-            return activity().top().mDisplayContent.mAppCompatCameraPolicy;
-        }
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
index 9057b6c..76101d5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
@@ -37,14 +37,18 @@
 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
 import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
 
 import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.verify;
 
 import android.compat.testing.PlatformCompatChangeRule;
 import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.annotation.NonNull;
@@ -321,7 +325,22 @@
             });
             robot.applyOnActivity((a) -> {
                 a.createActivityWithComponentInNewTaskAndDisplay();
-                a.setTopActivityCameraActive(false);
+                a.setIsCameraRunningAndWindowingModeEligibleFullscreen(false);
+            });
+
+            robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+                    /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+        });
+    }
+
+    @Test
+    @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
+    public void testOverrideOrientationIfNeeded_fullscrOverrideFreeform_cameraActivity_unchanged() {
+        runTestScenario((robot) -> {
+            robot.applyOnActivity((a) -> {
+                robot.allowEnterDesktopMode(true);
+                a.createActivityWithComponentInNewTaskAndDisplay();
+                a.setIsCameraRunningAndWindowingModeEligibleFreeform(false);
             });
 
             robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
@@ -426,8 +445,8 @@
                 c.enablePolicyForIgnoringRequestedOrientation(true);
             });
             robot.applyOnActivity((a) -> {
-                a.createActivityWithComponentInNewTask();
-                a.enableTreatmentForTopActivity(true);
+                a.createActivityWithComponentInNewTaskAndDisplay();
+                a.enableFullscreenCameraCompatTreatmentForTopActivity(true);
             });
             robot.prepareRelaunchingAfterRequestedOrientationChanged(false);
 
@@ -591,5 +610,11 @@
         private AppCompatOrientationPolicy getTopAppCompatOrientationPolicy() {
             return activity().top().mAppCompatController.getOrientationPolicy();
         }
+
+        // TODO(b/350460645): Create Desktop Windowing Robot to reuse common functionalities.
+        void allowEnterDesktopMode(boolean isAllowed) {
+            doReturn(isAllowed).when(() ->
+                    DesktopModeHelper.canEnterDesktopMode(any()));
+        }
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index 401964c..1fa6578 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -23,6 +23,10 @@
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
+import static android.view.WindowInsets.Type.captionBar;
+import static android.view.WindowInsets.Type.ime;
+import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.statusBars;
 import static android.view.WindowInsets.Type.systemOverlays;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
@@ -36,6 +40,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyFloat;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
@@ -80,6 +85,8 @@
 import android.os.ShellCallback;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.util.ArraySet;
 import android.view.IRemoteAnimationFinishedCallback;
 import android.view.IRemoteAnimationRunner;
 import android.view.InsetsFrameProvider;
@@ -103,6 +110,7 @@
 import java.io.FileDescriptor;
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.List;
 import java.util.NoSuchElementException;
 
 
@@ -967,7 +975,7 @@
         Rect insetsRect = new Rect(0, 200, 1080, 700);
         final int flags = FLAG_FORCE_CONSUMING;
         final InsetsFrameProvider provider =
-                new InsetsFrameProvider(owner, 1, WindowInsets.Type.captionBar())
+                new InsetsFrameProvider(owner, 1, captionBar())
                         .setArbitraryRectangle(insetsRect)
                         .setFlags(flags);
         task.addLocalInsetsFrameProvider(provider, owner);
@@ -1678,6 +1686,178 @@
         assertFalse("The source must be removed.", hasLocalSource(task, provider.getId()));
     }
 
+    @Test
+    public void testSetExcludeInsetsTypes_appliedOnNodeAndChildren() {
+        final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+        final TestWindowContainer root = builder.setLayer(0).build();
+
+        final TestWindowContainer child1 = root.addChildWindow();
+        final TestWindowContainer child2 = root.addChildWindow();
+        final TestWindowContainer child11 = child1.addChildWindow();
+        final TestWindowContainer child12 = child1.addChildWindow();
+        final TestWindowContainer child21 = child2.addChildWindow();
+
+        assertEquals(0, root.mMergedExcludeInsetsTypes);
+        assertEquals(0, child1.mMergedExcludeInsetsTypes);
+        assertEquals(0, child11.mMergedExcludeInsetsTypes);
+        assertEquals(0, child12.mMergedExcludeInsetsTypes);
+        assertEquals(0, child2.mMergedExcludeInsetsTypes);
+        assertEquals(0, child21.mMergedExcludeInsetsTypes);
+
+        child1.setExcludeInsetsTypes(ime());
+        assertEquals(0, root.mMergedExcludeInsetsTypes);
+        assertEquals(ime(), child1.mMergedExcludeInsetsTypes);
+        assertEquals(ime(), child11.mMergedExcludeInsetsTypes);
+        assertEquals(ime(), child12.mMergedExcludeInsetsTypes);
+        assertEquals(0, child2.mMergedExcludeInsetsTypes);
+        assertEquals(0, child21.mMergedExcludeInsetsTypes);
+
+        child11.setExcludeInsetsTypes(navigationBars());
+        assertEquals(0, root.mMergedExcludeInsetsTypes);
+        assertEquals(ime(), child1.mMergedExcludeInsetsTypes);
+        assertEquals(ime() | navigationBars(), child11.mMergedExcludeInsetsTypes);
+        assertEquals(ime(), child12.mMergedExcludeInsetsTypes);
+        assertEquals(0, child2.mMergedExcludeInsetsTypes);
+        assertEquals(0, child21.mMergedExcludeInsetsTypes);
+
+        // overwriting the same value has no change
+        for (int i = 0; i < 2; i++) {
+            root.setExcludeInsetsTypes(statusBars());
+            assertEquals(statusBars(), root.mMergedExcludeInsetsTypes);
+            assertEquals(statusBars() | ime(), child1.mMergedExcludeInsetsTypes);
+            assertEquals(statusBars() | ime() | navigationBars(),
+                    child11.mMergedExcludeInsetsTypes);
+            assertEquals(statusBars() | ime(), child12.mMergedExcludeInsetsTypes);
+            assertEquals(statusBars(), child2.mMergedExcludeInsetsTypes);
+            assertEquals(statusBars(), child21.mMergedExcludeInsetsTypes);
+        }
+
+        // set and reset type statusBars on child. Should have no effect because of parent
+        child2.setExcludeInsetsTypes(statusBars());
+        assertEquals(statusBars(), root.mMergedExcludeInsetsTypes);
+        assertEquals(statusBars() | ime(), child1.mMergedExcludeInsetsTypes);
+        assertEquals(statusBars() | ime() | navigationBars(), child11.mMergedExcludeInsetsTypes);
+        assertEquals(statusBars() | ime(), child12.mMergedExcludeInsetsTypes);
+        assertEquals(statusBars(), child2.mMergedExcludeInsetsTypes);
+        assertEquals(statusBars(), child21.mMergedExcludeInsetsTypes);
+
+        // reset
+        child2.setExcludeInsetsTypes(0);
+        assertEquals(statusBars(), root.mMergedExcludeInsetsTypes);
+        assertEquals(statusBars() | ime(), child1.mMergedExcludeInsetsTypes);
+        assertEquals(statusBars() | ime() | navigationBars(), child11.mMergedExcludeInsetsTypes);
+        assertEquals(statusBars() | ime(), child12.mMergedExcludeInsetsTypes);
+        assertEquals(statusBars(), child2.mMergedExcludeInsetsTypes);
+        assertEquals(statusBars(), child21.mMergedExcludeInsetsTypes);
+
+        // when parent has statusBars also removed, it should be cleared from all children in the
+        // hierarchy
+        root.setExcludeInsetsTypes(0);
+        assertEquals(0, root.mMergedExcludeInsetsTypes);
+        assertEquals(ime(), child1.mMergedExcludeInsetsTypes);
+        assertEquals(ime() | navigationBars(), child11.mMergedExcludeInsetsTypes);
+        assertEquals(ime(), child12.mMergedExcludeInsetsTypes);
+        assertEquals(0, child2.mMergedExcludeInsetsTypes);
+        assertEquals(0, child21.mMergedExcludeInsetsTypes);
+
+        // change on node should have no effect on siblings
+        child12.setExcludeInsetsTypes(captionBar());
+        assertEquals(0, root.mMergedExcludeInsetsTypes);
+        assertEquals(ime(), child1.mMergedExcludeInsetsTypes);
+        assertEquals(ime() | navigationBars(), child11.mMergedExcludeInsetsTypes);
+        assertEquals(captionBar() | ime(), child12.mMergedExcludeInsetsTypes);
+        assertEquals(0, child2.mMergedExcludeInsetsTypes);
+        assertEquals(0, child21.mMergedExcludeInsetsTypes);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.inputmethod.Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
+    public void testSetExcludeInsetsTypes_appliedAfterReparenting() {
+        final SurfaceControl mockSurfaceControl = mock(SurfaceControl.class);
+        final DisplayContent mockDisplayContent = mock(DisplayContent.class);
+        final var mockInsetsStateController = mock(InsetsStateController.class);
+        doReturn(mockInsetsStateController).when(mockDisplayContent).getInsetsStateController();
+
+        final WindowContainer root1 = createWindowContainerSpy(mockSurfaceControl,
+                mockDisplayContent);
+        final WindowContainer root2 = createWindowContainerSpy(mockSurfaceControl,
+                mockDisplayContent);
+        final WindowContainer child = createWindowContainerSpy(mockSurfaceControl,
+                mockDisplayContent);
+        doNothing().when(child).onConfigurationChanged(any());
+
+        root1.setExcludeInsetsTypes(ime());
+        root2.setExcludeInsetsTypes(captionBar());
+        assertEquals(ime(), root1.mMergedExcludeInsetsTypes);
+        assertEquals(captionBar(), root2.mMergedExcludeInsetsTypes);
+        assertEquals(0, child.mMergedExcludeInsetsTypes);
+        clearInvocations(mockInsetsStateController);
+
+        root1.addChild(child, 0);
+        assertEquals(ime(), child.mMergedExcludeInsetsTypes);
+        verify(mockInsetsStateController).notifyInsetsChanged(
+                new ArraySet<>(List.of(child.asWindowState())));
+        clearInvocations(mockInsetsStateController);
+
+        // Make sure that reparenting does not call notifyInsetsChanged twice
+        child.reparent(root2, 0);
+        assertEquals(captionBar(), child.mMergedExcludeInsetsTypes);
+        verify(mockInsetsStateController).notifyInsetsChanged(
+                new ArraySet<>(List.of(child.asWindowState())));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(android.view.inputmethod.Flags.FLAG_REFACTOR_INSETS_CONTROLLER)
+    public void testSetExcludeInsetsTypes_notifyInsetsAfterChange() {
+        final var mockDisplayContent = mock(DisplayContent.class);
+        final var mockInsetsStateController = mock(InsetsStateController.class);
+        doReturn(mockInsetsStateController).when(mockDisplayContent).getInsetsStateController();
+
+        final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
+        final WindowState mockRootWs = mock(WindowState.class);
+        final TestWindowContainer root = builder.setLayer(0).setAsWindowState(mockRootWs).build();
+        root.mDisplayContent = mockDisplayContent;
+        verify(mockInsetsStateController, never()).notifyInsetsChanged(any());
+
+        root.setExcludeInsetsTypes(ime());
+        assertEquals(ime(), root.mMergedExcludeInsetsTypes);
+        verify(mockInsetsStateController).notifyInsetsChanged(
+                new ArraySet<>(List.of(root.asWindowState())));
+        clearInvocations(mockInsetsStateController);
+
+        // adding a child (while parent has set excludedInsetsTypes) should trigger
+        // notifyInsetsChanged
+        final WindowState mockChildWs = mock(WindowState.class);
+        final TestWindowContainer child1 = builder.setLayer(0).setAsWindowState(
+                mockChildWs).build();
+        child1.mDisplayContent = mockDisplayContent;
+        root.addChildWindow(child1);
+        // TestWindowContainer overrides onParentChanged and therefore doesn't call into
+        // mergeExcludeInsetsTypesAndNotifyInsetsChanged. This is checked in another test
+        assertTrue(child1.mOnParentChangedCalled);
+        child1.setExcludeInsetsTypes(ime());
+        assertEquals(ime(), child1.mMergedExcludeInsetsTypes);
+        verify(mockInsetsStateController).notifyInsetsChanged(
+                new ArraySet<>(List.of(child1.asWindowState())));
+        clearInvocations(mockInsetsStateController);
+
+        // not changing excludedInsetsTypes should not trigger notifyInsetsChanged again
+        root.setExcludeInsetsTypes(ime());
+        assertEquals(ime(), root.mMergedExcludeInsetsTypes);
+        assertEquals(ime(), child1.mMergedExcludeInsetsTypes);
+        verify(mockInsetsStateController, never()).notifyInsetsChanged(any());
+    }
+
+    private WindowContainer<?> createWindowContainerSpy(SurfaceControl mockSurfaceControl,
+            DisplayContent mockDisplayContent) {
+        final WindowContainer<?> wc = spy(new WindowContainer<>(mWm));
+        final WindowState mocWs = mock(WindowState.class);
+        doReturn(mocWs).when(wc).asWindowState();
+        wc.mSurfaceControl = mockSurfaceControl;
+        wc.mDisplayContent = mockDisplayContent;
+        return wc;
+    }
+
     private static boolean hasLocalSource(WindowContainer container, int sourceId) {
         if (container.mLocalInsetsSources == null) {
             return false;
@@ -1693,6 +1873,7 @@
         private boolean mFillsParent;
         private boolean mWaitForTransitStart;
         private Integer mOrientation;
+        private WindowState mWindowState;
 
         private boolean mOnParentChangedCalled;
         private boolean mOnDescendantOverrideCalled;
@@ -1714,7 +1895,7 @@
         };
 
         TestWindowContainer(WindowManagerService wm, int layer, boolean isAnimating,
-                boolean isVisible, boolean waitTransitStart, Integer orientation) {
+                boolean isVisible, boolean waitTransitStart, Integer orientation, WindowState ws) {
             super(wm);
 
             mLayer = layer;
@@ -1723,6 +1904,7 @@
             mFillsParent = true;
             mOrientation = orientation;
             mWaitForTransitStart = waitTransitStart;
+            mWindowState = ws;
             spyOn(mSurfaceAnimator);
             doReturn(mIsAnimating).when(mSurfaceAnimator).isAnimating();
             doReturn(ANIMATION_TYPE_APP_TRANSITION).when(mSurfaceAnimator).getAnimationType();
@@ -1790,6 +1972,11 @@
         boolean isWaitingForTransitionStart() {
             return mWaitForTransitStart;
         }
+
+        @Override
+        WindowState asWindowState() {
+            return mWindowState;
+        }
     }
 
     private static class TestWindowContainerBuilder {
@@ -1799,6 +1986,7 @@
         private boolean mIsVisible;
         private boolean mIsWaitTransitStart;
         private Integer mOrientation;
+        private WindowState mWindowState;
 
         TestWindowContainerBuilder(WindowManagerService wm) {
             mWm = wm;
@@ -1806,6 +1994,7 @@
             mIsAnimating = false;
             mIsVisible = false;
             mOrientation = null;
+            mWindowState = null;
         }
 
         TestWindowContainerBuilder setLayer(int layer) {
@@ -1828,6 +2017,11 @@
             return this;
         }
 
+        TestWindowContainerBuilder setAsWindowState(WindowState ws) {
+            mWindowState = ws;
+            return this;
+        }
+
         TestWindowContainerBuilder setWaitForTransitionStart(boolean waitTransitStart) {
             mIsWaitTransitStart = waitTransitStart;
             return this;
@@ -1835,7 +2029,7 @@
 
         TestWindowContainer build() {
             return new TestWindowContainer(mWm, mLayer, mIsAnimating, mIsVisible,
-                    mIsWaitTransitStart, mOrientation);
+                    mIsWaitTransitStart, mOrientation, mWindowState);
         }
     }
 
diff --git a/tools/processors/property_cache/Android.bp b/tools/processors/property_cache/Android.bp
new file mode 100644
index 0000000..81fab7a
--- /dev/null
+++ b/tools/processors/property_cache/Android.bp
@@ -0,0 +1,57 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+    default_team: "trendy_team_framework_android_multiuser",
+}
+
+java_library_host {
+    name: "libcached-property-annotation-processor",
+    srcs: [
+        ":framework-annotations",
+        "src/**/*.java",
+    ],
+    static_libs: [
+        "codegen-version-info",
+        "android.multiuser.flags-aconfig-java-host",
+        "guava",
+    ],
+    use_tools_jar: true,
+}
+
+java_plugin {
+    name: "cached-property-annotation-processor",
+    processor_class: "android.processor.property_cache.CachedPropertyProcessor",
+    static_libs: ["libcached-property-annotation-processor"],
+}
+
+java_aconfig_library {
+    name: "android.multiuser.flags-aconfig-java-host",
+    aconfig_declarations: "android.multiuser.flags-aconfig",
+    host_supported: true,
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
+java_test_host {
+    name: "cached-property-annotation-processor-test",
+    srcs: ["test/java/**/*.java"],
+    java_resources: [":CachedPropertyAnnotationJavaTestSource"],
+    static_libs: [
+        "compile-testing-prebuilt",
+        "truth",
+        "junit",
+        "guava",
+        "libcached-property-annotation-processor",
+    ],
+    test_suites: ["general-tests"],
+}
+
+filegroup {
+    name: "CachedPropertyAnnotationJavaTestSource",
+    srcs: ["test/resources/*.java"],
+    path: "test/resources/",
+    visibility: ["//visibility:private"],
+}
diff --git a/tools/processors/property_cache/TEST_MAPPING b/tools/processors/property_cache/TEST_MAPPING
new file mode 100644
index 0000000..7177abc
--- /dev/null
+++ b/tools/processors/property_cache/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "postsubmit": [
+    {
+      "name": "cached-property-annotation-processor-test"
+    }
+  ]
+}
diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/CacheConfig.java b/tools/processors/property_cache/src/java/android/processor/property_cache/CacheConfig.java
new file mode 100644
index 0000000..c665c84
--- /dev/null
+++ b/tools/processors/property_cache/src/java/android/processor/property_cache/CacheConfig.java
@@ -0,0 +1,138 @@
+/*
+ * 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 android.processor.property_cache;
+
+import com.android.internal.annotations.CachedProperty;
+import com.android.internal.annotations.CachedPropertyDefaults;
+
+import com.google.common.base.CaseFormat;
+
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+
+public class CacheConfig {
+    private final CacheModifiers mModifiers;
+    private final int mMaxSize;
+    private final String mModuleName;
+    private final String mApiName;
+    private final String mClassName;
+    private final String mQualifiedName;
+    private String mPropertyName;
+    private String mMethodName;
+    private int mNumberOfParams = 0;
+    private String mInputType = Constants.JAVA_LANG_VOID;
+    private String mResultType;
+
+    public CacheConfig(TypeElement classElement, ExecutableElement method) {
+        CachedPropertyDefaults classAnnotation = classElement.getAnnotation(
+                CachedPropertyDefaults.class);
+        CachedProperty methodAnnotation = method.getAnnotation(CachedProperty.class);
+
+        mModuleName = methodAnnotation.module().isEmpty() ? classAnnotation.module()
+                : methodAnnotation.module();
+        mClassName = classElement.getSimpleName().toString();
+        mQualifiedName = classElement.getQualifiedName().toString();
+        mModifiers = new CacheModifiers(methodAnnotation.modsFlagOnOrNone());
+        mMethodName = method.getSimpleName().toString();
+        mPropertyName = getPropertyName(mMethodName);
+        mApiName = methodAnnotation.api().isEmpty() ? getUniqueApiName(mClassName, mPropertyName)
+                : methodAnnotation.api();
+        mMaxSize = methodAnnotation.max() == -1 ? classAnnotation.max() : methodAnnotation.max();
+        mNumberOfParams = method.getParameters().size();
+        if (mNumberOfParams > 0) {
+            mInputType = primitiveTypeToObjectEquivalent(
+                method.getParameters().get(0).asType().toString());
+        }
+        mResultType = primitiveTypeToObjectEquivalent(method.getReturnType().toString());
+    }
+
+    public CacheModifiers getModifiers() {
+        return mModifiers;
+    }
+
+    public int getMaxSize() {
+        return mMaxSize;
+    }
+
+    public String getApiName() {
+        return mApiName;
+    }
+
+    public String getClassName() {
+        return mClassName;
+    }
+
+    public String getQualifiedName() {
+        return mQualifiedName;
+    }
+
+    public String getModuleName() {
+        return mModuleName;
+    }
+
+    public String getMethodName() {
+        return mMethodName;
+    }
+
+    public String getPropertyName() {
+        return mPropertyName;
+    }
+
+    public String getPropertyVariable() {
+        return (mModifiers.isStatic() ? "s" : "m") + mPropertyName;
+    }
+
+    private String getPropertyName(String methodName) {
+        if (methodName.startsWith("get")) {
+            return methodName.substring(3);
+        } else if (methodName.startsWith("is")) {
+            return methodName.substring(2);
+        } else {
+            return CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, methodName);
+        }
+    }
+
+    public int getNumberOfParams() {
+        return mNumberOfParams;
+    }
+
+    public String getInputType() {
+        return mInputType;
+    }
+
+    public String getResultType() {
+        return mResultType;
+    }
+
+    /**
+     * This method returns the unique api name for a given class and property name.
+     * Property name is retrieved from the method name.
+     * Both names are combined and converted to lower snake case.
+     *
+     * @param className    The name of the class that contains the property.
+     * @param propertyName The name of the property.
+     * @return The registration name for the property.
+     */
+    private String getUniqueApiName(String className, String propertyName) {
+        return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, className + propertyName);
+    }
+
+    private String primitiveTypeToObjectEquivalent(String simpleType) {
+        // checking against primitive types
+        return Constants.PRIMITIVE_TYPE_MAP.getOrDefault(simpleType, simpleType);
+    }
+}
diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/CacheModifiers.java b/tools/processors/property_cache/src/java/android/processor/property_cache/CacheModifiers.java
new file mode 100644
index 0000000..fda9b2c
--- /dev/null
+++ b/tools/processors/property_cache/src/java/android/processor/property_cache/CacheModifiers.java
@@ -0,0 +1,40 @@
+/*
+ * 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 android.processor.property_cache;
+
+import com.android.internal.annotations.CacheModifier;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class CacheModifiers {
+    private final boolean mIsStatic;
+    private static final String STATIC_MODIFIER_STRING = "static ";
+
+    CacheModifiers(CacheModifier[] modifierArray) {
+        final List<CacheModifier> modifiers = Arrays.asList(modifierArray);
+        mIsStatic = modifiers.contains(CacheModifier.STATIC);
+    }
+
+    public boolean isStatic() {
+        return mIsStatic;
+    }
+
+    public String getStaticModifier() {
+        return mIsStatic ? STATIC_MODIFIER_STRING : Constants.EMPTY_STRING;
+    }
+}
diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/CachedPropertyProcessor.java b/tools/processors/property_cache/src/java/android/processor/property_cache/CachedPropertyProcessor.java
new file mode 100644
index 0000000..0361012
--- /dev/null
+++ b/tools/processors/property_cache/src/java/android/processor/property_cache/CachedPropertyProcessor.java
@@ -0,0 +1,95 @@
+/*
+ * 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 android.processor.property_cache;
+
+import com.android.internal.annotations.CachedProperty;
+import com.android.internal.annotations.CachedPropertyDefaults;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.RoundEnvironment;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.util.ElementFilter;
+import javax.tools.JavaFileObject;
+
+public class CachedPropertyProcessor extends AbstractProcessor {
+
+    IpcDataCacheComposer mIpcDataCacheComposer =
+            new IpcDataCacheComposer();
+
+    @Override
+    public Set<String> getSupportedAnnotationTypes() {
+        return new HashSet<String>(
+                ImmutableSet.of(CachedPropertyDefaults.class.getCanonicalName()));
+    }
+
+    @Override
+    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+        for (Element element : roundEnv.getElementsAnnotatedWith(CachedPropertyDefaults.class)) {
+            try {
+                generateCachedClass((TypeElement) element, processingEnv.getFiler());
+            } catch (IOException e) {
+                e.printStackTrace();
+                return false;
+            }
+        }
+        return false;
+    }
+
+    private void generateCachedClass(TypeElement classElement, Filer filer) throws IOException {
+        String packageName =
+                processingEnv
+                        .getElementUtils()
+                        .getPackageOf(classElement)
+                        .getQualifiedName()
+                        .toString();
+        String className = classElement.getSimpleName().toString() + "Cache";
+        JavaFileObject jfo = filer.createSourceFile(packageName + "." + className);
+        Writer writer = jfo.openWriter();
+        writer.write("package " + packageName + ";\n\n");
+        writer.write("import android.os.IpcDataCache;\n");
+        writer.write("\n    /** \n    * This class is auto-generated \n    * @hide \n    **/");
+        writer.write("\npublic class " + className + " {\n");
+
+        List<ExecutableElement> methods =
+                ElementFilter.methodsIn(classElement.getEnclosedElements());
+        String initCache = String.format(Constants.METHOD_COMMENT,
+                " - initialise all caches for class " + className)
+                + "\npublic static void initCache() {";
+        for (ExecutableElement method : methods) {
+            if (method.getAnnotation(CachedProperty.class) != null) {
+                mIpcDataCacheComposer.generatePropertyCache(writer, classElement, method);
+                initCache += "\n    " + mIpcDataCacheComposer.generateInvalidatePropertyCall();
+            }
+        }
+        initCache += "\n}";
+        writer.write(initCache);
+        writer.write("\n}");
+        writer.write("\n");
+        writer.close();
+    }
+}
diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/Constants.java b/tools/processors/property_cache/src/java/android/processor/property_cache/Constants.java
new file mode 100644
index 0000000..03961bc
--- /dev/null
+++ b/tools/processors/property_cache/src/java/android/processor/property_cache/Constants.java
@@ -0,0 +1,40 @@
+/*
+ * 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 android.processor.property_cache;
+
+import com.google.common.collect.ImmutableMap;
+
+public final class Constants {
+    public static final String EMPTY_STRING = "";
+    public static final String JAVA_LANG_VOID = "java.lang.Void";
+    public static final ImmutableMap<String, String> PRIMITIVE_TYPE_MAP =
+            ImmutableMap.of(
+                    "int", "java.lang.Integer",
+                    "boolean", "java.lang.Boolean",
+                    "long", "java.lang.Long",
+                    "float", "java.lang.Float",
+                    "double", "java.lang.Double",
+                    "byte", "java.lang.Byte",
+                    "short", "java.lang.Short",
+                    "char", "java.lang.Character");
+
+    public static final String METHOD_COMMENT = "\n    /**"
+            + "\n    * This method is auto-generated%s"
+            + "\n    * "
+            + "\n    * @hide"
+            + "\n    */";
+}
diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/IpcDataCacheComposer.java b/tools/processors/property_cache/src/java/android/processor/property_cache/IpcDataCacheComposer.java
new file mode 100644
index 0000000..8526a04
--- /dev/null
+++ b/tools/processors/property_cache/src/java/android/processor/property_cache/IpcDataCacheComposer.java
@@ -0,0 +1,152 @@
+/*
+ * 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 android.processor.property_cache;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+
+public class IpcDataCacheComposer {
+
+    private static final String PROPERTY_DEFINITION_LINE = "private %s%s %s;\n";
+    private static final String METHOD_NAME_LINE = "\npublic %s%s %s(%s%s%s\n) {\n";
+    private static final String RETURN_IF_NOT_NULL_LINE =
+            "if (%s != null) {\n   return %s.%s;\n  }";
+
+    private CacheConfig mCacheConfig;
+
+    /**
+     * Generates code for property cache.
+     *
+     * @param writer       writer to write code to.
+     * @param classElement class element to generate code for.
+     * @param method       method element to generate code for.
+     * @throws IOException if writer throws IOException.
+     */
+    public void generatePropertyCache(Writer writer, TypeElement classElement,
+            ExecutableElement method) throws IOException {
+
+        mCacheConfig = new CacheConfig(classElement, method);
+
+        ParamComposer inputParam = new ParamComposer(null, null);
+        ParamComposer binderParam = new ParamComposer(
+                String.format("IpcDataCache.RemoteCall<%s, %s>", mCacheConfig.getInputType(),
+                        mCacheConfig.getResultType()), "binderCall");
+
+        ParamComposer bypassParam = new ParamComposer(null, null); // empty if method have no params
+        String queryCall = "query(null)";
+        if (mCacheConfig.getNumberOfParams() > 0) {
+            bypassParam = new ParamComposer(
+                    String.format("IpcDataCache.BypassCall<%s> ", mCacheConfig.getInputType()),
+                    "bypassPredicate");
+            inputParam = new ParamComposer(mCacheConfig.getInputType(), "query");
+            queryCall = "query(query)";
+        }
+        String propertyClass =
+                "IpcDataCache<" + mCacheConfig.getInputType() + ", " + mCacheConfig.getResultType()
+                        + ">";
+        String invalidateName = "invalidate" + mCacheConfig.getPropertyName();
+        String lockObject = mCacheConfig.getPropertyVariable() + "Lock";
+        writer.write("private " + mCacheConfig.getModifiers().getStaticModifier() + "final Object "
+                + lockObject + " = new Object();\n");
+        writer.write(String.format(PROPERTY_DEFINITION_LINE,
+                mCacheConfig.getModifiers().getStaticModifier(), propertyClass,
+                mCacheConfig.getPropertyVariable()));
+
+        writer.write(propertyInvalidatedCacheMethod(binderParam, bypassParam, inputParam, queryCall,
+                lockObject));
+
+        // If binder param is not empty then generate getter without binder param to be called
+        if (!bypassParam.getParam().isEmpty()) {
+            writer.write(propertyInvalidatedCacheMethod(binderParam, new ParamComposer(null, null),
+                    inputParam, queryCall, lockObject));
+        }
+        writer.write(String.format(Constants.METHOD_COMMENT,
+                "- invalidate cache for {@link  " + mCacheConfig.getQualifiedName() + "#"
+                        + mCacheConfig.getMethodName() + "}"));
+        writer.write("\n public static final void " + invalidateName + "() {");
+        writer.write(
+                "\n     IpcDataCache.invalidateCache(\"" + mCacheConfig.getModuleName() + "\", \""
+                        + mCacheConfig.getApiName() + "\");");
+        writer.write("\n }");
+        writer.write("\n");
+        writer.write("\n");
+    }
+
+    /**
+     * Generates code to call cache invalidation.
+     *
+     * @return code string calling cache invalidation.
+     */
+    public String generateInvalidatePropertyCall() {
+        String invalidateName = "invalidate" + mCacheConfig.getPropertyName();
+        return mCacheConfig.getClassName() + "Cache." + invalidateName + "();";
+    }
+
+    /**
+     * Generates code for getter that returns cached value or calls binder and caches result.
+     *
+     * @param binderParam parameter for binder call.
+     * @param bypassParam parameter for bypass predicate.
+     * @param inputParam  parameter for input value.
+     * @param queryCall   cache query call syntax.
+     * @param lockObject  object to synchronize on.
+     * @return String with code for method.
+     */
+    private String propertyInvalidatedCacheMethod(ParamComposer binderParam,
+            ParamComposer bypassParam, ParamComposer inputParam, String queryCall,
+            String lockObject) {
+        String result = "\n";
+        CacheModifiers modifiers = mCacheConfig.getModifiers();
+        String paramsComments = binderParam.getParamComment(
+                "lambda for remote call" + " {@link  " + mCacheConfig.getQualifiedName() + "#"
+                        + mCacheConfig.getMethodName() + " }") + bypassParam.getParamComment(
+                "lambda to bypass remote call") + inputParam.getParamComment(
+                "parameter to call remote lambda");
+        result += String.format(Constants.METHOD_COMMENT, paramsComments);
+        result += String.format(METHOD_NAME_LINE, modifiers.getStaticModifier(),
+                mCacheConfig.getResultType(), mCacheConfig.getMethodName(),
+                binderParam.getParam(), bypassParam.getNextParam(),
+                inputParam.getNextParam());
+        result += String.format(RETURN_IF_NOT_NULL_LINE, mCacheConfig.getPropertyVariable(),
+                mCacheConfig.getPropertyVariable(), queryCall);
+        result += "\n  synchronized (" + lockObject + " ) {";
+        result += "\n    if (" + mCacheConfig.getPropertyVariable() + " == null) {";
+        result += "\n      " + mCacheConfig.getPropertyVariable() + " = new IpcDataCache" + "("
+                + generateCreateIpcConfig() + ", " + binderParam.getName()
+                + bypassParam.getNextName() + ");\n";
+        result += "\n   }";
+        result += "\n  }";
+        result += "\n  return " + mCacheConfig.getPropertyVariable() + "." + queryCall + ";";
+        result += "\n }";
+        result += "\n";
+        return result;
+    }
+
+    /**
+     * Generates code for new IpcDataCache.Config object for given configuration.
+     *
+     * @return String with code for new IpcDataCache.Config object.
+     */
+    public String generateCreateIpcConfig() {
+        return "new IpcDataCache.Config(" + mCacheConfig.getMaxSize() + ", " + "\""
+                + mCacheConfig.getModuleName() + "\"" + ", " + "\"" + mCacheConfig.getApiName()
+                + "\"" + ", " + "\"" + mCacheConfig.getPropertyName() + "\"" + ")";
+    }
+}
diff --git a/tools/processors/property_cache/src/java/android/processor/property_cache/ParamComposer.java b/tools/processors/property_cache/src/java/android/processor/property_cache/ParamComposer.java
new file mode 100644
index 0000000..307443a
--- /dev/null
+++ b/tools/processors/property_cache/src/java/android/processor/property_cache/ParamComposer.java
@@ -0,0 +1,103 @@
+/*
+ * 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 android.processor.property_cache;
+
+public class ParamComposer {
+    private String mType;
+    private String mName;
+
+    /** Creates ParamComposer with given type and name.
+     *
+     * @param type type of parameter.
+     * @param name name of parameter.
+     */
+    public ParamComposer(String type, String name) {
+        mType = type;
+        mName = name;
+    }
+
+    /** Returns name of parameter.
+     *
+     * @return name of parameter.
+     */
+    public String getName() {
+        if (mName != null) {
+            return mName;
+        }
+        return Constants.EMPTY_STRING;
+    }
+
+    /** Returns name of parameter for next parameter followed by comma.
+     *
+     * @return name of parameter for next parameter if exists, empty string otherwise.
+     */
+    public String getNextName() {
+        if (!getName().isEmpty()) {
+            return ", " + getName();
+        }
+        return Constants.EMPTY_STRING;
+    }
+
+    /**
+     * Returns type of parameter.
+     *
+     * @return type of parameter.
+     */
+    public String getType() {
+        if (mType != null) {
+            return mType;
+        }
+        return Constants.EMPTY_STRING;
+    }
+
+    /**
+     * Returns type and name of parameter.
+     *
+     * @return type and name of parameter if exists, empty string otherwise.
+     */
+    public String getParam() {
+        if (!getType().isEmpty() && !getName().isEmpty()) {
+            return getType() + " " + getName();
+        }
+        return Constants.EMPTY_STRING;
+    }
+
+    /**
+     * Returns type and name of parameter for next parameter followed by comma.
+     *
+     * @return type and name of parameter for next parameter if exists, empty string otherwise.
+     */
+    public String getNextParam() {
+        if (!getType().isEmpty() && !getName().isEmpty()) {
+            return ", " + getParam();
+        }
+        return Constants.EMPTY_STRING;
+    }
+
+    /**
+     * Returns comment for parameter.
+     *
+     * @param description of parameter.
+     * @return comment for parameter if exists, empty string otherwise.
+     */
+    public String getParamComment(String description) {
+        if (!getType().isEmpty() && !getName().isEmpty()) {
+            return "\n    * @param " + getName() + " - " + description;
+        }
+        return Constants.EMPTY_STRING;
+    }
+}
diff --git a/tools/processors/property_cache/test/java/android/processor/property_cache/CachedPropertyProcessorTest.java b/tools/processors/property_cache/test/java/android/processor/property_cache/CachedPropertyProcessorTest.java
new file mode 100644
index 0000000..1e23c78
--- /dev/null
+++ b/tools/processors/property_cache/test/java/android/processor/property_cache/CachedPropertyProcessorTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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 android.processor.property_cache.test;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.testing.compile.CompilationSubject.assertThat;
+
+import android.processor.property_cache.CachedPropertyProcessor;
+
+import com.google.testing.compile.Compilation;
+import com.google.testing.compile.Compiler;
+import com.google.testing.compile.JavaFileObjects;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import javax.tools.JavaFileObject;
+import javax.tools.StandardLocation;
+
+/** Tests the {@link CachedPropertyProcessor}. */
+@RunWith(JUnit4.class)
+public class CachedPropertyProcessorTest {
+    private final Compiler mCompiler =
+            Compiler.javac().withProcessors(new CachedPropertyProcessor());
+
+    @Test
+    public void testDefaultValues() {
+        JavaFileObject expectedJava = JavaFileObjects.forResource("DefaultCache.java");
+
+        Compilation compilation = mCompiler.compile(JavaFileObjects.forResource("Default.java"));
+        assertThat(compilation).succeeded();
+        assertThat(compilation)
+                .generatedFile(StandardLocation.SOURCE_OUTPUT,
+                        "android/processor/property_cache/test/DefaultCache.java")
+                .hasSourceEquivalentTo(expectedJava);
+    }
+
+    @Test
+    public void testCustomValues() {
+        JavaFileObject expectedJava = JavaFileObjects.forResource("CustomCache.java");
+
+        Compilation compilation = mCompiler.compile(JavaFileObjects.forResource("Custom.java"));
+        assertThat(compilation).succeeded();
+        assertThat(compilation)
+                .generatedFile(StandardLocation.SOURCE_OUTPUT,
+                        "android/processor/property_cache/test/CustomCache.java")
+                .hasSourceEquivalentTo(expectedJava);
+    }
+}
diff --git a/tools/processors/property_cache/test/java/android/processor/property_cache/shadows/IpcDataCache.java b/tools/processors/property_cache/test/java/android/processor/property_cache/shadows/IpcDataCache.java
new file mode 100644
index 0000000..e5ef48c
--- /dev/null
+++ b/tools/processors/property_cache/test/java/android/processor/property_cache/shadows/IpcDataCache.java
@@ -0,0 +1,134 @@
+/*
+ * 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 android.os;
+
+// Mocked class for generation compilation tests purposes only.
+public class IpcDataCache<Input, Output> {
+    public static class Config {
+        public Config(int max, String module, String api, String name) {
+        }
+    }
+
+    /** Shadow method for generated code compilation tests purposes only.
+     *
+     * @param query - shadow parameter from IpcDataCache in Frameworks.
+     * @return null
+     */
+    public Output query(Input query) {
+        return null;
+    }
+
+    /** Shadow method for generated code compilation tests purposes only.
+     *
+     * @param key - shadow parameter from IpcDataCache in Frameworks;
+     */
+    public static void invalidateCache(String key) {
+    }
+
+    /** Shadow method for generated code compilation tests purposes only.
+     *
+     * @param query - shadow parameter from IpcDataCache in Frameworks;
+     * @return null
+     */
+    public Output recompute(Input query) {
+        return null;
+    }
+
+    /** Shadow method for generated code compilation tests purposes only.
+     *
+     * @param query - parameter equivalent to IpcDataCache in android framework.
+     * @param query - shadow parameter from IpcDataCache in Frameworks;
+     * @return false
+     */
+    public boolean bypass(Input query) {
+        return false;
+    }
+
+    /** Shadow method for generated code compilation tests purposes only.
+     *
+     * @param module - parameter equivalent to IpcDataCache in android framework.
+     * @param key - parameter equivalent to IpcDataCache in android framework.
+     * @return module + key sttring
+     */
+    public static String createPropertyName(String module, String key) {
+        return module + key;
+    }
+
+    public abstract static class QueryHandler<Input, Output> {
+        /** Shadow method for generated code compilation tests purposes only.
+         *
+         * @param query - parameter equivalent to IpcDataCache.QueryHandler in android framework.
+         * @return expected value
+         */
+        public abstract Output apply(Input query);
+        /** Shadow method for generated code compilation tests purposes only.
+         *
+         * @param query - parameter equivalent to IpcDataCache.QueryHandler in android framework.
+         */
+        public boolean shouldBypassCache(Input query) {
+            return false;
+        }
+    }
+
+    public interface RemoteCall<Input, Output> {
+        /** Shadow method for generated code compilation tests purposes only.
+         *
+         * @param query - parameter equivalent to IpcDataCache.RemoteCall in android framework.
+         */
+        Output apply(Input query);
+    }
+
+    public interface BypassCall<Input> {
+        /** Shadow method for generated code compilation tests purposes only.
+         *
+         * @param query - parameter equivalent to IpcDataCache.BypassCall in android framework.
+         */
+        boolean apply(Input query);
+    }
+
+    public IpcDataCache(
+            int maxEntries,
+            String module,
+            String api,
+            String cacheName,
+            QueryHandler<Input, Output> computer) {
+    }
+
+    public IpcDataCache(Config config, QueryHandler<Input, Output> computer) {
+    }
+
+    public IpcDataCache(Config config, RemoteCall<Input, Output> computer) {
+    }
+
+    public IpcDataCache(Config config, RemoteCall<Input, Output> computer,
+            BypassCall<Input> bypassCall) {
+    }
+
+    /** Shadow method for generated code compilation tests purposes only.*/
+    public void invalidateCache() {
+    }
+
+
+    /** Shadow method for generated code compilation tests purposes only.
+     *
+     * @param module - shadow parameter from IpcDataCache in Frameworks.
+     * @param api - shadow parameter from IpcDataCache in Frameworks.
+     */
+    public static void invalidateCache(String module, String api) {
+    }
+
+}
diff --git a/tools/processors/property_cache/test/resources/Custom.java b/tools/processors/property_cache/test/resources/Custom.java
new file mode 100644
index 0000000..05024da
--- /dev/null
+++ b/tools/processors/property_cache/test/resources/Custom.java
@@ -0,0 +1,127 @@
+/*
+ * 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 android.processor.property_cache.test;
+
+import com.android.internal.annotations.CacheModifier;
+import com.android.internal.annotations.CachedProperty;
+import com.android.internal.annotations.CachedPropertyDefaults;
+
+import java.util.Date;
+
+@CachedPropertyDefaults(max = 4, module = "bluetooth")
+public class Custom {
+    BirthdayManagerService mService = new BirthdayManagerService();
+    Object mCache = new CustomCache();
+
+    public Custom() {
+        CustomCache.initCache();
+    }
+
+    /**
+     * Testing custom class values to generate static IpcDataCache
+     *
+     * @param userId - user Id
+     * @return birthday date of given user Id
+     */
+    @CachedProperty()
+    public Date getBirthday(int userId) {
+        return CustomCache.getBirthday(mService::getBirthday, userId);
+    }
+
+    /**
+     * Testing custom class values to generate static IpcDataCache
+     *
+     * @param userId - user Id
+     * @return number of days till birthday of given user Id
+     */
+    @CachedProperty(modsFlagOnOrNone = {CacheModifier.STATIC})
+    public int getDaysTillBirthday(int userId) {
+        return CustomCache.getDaysTillBirthday(mService::getDaysTillBirthday, userId);
+    }
+
+    /**
+     * Testing custom class values to generate non-static IpcDataCache
+     *
+     * @param userId - user Id
+     * @return number of days since birthday of given user Id
+     */
+    @CachedProperty(modsFlagOnOrNone = {})
+    public int getDaysSinceBirthday(int userId) {
+        return ((CustomCache) mCache).getDaysSinceBirthday(mService::getDaysSinceBirthday, userId);
+    }
+
+    /**
+     * Testing custom class values to generate static IpcDataCache with max capasity of 1
+     *
+     * @return number of days till birthay of current user
+     */
+    @CachedProperty(modsFlagOnOrNone = {CacheModifier.STATIC}, max = 1)
+    public int getDaysTillMyBirthday() {
+        return CustomCache.getDaysTillMyBirthday((Void) -> mService.getDaysTillMyBirthday());
+    }
+
+    /**
+     * Testing custom class values to generate static IpcDataCache with max capasity of 1 and custom
+     * api
+     *
+     * @return number of days since birthay of current user
+     */
+    @CachedProperty(modsFlagOnOrNone = {}, max = 1, api = "my_unique_key")
+    public int getDaysSinceMyBirthday() {
+        return ((CustomCache) mCache).getDaysSinceMyBirthday(
+                (Void) -> mService.getDaysSinceMyBirthday());
+    }
+
+    /**
+     * Testing custom class values to generate static IpcDataCache with custom module name
+     *
+     * @return birthday wishes of given user Id
+     */
+    @CachedProperty(module = "telephony")
+    public String getBirthdayWishesFromUser(int userId) {
+        return CustomCache.getBirthdayWishesFromUser(mService::getBirthdayWishesFromUser,
+                userId);
+    }
+
+    class BirthdayManagerService {
+        int mDaysTillBirthday = 182;
+
+        public Date getBirthday(int userId) {
+            return new Date(2024, 6, 1 + userId);
+        }
+
+        public int getDaysTillBirthday(int userId) {
+            return mDaysTillBirthday + userId;
+        }
+
+        public int getDaysSinceBirthday(int userId) {
+            return 365 - getDaysTillBirthday(userId);
+        }
+
+        public int getDaysTillMyBirthday() {
+            return 0;
+        }
+
+        public int getDaysSinceMyBirthday() {
+            return 365;
+        }
+
+        public String getBirthdayWishesFromUser(int userId) {
+            return "Happy Birthday!\n- " + userId;
+        }
+    }
+}
diff --git a/tools/processors/property_cache/test/resources/CustomCache.java b/tools/processors/property_cache/test/resources/CustomCache.java
new file mode 100644
index 0000000..326467f
--- /dev/null
+++ b/tools/processors/property_cache/test/resources/CustomCache.java
@@ -0,0 +1,384 @@
+/*
+ * 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 android.processor.property_cache.test;
+
+import android.os.IpcDataCache;
+
+/**
+ * This class is auto-generated
+ *
+ * @hide
+ **/
+public class CustomCache {
+    private static final Object sBirthdayLock = new Object();
+    private static IpcDataCache<java.lang.Integer, java.util.Date> sBirthday;
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall      - lambda for remote call
+     *                        {@link  android.processor.property_cache.test.Custom#getBirthday }
+     * @param bypassPredicate - lambda to bypass remote call
+     * @param query           - parameter to call remote lambda
+     * @hide
+     */
+    public static java.util.Date getBirthday(
+            IpcDataCache.RemoteCall<java.lang.Integer, java.util.Date> binderCall,
+            IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query) {
+        if (sBirthday != null) {
+            return sBirthday.query(query);
+        }
+        synchronized (sBirthdayLock) {
+            if (sBirthday == null) {
+                sBirthday = new IpcDataCache(
+                        new IpcDataCache.Config(4, "bluetooth", "custom_birthday", "Birthday"),
+                        binderCall, bypassPredicate);
+
+            }
+        }
+        return sBirthday.query(query);
+    }
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall - lambda for remote call
+     *                   {@link  android.processor.property_cache.test.Custom#getBirthday }
+     * @param query      - parameter to call remote lambda
+     * @hide
+     */
+    public static java.util.Date getBirthday(
+            IpcDataCache.RemoteCall<java.lang.Integer, java.util.Date> binderCall,
+            java.lang.Integer query) {
+        if (sBirthday != null) {
+            return sBirthday.query(query);
+        }
+        synchronized (sBirthdayLock) {
+            if (sBirthday == null) {
+                sBirthday = new IpcDataCache(
+                        new IpcDataCache.Config(4, "bluetooth", "custom_birthday", "Birthday"),
+                        binderCall);
+            }
+        }
+        return sBirthday.query(query);
+    }
+
+    /**
+     * This method is auto-generated- invalidate cache for
+     * {@link  android.processor.property_cache.test.Custom#getBirthday}
+     *
+     * @hide
+     */
+    public static final void invalidateBirthday() {
+        IpcDataCache.invalidateCache("bluetooth", "custom_birthday");
+    }
+
+    private static final Object sDaysTillBirthdayLock = new Object();
+    private static IpcDataCache<java.lang.Integer, java.lang.Integer> sDaysTillBirthday;
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall      - lambda for remote call
+     *                        {@link
+     *                        android.processor.property_cache.test.Custom#getDaysTillBirthday }
+     * @param bypassPredicate - lambda to bypass remote call
+     * @param query           - parameter to call remote lambda
+     * @hide
+     */
+    public static java.lang.Integer getDaysTillBirthday(
+            IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall,
+            IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query) {
+        if (sDaysTillBirthday != null) {
+            return sDaysTillBirthday.query(query);
+        }
+        synchronized (sDaysTillBirthdayLock) {
+            if (sDaysTillBirthday == null) {
+                sDaysTillBirthday = new IpcDataCache(
+                        new IpcDataCache.Config(4, "bluetooth", "custom_days_till_birthday",
+                                "DaysTillBirthday"), binderCall, bypassPredicate);
+
+            }
+        }
+        return sDaysTillBirthday.query(query);
+    }
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall - lambda for remote call
+     *                   {@link  android.processor.property_cache.test.Custom#getDaysTillBirthday }
+     * @param query      - parameter to call remote lambda
+     * @hide
+     */
+    public static java.lang.Integer getDaysTillBirthday(
+            IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall,
+            java.lang.Integer query) {
+        if (sDaysTillBirthday != null) {
+            return sDaysTillBirthday.query(query);
+        }
+        synchronized (sDaysTillBirthdayLock) {
+            if (sDaysTillBirthday == null) {
+                sDaysTillBirthday = new IpcDataCache(
+                        new IpcDataCache.Config(4, "bluetooth", "custom_days_till_birthday",
+                                "DaysTillBirthday"), binderCall);
+
+            }
+        }
+        return sDaysTillBirthday.query(query);
+    }
+
+    /**
+     * This method is auto-generated- invalidate cache for
+     * {@link  android.processor.property_cache.test.Custom#getDaysTillBirthday}
+     *
+     * @hide
+     */
+    public static final void invalidateDaysTillBirthday() {
+        IpcDataCache.invalidateCache("bluetooth", "custom_days_till_birthday");
+    }
+
+    private final Object mDaysSinceBirthdayLock = new Object();
+    private IpcDataCache<java.lang.Integer, java.lang.Integer> mDaysSinceBirthday;
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall      - lambda for remote call
+     *                        {@link
+     *                        android.processor.property_cache.test.Custom#getDaysSinceBirthday }
+     * @param bypassPredicate - lambda to bypass remote call
+     * @param query           - parameter to call remote lambda
+     * @hide
+     */
+    public java.lang.Integer getDaysSinceBirthday(
+            IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall,
+            IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query) {
+        if (mDaysSinceBirthday != null) {
+            return mDaysSinceBirthday.query(query);
+        }
+        synchronized (mDaysSinceBirthdayLock) {
+            if (mDaysSinceBirthday == null) {
+                mDaysSinceBirthday = new IpcDataCache(
+                        new IpcDataCache.Config(4, "bluetooth", "custom_days_since_birthday",
+                                "DaysSinceBirthday"), binderCall, bypassPredicate);
+
+            }
+        }
+        return mDaysSinceBirthday.query(query);
+    }
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall - lambda for remote call
+     *                   {@link  android.processor.property_cache.test.Custom#getDaysSinceBirthday
+     *                   }
+     * @param query      - parameter to call remote lambda
+     * @hide
+     */
+    public java.lang.Integer getDaysSinceBirthday(
+            IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall,
+            java.lang.Integer query) {
+        if (mDaysSinceBirthday != null) {
+            return mDaysSinceBirthday.query(query);
+        }
+        synchronized (mDaysSinceBirthdayLock) {
+            if (mDaysSinceBirthday == null) {
+                mDaysSinceBirthday = new IpcDataCache(
+                        new IpcDataCache.Config(4, "bluetooth", "custom_days_since_birthday",
+                                "DaysSinceBirthday"), binderCall);
+
+            }
+        }
+        return mDaysSinceBirthday.query(query);
+    }
+
+    /**
+     * This method is auto-generated- invalidate cache for
+     * {@link  android.processor.property_cache.test.Custom#getDaysSinceBirthday}
+     *
+     * @hide
+     */
+    public static final void invalidateDaysSinceBirthday() {
+        IpcDataCache.invalidateCache("bluetooth", "custom_days_since_birthday");
+    }
+
+    private static final Object sDaysTillMyBirthdayLock = new Object();
+    private static IpcDataCache<java.lang.Void, java.lang.Integer> sDaysTillMyBirthday;
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall - lambda for remote call
+     *                   {@link  android.processor.property_cache.test.Custom#getDaysTillMyBirthday
+     *                   }
+     * @hide
+     */
+    public static java.lang.Integer getDaysTillMyBirthday(
+            IpcDataCache.RemoteCall<java.lang.Void, java.lang.Integer> binderCall) {
+        if (sDaysTillMyBirthday != null) {
+            return sDaysTillMyBirthday.query(null);
+        }
+        synchronized (sDaysTillMyBirthdayLock) {
+            if (sDaysTillMyBirthday == null) {
+                sDaysTillMyBirthday = new IpcDataCache(
+                        new IpcDataCache.Config(1, "bluetooth", "custom_days_till_my_birthday",
+                                "DaysTillMyBirthday"), binderCall);
+
+            }
+        }
+        return sDaysTillMyBirthday.query(null);
+    }
+
+    /**
+     * This method is auto-generated- invalidate cache for
+     * {@link  android.processor.property_cache.test.Custom#getDaysTillMyBirthday}
+     *
+     * @hide
+     */
+    public static final void invalidateDaysTillMyBirthday() {
+        IpcDataCache.invalidateCache("bluetooth", "custom_days_till_my_birthday");
+    }
+
+    private final Object mDaysSinceMyBirthdayLock = new Object();
+    private IpcDataCache<java.lang.Void, java.lang.Integer> mDaysSinceMyBirthday;
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall - lambda for remote call
+     *                   {@link  android.processor.property_cache.test.Custom#getDaysSinceMyBirthday
+     *                   }
+     * @hide
+     */
+    public java.lang.Integer getDaysSinceMyBirthday(
+            IpcDataCache.RemoteCall<java.lang.Void, java.lang.Integer> binderCall) {
+        if (mDaysSinceMyBirthday != null) {
+            return mDaysSinceMyBirthday.query(null);
+        }
+        synchronized (mDaysSinceMyBirthdayLock) {
+            if (mDaysSinceMyBirthday == null) {
+                mDaysSinceMyBirthday = new IpcDataCache(
+                        new IpcDataCache.Config(1, "bluetooth", "my_unique_key",
+                                "DaysSinceMyBirthday"), binderCall);
+
+            }
+        }
+        return mDaysSinceMyBirthday.query(null);
+    }
+
+    /**
+     * This method is auto-generated- invalidate cache for
+     * {@link  android.processor.property_cache.test.Custom#getDaysSinceMyBirthday}
+     *
+     * @hide
+     */
+    public static final void invalidateDaysSinceMyBirthday() {
+        IpcDataCache.invalidateCache("bluetooth", "my_unique_key");
+    }
+
+    private static final Object sBirthdayWishesFromUserLock = new Object();
+    private static IpcDataCache<java.lang.Integer, java.lang.String> sBirthdayWishesFromUser;
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall      - lambda for remote call
+     *                        {@link
+     *                        android.processor.property_cache.test.Custom#getBirthdayWishesFromUser
+     *                        }
+     * @param bypassPredicate - lambda to bypass remote call
+     * @param query           - parameter to call remote lambda
+     * @hide
+     */
+    public static java.lang.String getBirthdayWishesFromUser(
+            IpcDataCache.RemoteCall<java.lang.Integer, java.lang.String> binderCall,
+            IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query) {
+        if (sBirthdayWishesFromUser != null) {
+            return sBirthdayWishesFromUser.query(query);
+        }
+        synchronized (sBirthdayWishesFromUserLock) {
+            if (sBirthdayWishesFromUser == null) {
+                sBirthdayWishesFromUser = new IpcDataCache(
+                        new IpcDataCache.Config(4, "telephony", "custom_birthday_wishes_from_user",
+                                "BirthdayWishesFromUser"), binderCall, bypassPredicate);
+
+            }
+        }
+        return sBirthdayWishesFromUser.query(query);
+    }
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall - lambda for remote call
+     *                   {@link
+     *                   android.processor.property_cache.test.Custom#getBirthdayWishesFromUser }
+     * @param query      - parameter to call remote lambda
+     * @hide
+     */
+    public static java.lang.String getBirthdayWishesFromUser(
+            IpcDataCache.RemoteCall<java.lang.Integer, java.lang.String> binderCall,
+            java.lang.Integer query) {
+        if (sBirthdayWishesFromUser != null) {
+            return sBirthdayWishesFromUser.query(query);
+        }
+        synchronized (sBirthdayWishesFromUserLock) {
+            if (sBirthdayWishesFromUser == null) {
+                sBirthdayWishesFromUser = new IpcDataCache(
+                        new IpcDataCache.Config(4, "telephony", "custom_birthday_wishes_from_user",
+                                "BirthdayWishesFromUser"), binderCall);
+
+            }
+        }
+        return sBirthdayWishesFromUser.query(query);
+    }
+
+    /**
+     * This method is auto-generated- invalidate cache for
+     * {@link  android.processor.property_cache.test.Custom#getBirthdayWishesFromUser}
+     *
+     * @hide
+     */
+    public static final void invalidateBirthdayWishesFromUser() {
+        IpcDataCache.invalidateCache("telephony", "custom_birthday_wishes_from_user");
+    }
+
+
+    /**
+     * This method is auto-generated - initialise all caches for class CustomCache
+     *
+     * @hide
+     */
+    public static void initCache() {
+        CustomCache.invalidateBirthday();
+        CustomCache.invalidateDaysTillBirthday();
+        CustomCache.invalidateDaysSinceBirthday();
+        CustomCache.invalidateDaysTillMyBirthday();
+        CustomCache.invalidateDaysSinceMyBirthday();
+        CustomCache.invalidateBirthdayWishesFromUser();
+    }
+}
diff --git a/tools/processors/property_cache/test/resources/Default.java b/tools/processors/property_cache/test/resources/Default.java
new file mode 100644
index 0000000..d2449aa
--- /dev/null
+++ b/tools/processors/property_cache/test/resources/Default.java
@@ -0,0 +1,125 @@
+/*
+ * 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 android.processor.property_cache.test;
+
+import com.android.internal.annotations.CacheModifier;
+import com.android.internal.annotations.CachedProperty;
+import com.android.internal.annotations.CachedPropertyDefaults;
+
+import java.util.Date;
+
+@CachedPropertyDefaults()
+public class Default {
+    BirthdayManagerService mService = new BirthdayManagerService();
+    Object mCache = new DefaultCache();
+
+    /** Testing default class values to generate static IpcDataCache
+     *
+     * @param userId - user Id
+     * @return birthday date of given user Id
+     */
+    @CachedProperty()
+    public Date getBirthday(int userId) {
+        return DefaultCache.getBirthday(mService::getBirthday, userId);
+    }
+
+    /** Testing default class values to generate static IpcDataCache
+     *
+     * @param userId - user Id
+     * @return number of days till birthday of given user Id
+     */
+    @CachedProperty(modsFlagOnOrNone = {CacheModifier.STATIC})
+    public int getDaysTillBirthday(int userId) {
+        return DefaultCache.getDaysTillBirthday(mService::getDaysTillBirthday, userId);
+    }
+
+    /** Testing generate non-static IpcDataCache
+     *
+     * @param userId - user Id
+     * @return number of days since birthday of given user Id
+     */
+    @CachedProperty(modsFlagOnOrNone = {})
+    public int getDaysSinceBirthday(int userId) {
+        return ((DefaultCache) mCache).getDaysSinceBirthday(mService::getDaysSinceBirthday, userId);
+    }
+
+    /** Testing default class values to generate static IpcDataCache with max capacity of 1
+     *
+     * @return number of days till birthay of current user
+     */
+    @CachedProperty(
+            modsFlagOnOrNone = {CacheModifier.STATIC},
+            max = 1)
+    public int getDaysTillMyBirthday() {
+        return DefaultCache.getDaysTillMyBirthday((Void) -> mService.getDaysTillMyBirthday());
+    }
+
+    /** Testing default class values to generate static IpcDataCache with max capacity of 1 and
+    custom api
+     *
+     * @return number of days since birthay of current user
+     */
+    @CachedProperty(
+            modsFlagOnOrNone = {},
+            max = 1,
+            api = "my_unique_key")
+    public int getDaysSinceMyBirthday() {
+        return ((DefaultCache) mCache).getDaysSinceMyBirthday(
+                (Void) -> mService.getDaysSinceMyBirthday());
+    }
+
+    /** Testing default class values to generate static IpcDataCache with custom module name
+     *
+     * @return birthday wishes of given user Id
+     */
+    @CachedProperty(module = "telephony")
+    public String getBirthdayWishesFromUser(int userId) {
+        return DefaultCache.getBirthdayWishesFromUser(mService::getBirthdayWishesFromUser,
+                userId);
+    }
+
+    class BirthdayManagerService {
+
+        BirthdayManagerService() {
+            DefaultCache.initCache();
+        }
+
+        public Date getBirthday(int userId) {
+            return new Date();
+        }
+
+        public int getDaysTillBirthday(int userId) {
+            return 0;
+        }
+
+        public int getDaysSinceBirthday(int userId) {
+            return 0;
+        }
+
+        public int getDaysTillMyBirthday() {
+            return 0;
+        }
+
+        public int getDaysSinceMyBirthday() {
+            return 0;
+        }
+
+        public String getBirthdayWishesFromUser(int userId) {
+            return "Happy Birthday!\n- " + userId;
+        }
+    }
+}
diff --git a/tools/processors/property_cache/test/resources/DefaultCache.java b/tools/processors/property_cache/test/resources/DefaultCache.java
new file mode 100644
index 0000000..9531118
--- /dev/null
+++ b/tools/processors/property_cache/test/resources/DefaultCache.java
@@ -0,0 +1,402 @@
+/*
+ * 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 android.processor.property_cache.test;
+
+import android.os.IpcDataCache;
+
+/**
+ * This class is auto-generated
+ *
+ * @hide
+ **/
+public class DefaultCache {
+    private static final Object sBirthdayLock = new Object();
+    private static IpcDataCache<java.lang.Integer, java.util.Date> sBirthday;
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall      - lambda for remote call
+     *                        {@link  android.processor.property_cache.test.Default#getBirthday }
+     * @param bypassPredicate - lambda to bypass remote call
+     * @param query           - parameter to call remote lambda
+     * @hide
+     */
+    public static java.util.Date getBirthday(
+            IpcDataCache.RemoteCall<java.lang.Integer, java.util.Date> binderCall,
+            IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query
+    ) {
+        if (sBirthday != null) {
+            return sBirthday.query(query);
+        }
+        synchronized (sBirthdayLock) {
+            if (sBirthday == null) {
+                sBirthday = new IpcDataCache(
+                        new IpcDataCache.Config(32, "system_server",
+                                "default_birthday", "Birthday"),
+                        binderCall, bypassPredicate);
+
+            }
+        }
+        return sBirthday.query(query);
+    }
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall - lambda for remote call
+     *                   {@link  android.processor.property_cache.test.Default#getBirthday }
+     * @param query      - parameter to call remote lambda
+     * @hide
+     */
+    public static java.util.Date getBirthday(
+            IpcDataCache.RemoteCall<java.lang.Integer, java.util.Date> binderCall,
+            java.lang.Integer query
+    ) {
+        if (sBirthday != null) {
+            return sBirthday.query(query);
+        }
+        synchronized (sBirthdayLock) {
+            if (sBirthday == null) {
+                sBirthday = new IpcDataCache(
+                        new IpcDataCache.Config(32, "system_server",
+                                "default_birthday", "Birthday"),
+                        binderCall);
+
+            }
+        }
+        return sBirthday.query(query);
+    }
+
+    /**
+     * This method is auto-generated- invalidate cache for
+     * {@link  android.processor.property_cache.test.Default#getBirthday}
+     *
+     * @hide
+     */
+    public static final void invalidateBirthday() {
+        IpcDataCache.invalidateCache("system_server", "default_birthday");
+    }
+
+    private static final Object sDaysTillBirthdayLock = new Object();
+    private static IpcDataCache<java.lang.Integer, java.lang.Integer> sDaysTillBirthday;
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall      - lambda for remote call
+     *                        {@link
+     *                        android.processor.property_cache.test.Default#getDaysTillBirthday }
+     * @param bypassPredicate - lambda to bypass remote call
+     * @param query           - parameter to call remote lambda
+     * @hide
+     */
+    public static java.lang.Integer getDaysTillBirthday(
+            IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall,
+            IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query
+    ) {
+        if (sDaysTillBirthday != null) {
+            return sDaysTillBirthday.query(query);
+        }
+        synchronized (sDaysTillBirthdayLock) {
+            if (sDaysTillBirthday == null) {
+                sDaysTillBirthday = new IpcDataCache(
+                        new IpcDataCache.Config(32, "system_server", "default_days_till_birthday",
+                                "DaysTillBirthday"), binderCall, bypassPredicate);
+
+            }
+        }
+        return sDaysTillBirthday.query(query);
+    }
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall - lambda for remote call
+     *                   {@link  android.processor.property_cache.test.Default#getDaysTillBirthday
+     *                   }
+     * @param query      - parameter to call remote lambda
+     * @hide
+     */
+    public static java.lang.Integer getDaysTillBirthday(
+            IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall,
+            java.lang.Integer query
+    ) {
+        if (sDaysTillBirthday != null) {
+            return sDaysTillBirthday.query(query);
+        }
+        synchronized (sDaysTillBirthdayLock) {
+            if (sDaysTillBirthday == null) {
+                sDaysTillBirthday = new IpcDataCache(
+                        new IpcDataCache.Config(32, "system_server", "default_days_till_birthday",
+                                "DaysTillBirthday"), binderCall);
+
+            }
+        }
+        return sDaysTillBirthday.query(query);
+    }
+
+    /**
+     * This method is auto-generated- invalidate cache for
+     * {@link  android.processor.property_cache.test.Default#getDaysTillBirthday}
+     *
+     * @hide
+     */
+    public static final void invalidateDaysTillBirthday() {
+        IpcDataCache.invalidateCache("system_server", "default_days_till_birthday");
+    }
+
+    private final Object mDaysSinceBirthdayLock = new Object();
+    private IpcDataCache<java.lang.Integer, java.lang.Integer> mDaysSinceBirthday;
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall      - lambda for remote call
+     *                        {@link
+     *                        android.processor.property_cache.test.Default#getDaysSinceBirthday }
+     * @param bypassPredicate - lambda to bypass remote call
+     * @param query           - parameter to call remote lambda
+     * @hide
+     */
+    public java.lang.Integer getDaysSinceBirthday(
+            IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall,
+            IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query
+    ) {
+        if (mDaysSinceBirthday != null) {
+            return mDaysSinceBirthday.query(query);
+        }
+        synchronized (mDaysSinceBirthdayLock) {
+            if (mDaysSinceBirthday == null) {
+                mDaysSinceBirthday = new IpcDataCache(
+                        new IpcDataCache.Config(32, "system_server", "default_days_since_birthday",
+                                "DaysSinceBirthday"), binderCall, bypassPredicate);
+
+            }
+        }
+        return mDaysSinceBirthday.query(query);
+    }
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall - lambda for remote call
+     *                   {@link  android.processor.property_cache.test.Default#getDaysSinceBirthday
+     *                   }
+     * @param query      - parameter to call remote lambda
+     * @hide
+     */
+    public java.lang.Integer getDaysSinceBirthday(
+            IpcDataCache.RemoteCall<java.lang.Integer, java.lang.Integer> binderCall,
+            java.lang.Integer query
+    ) {
+        if (mDaysSinceBirthday != null) {
+            return mDaysSinceBirthday.query(query);
+        }
+        synchronized (mDaysSinceBirthdayLock) {
+            if (mDaysSinceBirthday == null) {
+                mDaysSinceBirthday = new IpcDataCache(
+                        new IpcDataCache.Config(32, "system_server", "default_days_since_birthday",
+                                "DaysSinceBirthday"), binderCall);
+
+            }
+        }
+        return mDaysSinceBirthday.query(query);
+    }
+
+    /**
+     * This method is auto-generated- invalidate cache for
+     * {@link  android.processor.property_cache.test.Default#getDaysSinceBirthday}
+     *
+     * @hide
+     */
+    public static final void invalidateDaysSinceBirthday() {
+        IpcDataCache.invalidateCache("system_server", "default_days_since_birthday");
+    }
+
+    private static final Object sDaysTillMyBirthdayLock = new Object();
+    private static IpcDataCache<java.lang.Void, java.lang.Integer> sDaysTillMyBirthday;
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall - lambda for remote call
+     *                   {@link  android.processor.property_cache.test.Default#getDaysTillMyBirthday
+     *                   }
+     * @hide
+     */
+    public static java.lang.Integer getDaysTillMyBirthday(
+            IpcDataCache.RemoteCall<java.lang.Void, java.lang.Integer> binderCall
+    ) {
+        if (sDaysTillMyBirthday != null) {
+            return sDaysTillMyBirthday.query(null);
+        }
+        synchronized (sDaysTillMyBirthdayLock) {
+            if (sDaysTillMyBirthday == null) {
+                sDaysTillMyBirthday = new IpcDataCache(
+                        new IpcDataCache.Config(1, "system_server", "default_days_till_my_birthday",
+                                "DaysTillMyBirthday"), binderCall);
+
+            }
+        }
+        return sDaysTillMyBirthday.query(null);
+    }
+
+    /**
+     * This method is auto-generated- invalidate cache for
+     * {@link  android.processor.property_cache.test.Default#getDaysTillMyBirthday}
+     *
+     * @hide
+     */
+    public static final void invalidateDaysTillMyBirthday() {
+        IpcDataCache.invalidateCache("system_server", "default_days_till_my_birthday");
+    }
+
+    private final Object mDaysSinceMyBirthdayLock = new Object();
+    private IpcDataCache<java.lang.Void, java.lang.Integer> mDaysSinceMyBirthday;
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall - lambda for remote call
+     *                   {@link
+     *                   android.processor.property_cache.test.Default#getDaysSinceMyBirthday }
+     * @hide
+     */
+    public java.lang.Integer getDaysSinceMyBirthday(
+            IpcDataCache.RemoteCall<java.lang.Void, java.lang.Integer> binderCall
+    ) {
+        if (mDaysSinceMyBirthday != null) {
+            return mDaysSinceMyBirthday.query(null);
+        }
+        synchronized (mDaysSinceMyBirthdayLock) {
+            if (mDaysSinceMyBirthday == null) {
+                mDaysSinceMyBirthday = new IpcDataCache(
+                        new IpcDataCache.Config(1, "system_server", "my_unique_key",
+                                "DaysSinceMyBirthday"), binderCall);
+
+            }
+        }
+        return mDaysSinceMyBirthday.query(null);
+    }
+
+    /**
+     * This method is auto-generated- invalidate cache for
+     * {@link  android.processor.property_cache.test.Default#getDaysSinceMyBirthday}
+     *
+     * @hide
+     */
+    public static final void invalidateDaysSinceMyBirthday() {
+        IpcDataCache.invalidateCache("system_server", "my_unique_key");
+    }
+
+    private static final Object sBirthdayWishesFromUserLock = new Object();
+    private static IpcDataCache<java.lang.Integer, java.lang.String> sBirthdayWishesFromUser;
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall      - lambda for remote call
+     *                        {@link
+     *
+     *                       android.processor.property_cache.test.Default#getBirthdayWishesFromUser
+     *                        }
+     * @param bypassPredicate - lambda to bypass remote call
+     * @param query           - parameter to call remote lambda
+     * @hide
+     */
+    public static java.lang.String getBirthdayWishesFromUser(
+            IpcDataCache.RemoteCall<java.lang.Integer, java.lang.String> binderCall,
+            IpcDataCache.BypassCall<java.lang.Integer> bypassPredicate, java.lang.Integer query
+    ) {
+        if (sBirthdayWishesFromUser != null) {
+            return sBirthdayWishesFromUser.query(query);
+        }
+        synchronized (sBirthdayWishesFromUserLock) {
+            if (sBirthdayWishesFromUser == null) {
+                sBirthdayWishesFromUser = new IpcDataCache(
+                        new IpcDataCache.Config(32, "telephony",
+                                "default_birthday_wishes_from_user",
+                                "BirthdayWishesFromUser"), binderCall, bypassPredicate);
+
+            }
+        }
+        return sBirthdayWishesFromUser.query(query);
+    }
+
+
+    /**
+     * This method is auto-generated
+     *
+     * @param binderCall - lambda for remote call
+     *                   {@link
+     *                   android.processor.property_cache.test.Default#getBirthdayWishesFromUser }
+     * @param query      - parameter to call remote lambda
+     * @hide
+     */
+    public static java.lang.String getBirthdayWishesFromUser(
+            IpcDataCache.RemoteCall<java.lang.Integer, java.lang.String> binderCall,
+            java.lang.Integer query
+    ) {
+        if (sBirthdayWishesFromUser != null) {
+            return sBirthdayWishesFromUser.query(query);
+        }
+        synchronized (sBirthdayWishesFromUserLock) {
+            if (sBirthdayWishesFromUser == null) {
+                sBirthdayWishesFromUser = new IpcDataCache(
+                        new IpcDataCache.Config(32, "telephony",
+                                "default_birthday_wishes_from_user",
+                                "BirthdayWishesFromUser"), binderCall);
+
+            }
+        }
+        return sBirthdayWishesFromUser.query(query);
+    }
+
+    /**
+     * This method is auto-generated- invalidate cache for
+     * {@link  android.processor.property_cache.test.Default#getBirthdayWishesFromUser}
+     *
+     * @hide
+     */
+    public static final void invalidateBirthdayWishesFromUser() {
+        IpcDataCache.invalidateCache("telephony", "default_birthday_wishes_from_user");
+    }
+
+
+    /**
+     * This method is auto-generated - initialise all caches for class DefaultCache
+     *
+     * @hide
+     */
+    public static void initCache() {
+        DefaultCache.invalidateBirthday();
+        DefaultCache.invalidateDaysTillBirthday();
+        DefaultCache.invalidateDaysSinceBirthday();
+        DefaultCache.invalidateDaysTillMyBirthday();
+        DefaultCache.invalidateDaysSinceMyBirthday();
+        DefaultCache.invalidateBirthdayWishesFromUser();
+    }
+}