Merge "Add a switch for update ownership enforcement"
diff --git a/core/api/current.txt b/core/api/current.txt
index 945531f..b63b0aa 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -24224,6 +24224,7 @@
     method public int getDisableReason();
     method public int getFlags();
     method @NonNull public String getRouteId();
+    method public int getSessionParticipantCount();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.media.RouteListingPreference.Item> CREATOR;
     field public static final int DISABLE_REASON_AD = 3; // 0x3
@@ -24239,6 +24240,7 @@
     method @NonNull public android.media.RouteListingPreference.Item build();
     method @NonNull public android.media.RouteListingPreference.Item.Builder setDisableReason(int);
     method @NonNull public android.media.RouteListingPreference.Item.Builder setFlags(int);
+    method @NonNull public android.media.RouteListingPreference.Item.Builder setSessionParticipantCount(@IntRange(from=0) int);
   }
 
   public final class RoutingSessionInfo implements android.os.Parcelable {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index c09c6c5..ef074d8 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -4902,17 +4902,15 @@
 
   public final class VirtualTouchscreenConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
     method public int describeContents();
-    method public int getHeightInPixels();
-    method public int getWidthInPixels();
+    method public int getHeight();
+    method public int getWidth();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualTouchscreenConfig> CREATOR;
   }
 
   public static final class VirtualTouchscreenConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualTouchscreenConfig.Builder> {
-    ctor public VirtualTouchscreenConfig.Builder();
+    ctor public VirtualTouchscreenConfig.Builder(@IntRange(from=1) int, @IntRange(from=1) int);
     method @NonNull public android.hardware.input.VirtualTouchscreenConfig build();
-    method @NonNull public android.hardware.input.VirtualTouchscreenConfig.Builder setHeightInPixels(int);
-    method @NonNull public android.hardware.input.VirtualTouchscreenConfig.Builder setWidthInPixels(int);
   }
 
 }
@@ -6606,6 +6604,7 @@
   public class AudioManager {
     method @Deprecated public int abandonAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addAssistantServicesUids(@NonNull int[]);
+    method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnNonDefaultDevicesForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnNonDefaultDevicesForStrategyChangedListener) throws java.lang.SecurityException;
     method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDeviceForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener) throws java.lang.SecurityException;
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDevicesForCapturePresetChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDevicesForCapturePresetChangedListener) throws java.lang.SecurityException;
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDevicesForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDevicesForStrategyChangedListener) throws java.lang.SecurityException;
@@ -6627,6 +6626,7 @@
     method @IntRange(from=0) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getMaxVolumeIndexForAttributes(@NonNull android.media.AudioAttributes);
     method @IntRange(from=0) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getMinVolumeIndexForAttributes(@NonNull android.media.AudioAttributes);
     method @Nullable @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public android.media.AudioDeviceAttributes getMutingExpectedDevice();
+    method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioDeviceAttributes> getNonDefaultDevicesForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy);
     method @Nullable @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public android.media.AudioDeviceAttributes getPreferredDeviceForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy);
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioDeviceAttributes> getPreferredDevicesForCapturePreset(int);
     method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioDeviceAttributes> getPreferredDevicesForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy);
@@ -6642,6 +6642,8 @@
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void registerMuteAwaitConnectionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.MuteAwaitConnectionCallback);
     method public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeAssistantServicesUids(@NonNull int[]);
+    method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean removeDeviceAsNonDefaultForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull android.media.AudioDeviceAttributes);
+    method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnNonDefaultDevicesForStrategyChangedListener(@NonNull android.media.AudioManager.OnNonDefaultDevicesForStrategyChangedListener);
     method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDeviceForStrategyChangedListener(@NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDevicesForCapturePresetChangedListener(@NonNull android.media.AudioManager.OnPreferredDevicesForCapturePresetChangedListener);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDevicesForStrategyChangedListener(@NonNull android.media.AudioManager.OnPreferredDevicesForStrategyChangedListener);
@@ -6653,6 +6655,7 @@
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean setAdditionalOutputDeviceDelay(@NonNull android.media.AudioDeviceInfo, @IntRange(from=0) long);
     method public void setAudioServerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.AudioServerStateCallback);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setBluetoothVariableLatencyEnabled(boolean);
+    method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean setDeviceAsNonDefaultForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull android.media.AudioDeviceAttributes);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setDeviceVolumeBehavior(@NonNull android.media.AudioDeviceAttributes, int);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setFocusRequestResult(@NonNull android.media.AudioFocusInfo, int, @NonNull android.media.audiopolicy.AudioPolicy);
     method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean setPreferredDeviceForCapturePreset(int, @NonNull android.media.AudioDeviceAttributes);
@@ -6697,6 +6700,10 @@
     field public static final int EVENT_TIMEOUT = 2; // 0x2
   }
 
+  public static interface AudioManager.OnNonDefaultDevicesForStrategyChangedListener {
+    method public void onNonDefaultDevicesForStrategyChanged(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull java.util.List<android.media.AudioDeviceAttributes>);
+  }
+
   @Deprecated public static interface AudioManager.OnPreferredDeviceForStrategyChangedListener {
     method @Deprecated public void onPreferredDeviceForStrategyChanged(@NonNull android.media.audiopolicy.AudioProductStrategy, @Nullable android.media.AudioDeviceAttributes);
   }
@@ -7781,6 +7788,7 @@
     field public static final int STATUS_DATA_READY = 1; // 0x1
     field public static final int STATUS_HIGH_WATER = 4; // 0x4
     field public static final int STATUS_LOW_WATER = 2; // 0x2
+    field public static final int STATUS_NO_DATA = 16; // 0x10
     field public static final int STATUS_OVERFLOW = 8; // 0x8
     field public static final int SUBTYPE_AUDIO = 3; // 0x3
     field public static final int SUBTYPE_DOWNLOAD = 5; // 0x5
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 22ea9f20..9ab7cf9 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -60,62 +60,73 @@
     /**
      * Closes the virtual device and frees all associated resources.
      */
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void close();
 
     /**
      * Notifies of an audio session being started.
      */
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void onAudioSessionStarting(
             int displayId,
             IAudioRoutingCallback routingCallback,
             IAudioConfigChangedCallback configChangedCallback);
 
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void onAudioSessionEnded();
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void createVirtualDpad(
             in VirtualDpadConfig config,
             IBinder token);
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void createVirtualKeyboard(
             in VirtualKeyboardConfig config,
             IBinder token);
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void createVirtualMouse(
             in VirtualMouseConfig config,
             IBinder token);
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void createVirtualTouchscreen(
             in VirtualTouchscreenConfig config,
             IBinder token);
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void createVirtualNavigationTouchpad(
             in VirtualNavigationTouchpadConfig config,
             IBinder token);
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void unregisterInputDevice(IBinder token);
     int getInputDeviceId(IBinder token);
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event);
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendKeyEvent(IBinder token, in VirtualKeyEvent event);
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendButtonEvent(IBinder token, in VirtualMouseButtonEvent event);
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendRelativeEvent(IBinder token, in VirtualMouseRelativeEvent event);
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendScrollEvent(IBinder token, in VirtualMouseScrollEvent event);
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendTouchEvent(IBinder token, in VirtualTouchEvent event);
 
     /**
      * Creates a virtual sensor, capable of injecting sensor events into the system.
      */
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void createVirtualSensor(IBinder tokenm, in VirtualSensorConfig config);
 
     /**
      * Removes the sensor corresponding to the given token from the system.
      */
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void unregisterSensor(IBinder token);
 
     /**
      * Sends an event to the virtual sensor corresponding to the given token.
      */
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     boolean sendSensorEvent(IBinder token, in VirtualSensorEvent event);
 
     /**
@@ -126,6 +137,7 @@
     PointF getCursorPosition(IBinder token);
 
     /** Sets whether to show or hide the cursor while this virtual device is active. */
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void setShowPointerIcon(boolean showPointerIcon);
 
     /**
@@ -133,9 +145,9 @@
      * when matching the provided IntentFilter and calls the callback with the intercepted
      * intent.
      */
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void registerIntentInterceptor(
             in IVirtualDeviceIntentInterceptor intentInterceptor, in IntentFilter filter);
-    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)")
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
     void unregisterIntentInterceptor(in IVirtualDeviceIntentInterceptor intentInterceptor);
 }
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 8561018..adf59fb 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -775,13 +775,11 @@
             final Point size = new Point();
             display.getDisplay().getSize(size);
             VirtualTouchscreenConfig touchscreenConfig =
-                    new VirtualTouchscreenConfig.Builder()
+                    new VirtualTouchscreenConfig.Builder(size.x, size.y)
                             .setVendorId(vendorId)
                             .setProductId(productId)
                             .setInputDeviceName(inputDeviceName)
                             .setAssociatedDisplayId(display.getDisplay().getDisplayId())
-                            .setWidthInPixels(size.x)
-                            .setHeightInPixels(size.y)
                             .build();
             return createVirtualTouchscreen(touchscreenConfig);
         }
diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
index f2805bb..5ad5fd9 100644
--- a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
+++ b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java
@@ -88,12 +88,17 @@
      * Builder for creating a {@link VirtualNavigationTouchpadConfig}.
      */
     public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> {
-
         private final int mHeight;
         private final int mWidth;
 
-        public Builder(@IntRange(from = 1) int touchpadHeight,
-                @IntRange(from = 1) int touchpadWidth) {
+        /**
+         * Creates a new instance for the given dimensions of the {@link VirtualNavigationTouchpad}.
+         *
+         * @param touchpadWidth The width of the touchpad.
+         * @param touchpadHeight The height of the touchpad.
+         */
+        public Builder(@IntRange(from = 1) int touchpadWidth,
+                @IntRange(from = 1) int touchpadHeight) {
             if (touchpadHeight <= 0 || touchpadWidth <= 0) {
                 throw new IllegalArgumentException(
                         "Cannot create a virtual navigation touchpad, touchpad dimensions must be "
diff --git a/core/java/android/hardware/input/VirtualTouchscreenConfig.java b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
index e358619..aac341cc 100644
--- a/core/java/android/hardware/input/VirtualTouchscreenConfig.java
+++ b/core/java/android/hardware/input/VirtualTouchscreenConfig.java
@@ -16,6 +16,7 @@
 
 package android.hardware.input;
 
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.os.Parcel;
@@ -29,31 +30,31 @@
 @SystemApi
 public final class VirtualTouchscreenConfig extends VirtualInputDeviceConfig implements Parcelable {
 
-    /** The touchscreen width in pixels. */
-    private final int mWidthInPixels;
-    /** The touchscreen height in pixels. */
-    private final int mHeightInPixels;
+    /** The touchscreen width. */
+    private final int mWidth;
+    /** The touchscreen height. */
+    private final int mHeight;
 
     private VirtualTouchscreenConfig(@NonNull Builder builder) {
         super(builder);
-        mWidthInPixels = builder.mWidthInPixels;
-        mHeightInPixels = builder.mHeightInPixels;
+        mWidth = builder.mWidth;
+        mHeight = builder.mHeight;
     }
 
     private VirtualTouchscreenConfig(@NonNull Parcel in) {
         super(in);
-        mWidthInPixels = in.readInt();
-        mHeightInPixels = in.readInt();
+        mWidth = in.readInt();
+        mHeight = in.readInt();
     }
 
-    /** Returns the touchscreen width in pixels. */
-    public int getWidthInPixels() {
-        return mWidthInPixels;
+    /** Returns the touchscreen width. */
+    public int getWidth() {
+        return mWidth;
     }
 
-    /** Returns the touchscreen height in pixels. */
-    public int getHeightInPixels() {
-        return mHeightInPixels;
+    /** Returns the touchscreen height. */
+    public int getHeight() {
+        return mHeight;
     }
 
     @Override
@@ -64,8 +65,8 @@
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         super.writeToParcel(dest, flags);
-        dest.writeInt(mWidthInPixels);
-        dest.writeInt(mHeightInPixels);
+        dest.writeInt(mWidth);
+        dest.writeInt(mHeight);
     }
 
     @NonNull
@@ -86,25 +87,29 @@
      * Builder for creating a {@link VirtualTouchscreenConfig}.
      */
     public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> {
-        private int mWidthInPixels;
-        private int mHeightInPixels;
+        private int mWidth;
+        private int mHeight;
 
         /**
-         * @see VirtualTouchscreenConfig#getWidthInPixels().
+         * Creates a new instance for the given dimensions of the {@link VirtualTouchscreen}.
+         *
+         * <p>The dimensions are not pixels but in the touchscreens raw coordinate space. They do
+         * not necessarily have to correspond to the display size or aspect ratio. In this case the
+         * framework will handle the scaling appropriately.
+         *
+         * @param touchscreenWidth The width of the touchscreen.
+         * @param touchscreenHeight The height of the touchscreen.
          */
-        @NonNull
-        public Builder setWidthInPixels(int widthInPixels) {
-            mWidthInPixels = widthInPixels;
-            return this;
-        }
-
-        /**
-         * @see VirtualTouchscreenConfig#getHeightInPixels().
-         */
-        @NonNull
-        public Builder setHeightInPixels(int heightInPixels) {
-            mHeightInPixels = heightInPixels;
-            return this;
+        public Builder(@IntRange(from = 1) int touchscreenWidth,
+                @IntRange(from = 1) int touchscreenHeight) {
+            if (touchscreenHeight <= 0 || touchscreenWidth <= 0) {
+                throw new IllegalArgumentException(
+                        "Cannot create a virtual touchscreen, touchscreen dimensions must be "
+                                + "positive. Got: (" + touchscreenHeight + ", "
+                                + touchscreenWidth + ")");
+            }
+            mHeight = touchscreenHeight;
+            mWidth = touchscreenWidth;
         }
 
         /**
diff --git a/core/java/com/android/internal/app/AppLocaleCollector.java b/core/java/com/android/internal/app/AppLocaleCollector.java
index 65e8c64..a50fbb8 100644
--- a/core/java/com/android/internal/app/AppLocaleCollector.java
+++ b/core/java/com/android/internal/app/AppLocaleCollector.java
@@ -19,49 +19,184 @@
 import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET;
 import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG;
 
+import android.app.LocaleManager;
 import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
 import android.os.Build;
 import android.os.LocaleList;
+import android.os.SystemProperties;
+import android.provider.Settings;
 import android.util.Log;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
 
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Locale;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /** The Locale data collector for per-app language. */
-class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
+public class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase {
     private static final String TAG = AppLocaleCollector.class.getSimpleName();
     private final Context mContext;
     private final String mAppPackageName;
-    private final LocaleStore.LocaleInfo mAppCurrentLocale;
+    private LocaleStore.LocaleInfo mAppCurrentLocale;
+    private Set<LocaleStore.LocaleInfo> mAllAppActiveLocales;
+    private Set<LocaleStore.LocaleInfo> mImeLocales;
+    private static final String PROP_APP_LANGUAGE_SUGGESTION =
+            "android.app.language.suggestion.enhanced";
+    private static final boolean ENABLED = true;
 
-    AppLocaleCollector(Context context, String appPackageName) {
+    public AppLocaleCollector(Context context, String appPackageName) {
         mContext = context;
         mAppPackageName = appPackageName;
-        mAppCurrentLocale = LocaleStore.getAppCurrentLocaleInfo(
-                mContext, mAppPackageName);
+    }
+
+    @VisibleForTesting
+    public LocaleStore.LocaleInfo getAppCurrentLocale() {
+        return LocaleStore.getAppActivatedLocaleInfo(mContext, mAppPackageName, true);
+    }
+
+    /**
+     * Get all applications' activated locales.
+     * @return A set which includes all applications' activated LocaleInfo.
+     */
+    @VisibleForTesting
+    public Set<LocaleStore.LocaleInfo> getAllAppActiveLocales() {
+        PackageManager pm = mContext.getPackageManager();
+        LocaleManager lm = mContext.getSystemService(LocaleManager.class);
+        HashSet<LocaleStore.LocaleInfo> result = new HashSet<>();
+        if (pm != null && lm != null) {
+            HashMap<String, LocaleStore.LocaleInfo> map = new HashMap<>();
+            for (ApplicationInfo appInfo : pm.getInstalledApplications(
+                    PackageManager.ApplicationInfoFlags.of(0))) {
+                LocaleStore.LocaleInfo localeInfo = LocaleStore.getAppActivatedLocaleInfo(
+                        mContext, appInfo.packageName, false);
+                // For the locale to be added into the suggestion area, its country could not be
+                // empty.
+                if (localeInfo != null && localeInfo.getLocale().getCountry().length() > 0) {
+                    map.put(localeInfo.getId(), localeInfo);
+                }
+            }
+            map.forEach((language, localeInfo) -> result.add(localeInfo));
+        }
+        return result;
+    }
+
+    /**
+     * Get all locales that active IME supports.
+     *
+     * @return A set which includes all LocaleInfo that active IME supports.
+     */
+    @VisibleForTesting
+    public Set<LocaleStore.LocaleInfo> getActiveImeLocales() {
+        Set<LocaleStore.LocaleInfo> activeImeLocales = null;
+        InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
+        if (imm != null) {
+            InputMethodInfo activeIme = getActiveIme(imm);
+            if (activeIme != null) {
+                activeImeLocales = LocaleStore.transformImeLanguageTagToLocaleInfo(
+                        imm.getEnabledInputMethodSubtypeList(activeIme, true));
+            }
+        }
+        if (activeImeLocales == null) {
+            return Set.of();
+        } else {
+            return activeImeLocales.stream().filter(
+                    // For the locale to be added into the suggestion area, its country could not be
+                    // empty.
+                    info -> info.getLocale().getCountry().length() > 0).collect(
+                    Collectors.toSet());
+        }
+    }
+
+    private InputMethodInfo getActiveIme(InputMethodManager imm) {
+        InputMethodInfo activeIme = null;
+        List<InputMethodInfo> infoList = imm.getEnabledInputMethodList();
+        String imeId = Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                Settings.Secure.DEFAULT_INPUT_METHOD, mContext.getUserId());
+        for (InputMethodInfo method : infoList) {
+            if (method.getId().equals(imeId)) {
+                activeIme = method;
+            }
+        }
+        return activeIme;
+    }
+
+    /**
+     * Get the AppLocaleResult that the application supports.
+     * @return The AppLocaleResult that the application supports.
+     */
+    @VisibleForTesting
+    public AppLocaleStore.AppLocaleResult getAppSupportedLocales() {
+        return AppLocaleStore.getAppSupportedLocales(mContext, mAppPackageName);
+    }
+
+    /**
+     * Get the locales that system supports excluding langTagsToIgnore.
+     *
+     * @param langTagsToIgnore A language set to be ignored.
+     * @param parent The parent locale.
+     * @param translatedOnly specified if is is only for translation.
+     * @return A set which includes the LocaleInfo that system supports, excluding langTagsToIgnore.
+     */
+    @VisibleForTesting
+    public Set<LocaleStore.LocaleInfo> getSystemSupportedLocale(Set<String> langTagsToIgnore,
+            LocaleStore.LocaleInfo parent, boolean translatedOnly) {
+        return LocaleStore.getLevelLocales(mContext, langTagsToIgnore, parent, translatedOnly);
+    }
+
+    /**
+     * Get the locales that system activates.
+     * @return A set which includes all the locales that system activates.
+     */
+    @VisibleForTesting
+    public List<LocaleStore.LocaleInfo> getSystemCurrentLocale() {
+        return LocaleStore.getSystemCurrentLocaleInfo();
     }
 
     @Override
     public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) {
         HashSet<String> langTagsToIgnore = new HashSet<>();
 
-        LocaleList systemLangList = LocaleList.getDefault();
-        for(int i = 0; i < systemLangList.size(); i++) {
-            langTagsToIgnore.add(systemLangList.get(i).toLanguageTag());
-        }
-
         if (mAppCurrentLocale != null) {
             langTagsToIgnore.add(mAppCurrentLocale.getLocale().toLanguageTag());
         }
+
+        if (SystemProperties.getBoolean(PROP_APP_LANGUAGE_SUGGESTION, ENABLED)) {
+            // Add the locale that other App activated
+            mAllAppActiveLocales.forEach(
+                    info -> langTagsToIgnore.add(info.getLocale().toLanguageTag()));
+            // Add the locale that active IME enabled
+            mImeLocales.forEach(info -> langTagsToIgnore.add(info.getLocale().toLanguageTag()));
+        }
+
+        // Add System locales
+        LocaleList systemLangList = LocaleList.getDefault();
+        for (int i = 0; i < systemLangList.size(); i++) {
+            langTagsToIgnore.add(systemLangList.get(i).toLanguageTag());
+        }
         return langTagsToIgnore;
     }
 
     @Override
     public Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent,
             boolean translatedOnly, boolean isForCountryMode) {
-        AppLocaleStore.AppLocaleResult result =
-                AppLocaleStore.getAppSupportedLocales(mContext, mAppPackageName);
+        if (mAppCurrentLocale == null) {
+            mAppCurrentLocale = getAppCurrentLocale();
+        }
+        if (mAllAppActiveLocales == null) {
+            mAllAppActiveLocales = getAllAppActiveLocales();
+        }
+        if (mImeLocales == null) {
+            mImeLocales = getActiveImeLocales();
+        }
+        AppLocaleStore.AppLocaleResult result = getAppSupportedLocales();
         Set<String> langTagsToIgnore = getIgnoredLocaleList(translatedOnly);
         Set<LocaleStore.LocaleInfo> appLocaleList = new HashSet<>();
         Set<LocaleStore.LocaleInfo> systemLocaleList;
@@ -71,11 +206,9 @@
 
         // Get system supported locale list
         if (isForCountryMode) {
-            systemLocaleList = LocaleStore.getLevelLocales(mContext,
-                    langTagsToIgnore, parent, translatedOnly);
+            systemLocaleList = getSystemSupportedLocale(langTagsToIgnore, parent, translatedOnly);
         } else {
-            systemLocaleList = LocaleStore.getLevelLocales(mContext, langTagsToIgnore,
-                    null /* no parent */, translatedOnly);
+            systemLocaleList = getSystemSupportedLocale(langTagsToIgnore, null, translatedOnly);
         }
 
         // Add current app locale
@@ -84,19 +217,46 @@
         }
 
         // Add current system language into suggestion list
-        for(LocaleStore.LocaleInfo localeInfo:
-                LocaleStore.getSystemCurrentLocaleInfo()) {
-            boolean isNotCurrentLocale = mAppCurrentLocale == null
-                    || !localeInfo.getLocale().equals(mAppCurrentLocale.getLocale());
-            if (!isForCountryMode && isNotCurrentLocale) {
-                appLocaleList.add(localeInfo);
+        if (!isForCountryMode) {
+            boolean isCurrentLocale, isInAppOrIme;
+            for (LocaleStore.LocaleInfo localeInfo : getSystemCurrentLocale()) {
+                isCurrentLocale = mAppCurrentLocale != null
+                        && localeInfo.getLocale().equals(mAppCurrentLocale.getLocale());
+                isInAppOrIme = existsInAppOrIme(localeInfo.getLocale());
+                if (!isCurrentLocale && !isInAppOrIme) {
+                    appLocaleList.add(localeInfo);
+                }
             }
         }
 
-        // Add the languages that included in system supported locale
+        // Add the languages that are included in system supported locale
+        Set<LocaleStore.LocaleInfo> suggestedSet = null;
         if (shouldShowList) {
-            appLocaleList.addAll(filterTheLanguagesNotIncludedInSystemLocale(
-                    systemLocaleList, result.mAppSupportedLocales));
+            appLocaleList.addAll(filterSupportedLocales(systemLocaleList,
+                    result.mAppSupportedLocales));
+            suggestedSet = getSuggestedLocales(appLocaleList);
+        }
+
+        if (!isForCountryMode && SystemProperties.getBoolean(PROP_APP_LANGUAGE_SUGGESTION,
+                ENABLED)) {
+            // Add the language that other apps activate into the suggestion list.
+            Set<LocaleStore.LocaleInfo> localeSet = filterSupportedLocales(mAllAppActiveLocales,
+                    result.mAppSupportedLocales);
+            if (suggestedSet != null) {
+                // Filter out the locale with the same language and country
+                // like zh-TW vs zh-Hant-TW.
+                localeSet = filterSameLanguageAndCountry(localeSet, suggestedSet);
+            }
+            appLocaleList.addAll(localeSet);
+            suggestedSet.addAll(localeSet);
+
+            // Add the language that the active IME enables into the suggestion list.
+            localeSet = filterSupportedLocales(mImeLocales, result.mAppSupportedLocales);
+            if (suggestedSet != null) {
+                localeSet = filterSameLanguageAndCountry(localeSet, suggestedSet);
+            }
+            appLocaleList.addAll(localeSet);
+            suggestedSet.addAll(localeSet);
         }
 
         // Add "system language" option
@@ -117,17 +277,55 @@
         return true;
     }
 
-    private Set<LocaleStore.LocaleInfo> filterTheLanguagesNotIncludedInSystemLocale(
-            Set<LocaleStore.LocaleInfo> systemLocaleList,
+    private Set<LocaleStore.LocaleInfo> getSuggestedLocales(Set<LocaleStore.LocaleInfo> localeSet) {
+        return localeSet.stream().filter(localeInfo -> localeInfo.isSuggested()).collect(
+                Collectors.toSet());
+    }
+
+    private Set<LocaleStore.LocaleInfo> filterSameLanguageAndCountry(
+            Set<LocaleStore.LocaleInfo> newLocaleList,
+            Set<LocaleStore.LocaleInfo> existingLocaleList) {
+        Set<LocaleStore.LocaleInfo> result = new HashSet<>(newLocaleList.size());
+        for (LocaleStore.LocaleInfo appLocaleInfo : newLocaleList) {
+            boolean same = false;
+            Locale appLocale = appLocaleInfo.getLocale();
+            for (LocaleStore.LocaleInfo localeInfo : existingLocaleList) {
+                Locale suggested = localeInfo.getLocale();
+                if (appLocale.getLanguage().equals(suggested.getLanguage())
+                        && appLocale.getCountry().equals(suggested.getCountry())) {
+                    same = true;
+                    break;
+                }
+            }
+            if (!same) {
+                result.add(appLocaleInfo);
+            }
+        }
+        return result;
+    }
+
+    private boolean existsInAppOrIme(Locale locale) {
+        boolean existInApp = mAllAppActiveLocales.stream().anyMatch(
+                localeInfo -> localeInfo.getLocale().equals(locale));
+        if (existInApp) {
+            return true;
+        } else {
+            return mImeLocales.stream().anyMatch(
+                    localeInfo -> localeInfo.getLocale().equals(locale));
+        }
+    }
+
+    private Set<LocaleStore.LocaleInfo> filterSupportedLocales(
+            Set<LocaleStore.LocaleInfo> suggestedLocales,
             HashSet<Locale> appSupportedLocales) {
         Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>();
 
-        for(LocaleStore.LocaleInfo li: systemLocaleList) {
+        for (LocaleStore.LocaleInfo li : suggestedLocales) {
             if (appSupportedLocales.contains(li.getLocale())) {
                 filteredList.add(li);
             } else {
-                for(Locale l: appSupportedLocales) {
-                    if(LocaleList.matchesLanguageAndScript(li.getLocale(), l)) {
+                for (Locale l : appSupportedLocales) {
+                    if (LocaleList.matchesLanguageAndScript(li.getLocale(), l)) {
                         filteredList.add(li);
                         break;
                     }
diff --git a/core/java/com/android/internal/app/AppLocaleStore.java b/core/java/com/android/internal/app/AppLocaleStore.java
index f3a322c..a450a05 100644
--- a/core/java/com/android/internal/app/AppLocaleStore.java
+++ b/core/java/com/android/internal/app/AppLocaleStore.java
@@ -30,7 +30,10 @@
 import java.util.Locale;
 import java.util.stream.Collectors;
 
-class AppLocaleStore {
+/**
+ * A class used to access an application's supporting locales.
+ */
+public class AppLocaleStore {
     private static final String TAG = AppLocaleStore.class.getSimpleName();
 
     public static AppLocaleResult getAppSupportedLocales(
@@ -148,8 +151,11 @@
         return false;
     }
 
-    static class AppLocaleResult {
-        enum LocaleStatus {
+    /**
+     * A class used to store an application's supporting locales.
+     */
+    public static class AppLocaleResult {
+        public enum LocaleStatus {
             UNKNOWN_FAILURE,
             NO_SUPPORTED_LANGUAGE_IN_APP,
             ASSET_LOCALE_IS_EMPTY,
@@ -158,7 +164,7 @@
         }
 
         LocaleStatus mLocaleStatus;
-        HashSet<Locale> mAppSupportedLocales;
+        public HashSet<Locale> mAppSupportedLocales;
 
         public AppLocaleResult(LocaleStatus localeStatus, HashSet<Locale> appSupportedLocales) {
             this.mLocaleStatus = localeStatus;
diff --git a/core/java/com/android/internal/app/LocaleStore.java b/core/java/com/android/internal/app/LocaleStore.java
index 689dec4..d2eee91 100644
--- a/core/java/com/android/internal/app/LocaleStore.java
+++ b/core/java/com/android/internal/app/LocaleStore.java
@@ -23,6 +23,7 @@
 import android.provider.Settings;
 import android.telephony.TelephonyManager;
 import android.util.Log;
+import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.internal.annotations.VisibleForTesting;
 
@@ -45,10 +46,11 @@
         @VisibleForTesting static final int SUGGESTION_TYPE_SIM = 1 << 0;
         @VisibleForTesting static final int SUGGESTION_TYPE_CFG = 1 << 1;
         // Only for per-app language picker
-        private static final int SUGGESTION_TYPE_CURRENT = 1 << 2;
+        @VisibleForTesting static final int SUGGESTION_TYPE_CURRENT = 1 << 2;
         // Only for per-app language picker
-        private static final int SUGGESTION_TYPE_SYSTEM_LANGUAGE = 1 << 3;
-
+        @VisibleForTesting  static final int SUGGESTION_TYPE_SYSTEM_LANGUAGE = 1 << 3;
+        // Only for per-app language picker
+        @VisibleForTesting static final int SUGGESTION_TYPE_OTHER_APP_LANGUAGE = 1 << 4;
         private final Locale mLocale;
         private final Locale mParent;
         private final String mId;
@@ -259,7 +261,16 @@
         }
     }
 
-    public static LocaleInfo getAppCurrentLocaleInfo(Context context, String appPackageName) {
+    /**
+     * Get the application's activated locale.
+     *
+     * @param context UI activity's context.
+     * @param appPackageName The application's package name.
+     * @param isAppSelected True if the application is selected in the UI; false otherwise.
+     * @return A LocaleInfo with the application's activated locale.
+     */
+    public static LocaleInfo getAppActivatedLocaleInfo(Context context, String appPackageName,
+            boolean isAppSelected) {
         if (appPackageName == null) {
             return null;
         }
@@ -272,7 +283,11 @@
 
             if (locale != null) {
                 LocaleInfo localeInfo = new LocaleInfo(locale);
-                localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CURRENT;
+                if (isAppSelected) {
+                    localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CURRENT;
+                } else {
+                    localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE;
+                }
                 localeInfo.mIsTranslated = true;
                 return localeInfo;
             }
@@ -283,6 +298,24 @@
     }
 
     /**
+     * Transform IME's language tag to LocaleInfo.
+     *
+     * @param list A list which includes IME's subtype.
+     * @return A LocaleInfo set which includes IME's language tags.
+     */
+    public static Set<LocaleInfo> transformImeLanguageTagToLocaleInfo(
+            List<InputMethodSubtype> list) {
+        Set<LocaleInfo> imeLocales = new HashSet<>();
+        for (InputMethodSubtype subtype : list) {
+            LocaleInfo localeInfo = new LocaleInfo(subtype.getLanguageTag());
+            localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE;
+            localeInfo.mIsTranslated = true;
+            imeLocales.add(localeInfo);
+        }
+        return imeLocales;
+    }
+
+    /**
      * Returns a list of system languages with LocaleInfo
      */
     public static List<LocaleInfo> getSystemCurrentLocaleInfo() {
@@ -458,4 +491,13 @@
         }
         return result;
     }
+
+    /**
+     * API for testing.
+     */
+    @UnsupportedAppUsage
+    @VisibleForTesting
+    public static LocaleInfo fromLocale(Locale locale) {
+        return new LocaleInfo(locale);
+    }
 }
diff --git a/core/java/com/android/internal/content/InstallLocationUtils.java b/core/java/com/android/internal/content/InstallLocationUtils.java
index c456cf3..4d9c09e 100644
--- a/core/java/com/android/internal/content/InstallLocationUtils.java
+++ b/core/java/com/android/internal/content/InstallLocationUtils.java
@@ -31,6 +31,7 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.SystemProperties;
 import android.os.storage.IStorageManager;
 import android.os.storage.StorageManager;
 import android.os.storage.StorageVolume;
@@ -261,12 +262,12 @@
 
         // We're left with new installations with either preferring external or auto, so just pick
         // volume with most space
+        String bestCandidate = !volumePaths.isEmpty() ? volumePaths.keyAt(0) : null;
         if (volumePaths.size() == 1) {
             if (checkFitOnVolume(storageManager, volumePaths.valueAt(0), params)) {
-                return volumePaths.keyAt(0);
+                return bestCandidate;
             }
         } else {
-            String bestCandidate = null;
             long bestCandidateAvailBytes = Long.MIN_VALUE;
             for (String vol : volumePaths.keySet()) {
                 final String volumePath = volumePaths.get(vol);
@@ -289,6 +290,14 @@
 
         }
 
+        // For new installations of a predefined size, check property to let it through
+        // regardless of the actual free space.
+        if (bestCandidate != null && Integer.MAX_VALUE == params.sizeBytes
+                && SystemProperties.getBoolean("debug.pm.install_skip_size_check_for_maxint",
+                false)) {
+            return bestCandidate;
+        }
+
         throw new IOException("No special requests, but no room on allowed volumes. "
                 + " allow3rdPartyOnInternal? " + allow3rdPartyOnInternal);
     }
diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp
index 19cb30e..75f8402 100644
--- a/core/jni/android_media_AudioSystem.cpp
+++ b/core/jni/android_media_AudioSystem.cpp
@@ -2739,12 +2739,26 @@
 }
 
 static jint android_media_AudioSystem_removeDevicesRoleForStrategy(JNIEnv *env, jobject thiz,
-                                                                   jint strategy, jint role) {
+                                                                   jint strategy, jint role,
+                                                                   jintArray jDeviceTypes,
+                                                                   jobjectArray jDeviceAddresses) {
+    AudioDeviceTypeAddrVector nDevices;
+    jint results = getVectorOfAudioDeviceTypeAddr(env, jDeviceTypes, jDeviceAddresses, nDevices);
+    if (results != NO_ERROR) {
+        return results;
+    }
+    int status = check_AudioSystem_Command(
+            AudioSystem::removeDevicesRoleForStrategy((product_strategy_t)strategy,
+                                                      (device_role_t)role, nDevices));
+    return (jint)status;
+}
+
+static jint android_media_AudioSystem_clearDevicesRoleForStrategy(JNIEnv *env, jobject thiz,
+                                                                  jint strategy, jint role) {
     return (jint)
-            check_AudioSystem_Command(AudioSystem::removeDevicesRoleForStrategy((product_strategy_t)
-                                                                                        strategy,
-                                                                                (device_role_t)
-                                                                                        role),
+            check_AudioSystem_Command(AudioSystem::clearDevicesRoleForStrategy((product_strategy_t)
+                                                                                       strategy,
+                                                                               (device_role_t)role),
                                       {NAME_NOT_FOUND});
 }
 
@@ -3341,8 +3355,10 @@
           (void *)android_media_AudioSystem_isCallScreeningModeSupported},
          {"setDevicesRoleForStrategy", "(II[I[Ljava/lang/String;)I",
           (void *)android_media_AudioSystem_setDevicesRoleForStrategy},
-         {"removeDevicesRoleForStrategy", "(II)I",
+         {"removeDevicesRoleForStrategy", "(II[I[Ljava/lang/String;)I",
           (void *)android_media_AudioSystem_removeDevicesRoleForStrategy},
+         {"clearDevicesRoleForStrategy", "(II)I",
+          (void *)android_media_AudioSystem_clearDevicesRoleForStrategy},
          {"getDevicesForRoleAndStrategy", "(IILjava/util/List;)I",
           (void *)android_media_AudioSystem_getDevicesForRoleAndStrategy},
          {"setDevicesRoleForCapturePreset", "(II[I[Ljava/lang/String;)I",
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 3274e85..124d9e6 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -258,6 +258,7 @@
         android:name="com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY" />
     <protected-broadcast
         android:name="android.bluetooth.pan.profile.action.CONNECTION_STATE_CHANGED" />
+    <protected-broadcast android:name="android.bluetooth.action.HAP_CONNECTION_STATE_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_CONNECTION_STATE_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_ACTIVE_DEVICE_CHANGED" />
     <protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_CONF_CHANGED" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 35ff7e8..92b4ba1 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2701,6 +2701,10 @@
     <!-- Whether UI for multi user should be shown -->
     <bool name="config_enableMultiUserUI">false</bool>
 
+    <!-- Whether multiple admins are allowed on the device. If set to true, new users can be created
+     with admin privileges and admin privileges can be granted/revoked from existing users. -->
+    <bool name="config_enableMultipleAdmins">false</bool>
+
     <!-- Whether the new Auto Selection Network UI should be shown -->
     <bool name="config_enableNewAutoSelectNetworkUI">false</bool>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index c73f2f4..f54335a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -353,6 +353,7 @@
   <java-symbol type="bool" name="config_speed_up_audio_on_mt_calls" />
   <java-symbol type="bool" name="config_useFixedVolume" />
   <java-symbol type="bool" name="config_enableMultiUserUI"/>
+  <java-symbol type="bool" name="config_enableMultipleAdmins"/>
   <java-symbol type="bool" name="config_enableNewAutoSelectNetworkUI"/>
   <java-symbol type="bool" name="config_disableUsbPermissionDialogs"/>
   <java-symbol type="dimen" name="config_highResTaskSnapshotScale" />
diff --git a/data/keyboards/Vendor_054c_Product_0df2.kl b/data/keyboards/Vendor_054c_Product_0df2.kl
new file mode 100644
index 0000000..a47b310
--- /dev/null
+++ b/data/keyboards/Vendor_054c_Product_0df2.kl
@@ -0,0 +1,73 @@
+# Copyright (C) 2022 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.
+
+#
+# Sony Playstation(R) DualSense Edge Controller
+#
+
+# Only use this key layout if we have HID_PLAYSTATION!
+requires_kernel_config CONFIG_HID_PLAYSTATION
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 0x134   BUTTON_X
+# Cross
+key 0x130   BUTTON_A
+# Circle
+key 0x131   BUTTON_B
+# Triangle
+key 0x133   BUTTON_Y
+
+key 0x136   BUTTON_L1
+key 0x137   BUTTON_R1
+key 0x138   BUTTON_L2
+key 0x139   BUTTON_R2
+
+# L2 axis
+axis 0x02   LTRIGGER
+# R2 axis
+axis 0x05   RTRIGGER
+
+# Left Analog Stick
+axis 0x00   X
+axis 0x01   Y
+# Right Analog Stick
+axis 0x03   Z
+axis 0x04   RZ
+
+# Left stick click
+key 0x13d   BUTTON_THUMBL
+# Right stick click
+key 0x13e   BUTTON_THUMBR
+
+# Hat
+axis 0x10   HAT_X
+axis 0x11   HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share / "half-sun"
+key 0x13a   BUTTON_SELECT
+# Options / three horizontal lines
+key 0x13b   BUTTON_START
+# PS key
+key 0x13c   BUTTON_MODE
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/data/keyboards/Vendor_054c_Product_0df2_fallback.kl b/data/keyboards/Vendor_054c_Product_0df2_fallback.kl
new file mode 100644
index 0000000..bfebb17
--- /dev/null
+++ b/data/keyboards/Vendor_054c_Product_0df2_fallback.kl
@@ -0,0 +1,75 @@
+# Copyright (C) 2022 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.
+
+#
+# Sony Playstation(R) DualSense Edge Controller
+#
+
+# Use this if HID_PLAYSTATION is not available
+
+# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html
+
+# Square
+key 304   BUTTON_X
+# Cross
+key 305   BUTTON_A
+# Circle
+key 306   BUTTON_B
+# Triangle
+key 307   BUTTON_Y
+
+key 308   BUTTON_L1
+key 309   BUTTON_R1
+key 310   BUTTON_L2
+key 311   BUTTON_R2
+
+# L2 axis
+axis 0x03   LTRIGGER
+# R2 axis
+axis 0x04   RTRIGGER
+
+# Left Analog Stick
+axis 0x00   X
+axis 0x01   Y
+# Right Analog Stick
+axis 0x02   Z
+axis 0x05   RZ
+
+# Left stick click
+key 314   BUTTON_THUMBL
+# Right stick click
+key 315   BUTTON_THUMBR
+
+# Hat
+axis 0x10   HAT_X
+axis 0x11   HAT_Y
+
+# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt
+# Share / "half-sun"
+key 312   BUTTON_SELECT
+# Options / three horizontal lines
+key 313   BUTTON_START
+# PS key
+key 316   BUTTON_MODE
+
+# Touchpad press
+key 317 BUTTON_1
+
+# SENSORs
+sensor 0x00 ACCELEROMETER X
+sensor 0x01 ACCELEROMETER Y
+sensor 0x02 ACCELEROMETER Z
+sensor 0x03 GYROSCOPE X
+sensor 0x04 GYROSCOPE Y
+sensor 0x05 GYROSCOPE Z
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 23db233..774f6c6 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -115,4 +115,10 @@
     <!-- Components support to launch multiple instances into split-screen -->
     <string-array name="config_appsSupportMultiInstancesSplit">
     </string-array>
+
+    <!-- Whether the extended restart dialog is enabled -->
+    <bool name="config_letterboxIsRestartDialogEnabled">false</bool>
+
+    <!-- Whether the additional education about reachability is enabled -->
+    <bool name="config_letterboxIsReachabilityEducationEnabled">false</bool>
 </resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
new file mode 100644
index 0000000..4f33a71
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2022 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.wm.shell.compatui;
+
+import android.content.Context;
+import android.provider.DeviceConfig;
+
+import androidx.annotation.NonNull;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.dagger.WMSingleton;
+
+import javax.inject.Inject;
+
+/**
+ * Configuration flags for the CompatUX implementation
+ */
+@WMSingleton
+public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedListener {
+
+    static final String KEY_ENABLE_LETTERBOX_RESTART_DIALOG = "enable_letterbox_restart_dialog";
+
+    static final String KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION =
+            "enable_letterbox_reachability_education";
+
+    // Whether the extended restart dialog is enabled
+    private boolean mIsRestartDialogEnabled;
+
+    // Whether the additional education about reachability is enabled
+    private boolean mIsReachabilityEducationEnabled;
+
+    // Whether the extended restart dialog is enabled
+    private boolean mIsRestartDialogOverrideEnabled;
+
+    // Whether the additional education about reachability is enabled
+    private boolean mIsReachabilityEducationOverrideEnabled;
+
+    // Whether the extended restart dialog is allowed from backend
+    private boolean mIsLetterboxRestartDialogAllowed;
+
+    // Whether the additional education about reachability is allowed from backend
+    private boolean mIsLetterboxReachabilityEducationAllowed;
+
+    @Inject
+    public CompatUIConfiguration(Context context, @ShellMainThread ShellExecutor mainExecutor) {
+        mIsRestartDialogEnabled = context.getResources().getBoolean(
+                R.bool.config_letterboxIsRestartDialogEnabled);
+        mIsReachabilityEducationEnabled = context.getResources().getBoolean(
+                R.bool.config_letterboxIsReachabilityEducationEnabled);
+        mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG, false);
+        mIsLetterboxReachabilityEducationAllowed = DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION,
+                false);
+        DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_APP_COMPAT, mainExecutor,
+                this);
+    }
+
+    /**
+     * @return {@value true} if the restart dialog is enabled.
+     */
+    boolean isRestartDialogEnabled() {
+        return mIsRestartDialogOverrideEnabled || (mIsRestartDialogEnabled
+                && mIsLetterboxRestartDialogAllowed);
+    }
+
+    /**
+     * Enables/Disables the restart education dialog
+     */
+    void setIsRestartDialogOverrideEnabled(boolean enabled) {
+        mIsRestartDialogOverrideEnabled = enabled;
+    }
+
+    /**
+     * @return {@value true} if the reachability education is enabled.
+     */
+    boolean isReachabilityEducationEnabled() {
+        return mIsReachabilityEducationOverrideEnabled || (mIsReachabilityEducationEnabled
+                && mIsLetterboxReachabilityEducationAllowed);
+    }
+
+    /**
+     * Enables/Disables the reachability education
+     */
+    void setIsReachabilityEducationOverrideEnabled(boolean enabled) {
+        mIsReachabilityEducationOverrideEnabled = enabled;
+    }
+
+    @Override
+    public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
+        // TODO(b/263349751): Update flag and default value to true
+        if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_RESTART_DIALOG)) {
+            mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean(
+                    DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG,
+                    false);
+        }
+        if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION)) {
+            mIsLetterboxReachabilityEducationAllowed = DeviceConfig.getBoolean(
+                    DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+                    KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION, false);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java
new file mode 100644
index 0000000..4fb18e2
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2022 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.wm.shell.compatui;
+
+import com.android.wm.shell.dagger.WMSingleton;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+
+/**
+ * Handles the shell commands for the CompatUX.
+ *
+ * <p> Use with {@code adb shell dumpsys activity service SystemUIService WMShell compatui
+ * &lt;command&gt;}.
+ */
+@WMSingleton
+public final class CompatUIShellCommandHandler implements
+        ShellCommandHandler.ShellCommandActionHandler {
+
+    private final CompatUIConfiguration mCompatUIConfiguration;
+    private final ShellCommandHandler mShellCommandHandler;
+
+    @Inject
+    public CompatUIShellCommandHandler(ShellCommandHandler shellCommandHandler,
+            CompatUIConfiguration compatUIConfiguration) {
+        mShellCommandHandler = shellCommandHandler;
+        mCompatUIConfiguration = compatUIConfiguration;
+    }
+
+    void onInit() {
+        mShellCommandHandler.addCommandCallback("compatui", this, this);
+    }
+
+    @Override
+    public boolean onShellCommand(String[] args, PrintWriter pw) {
+        if (args.length != 2) {
+            pw.println("Invalid command: " + args[0]);
+            return false;
+        }
+        switch (args[0]) {
+            case "restartDialogEnabled":
+                return invokeOrError(args[1], pw,
+                        mCompatUIConfiguration::setIsRestartDialogOverrideEnabled);
+            case "reachabilityEducationEnabled":
+                return invokeOrError(args[1], pw,
+                        mCompatUIConfiguration::setIsReachabilityEducationOverrideEnabled);
+            default:
+                pw.println("Invalid command: " + args[0]);
+                return false;
+        }
+    }
+
+    @Override
+    public void printShellCommandHelp(PrintWriter pw, String prefix) {
+        pw.println(prefix + "restartDialogEnabled [0|false|1|true]");
+        pw.println(prefix + "  Enable/Disable the restart education dialog for Size Compat Mode");
+        pw.println(prefix + "reachabilityEducationEnabled [0|false|1|true]");
+        pw.println(prefix
+                + "  Enable/Disable the restart education dialog for letterbox reachability");
+        pw.println(prefix + "  Disable the restart education dialog for letterbox reachability");
+    }
+
+    private static boolean invokeOrError(String input, PrintWriter pw,
+            Consumer<Boolean> setter) {
+        Boolean asBoolean = strToBoolean(input);
+        if (asBoolean == null) {
+            pw.println("Error: expected true, 1, false, 0.");
+            return false;
+        }
+        setter.accept(asBoolean);
+        return true;
+    }
+
+    // Converts a String to boolean if possible or it returns null otherwise
+    private static Boolean strToBoolean(String str) {
+        switch(str) {
+            case "1":
+            case "true":
+                return true;
+            case "0":
+            case "false":
+                return false;
+        }
+        return null;
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogAnimationController.java
similarity index 85%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java
rename to libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogAnimationController.java
index 3061eab..7475fea 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogAnimationController.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.compatui.letterboxedu;
+package com.android.wm.shell.compatui;
 
 import static com.android.internal.R.styleable.WindowAnimation_windowEnterAnimation;
 import static com.android.internal.R.styleable.WindowAnimation_windowExitAnimation;
@@ -38,10 +38,15 @@
 import com.android.internal.policy.TransitionAnimation;
 
 /**
- * Controls the enter/exit animations of the letterbox education.
+ * Controls the enter/exit a dialog.
+ *
+ * @param <T> The {@link DialogContainerSupplier} to use
  */
-class LetterboxEduAnimationController {
-    private static final String TAG = "LetterboxEduAnimation";
+public class DialogAnimationController<T extends DialogContainerSupplier> {
+
+    // The alpha of a background is a number between 0 (fully transparent) to 255 (fully opaque).
+    // 204 is simply 255 * 0.8.
+    static final int BACKGROUND_DIM_ALPHA = 204;
 
     // If shell transitions are enabled, startEnterAnimation will be called after all transitions
     // have finished, and therefore the start delay should be shorter.
@@ -49,6 +54,7 @@
 
     private final TransitionAnimation mTransitionAnimation;
     private final String mPackageName;
+    private final String mTag;
     @AnyRes
     private final int mAnimStyleResId;
 
@@ -57,23 +63,24 @@
     @Nullable
     private Animator mBackgroundDimAnimator;
 
-    LetterboxEduAnimationController(Context context) {
-        mTransitionAnimation = new TransitionAnimation(context, /* debug= */ false, TAG);
+    public DialogAnimationController(Context context, String tag) {
+        mTransitionAnimation = new TransitionAnimation(context, /* debug= */ false, tag);
         mAnimStyleResId = (new ContextThemeWrapper(context,
                 android.R.style.ThemeOverlay_Material_Dialog).getTheme()).obtainStyledAttributes(
                 com.android.internal.R.styleable.Window).getResourceId(
                 com.android.internal.R.styleable.Window_windowAnimationStyle, 0);
         mPackageName = context.getPackageName();
+        mTag = tag;
     }
 
     /**
      * Starts both background dim fade-in animation and the dialog enter animation.
      */
-    void startEnterAnimation(@NonNull LetterboxEduDialogLayout layout, Runnable endCallback) {
+    public void startEnterAnimation(@NonNull T layout, Runnable endCallback) {
         // Cancel any previous animation if it's still running.
         cancelAnimation();
 
-        final View dialogContainer = layout.getDialogContainer();
+        final View dialogContainer = layout.getDialogContainerView();
         mDialogAnimation = loadAnimation(WindowAnimation_windowEnterAnimation);
         if (mDialogAnimation == null) {
             endCallback.run();
@@ -86,8 +93,8 @@
                     endCallback.run();
                 }));
 
-        mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDim(),
-                /* endAlpha= */ LetterboxEduDialogLayout.BACKGROUND_DIM_ALPHA,
+        mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDimDrawable(),
+                /* endAlpha= */ BACKGROUND_DIM_ALPHA,
                 mDialogAnimation.getDuration());
         mBackgroundDimAnimator.addListener(getDimAnimatorListener());
 
@@ -101,11 +108,11 @@
     /**
      * Starts both the background dim fade-out animation and the dialog exit animation.
      */
-    void startExitAnimation(@NonNull LetterboxEduDialogLayout layout, Runnable endCallback) {
+    public void startExitAnimation(@NonNull T layout, Runnable endCallback) {
         // Cancel any previous animation if it's still running.
         cancelAnimation();
 
-        final View dialogContainer = layout.getDialogContainer();
+        final View dialogContainer = layout.getDialogContainerView();
         mDialogAnimation = loadAnimation(WindowAnimation_windowExitAnimation);
         if (mDialogAnimation == null) {
             endCallback.run();
@@ -119,8 +126,8 @@
                     endCallback.run();
                 }));
 
-        mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDim(), /* endAlpha= */ 0,
-                mDialogAnimation.getDuration());
+        mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDimDrawable(),
+                /* endAlpha= */ 0, mDialogAnimation.getDuration());
         mBackgroundDimAnimator.addListener(getDimAnimatorListener());
 
         dialogContainer.startAnimation(mDialogAnimation);
@@ -130,7 +137,7 @@
     /**
      * Cancels all animations and resets the state of the controller.
      */
-    void cancelAnimation() {
+    public void cancelAnimation() {
         if (mDialogAnimation != null) {
             mDialogAnimation.cancel();
             mDialogAnimation = null;
@@ -145,7 +152,7 @@
         Animation animation = mTransitionAnimation.loadAnimationAttr(mPackageName, mAnimStyleResId,
                 animAttr, /* translucent= */ false);
         if (animation == null) {
-            Log.e(TAG, "Failed to load animation " + animAttr);
+            Log.e(mTag, "Failed to load animation " + animAttr);
         }
         return animation;
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java
new file mode 100644
index 0000000..7eea446
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 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.wm.shell.compatui;
+
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+/**
+ * A component which can provide a {@link View} to use as a container for a Dialog
+ */
+public interface DialogContainerSupplier {
+
+    /**
+     * @return The {@link View} to use as a container for a Dialog
+     */
+    View getDialogContainerView();
+
+    /**
+     * @return The {@link Drawable} to use as background of the dialog.
+     */
+    Drawable getBackgroundDimDrawable();
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java
index 2e0b09e..9232f36 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java
@@ -26,6 +26,7 @@
 import androidx.constraintlayout.widget.ConstraintLayout;
 
 import com.android.wm.shell.R;
+import com.android.wm.shell.compatui.DialogContainerSupplier;
 
 /**
  * Container for Letterbox Education Dialog and background dim.
@@ -33,11 +34,7 @@
  * <p>This layout should fill the entire task and the background around the dialog acts as the
  * background dim which dismisses the dialog when clicked.
  */
-class LetterboxEduDialogLayout extends ConstraintLayout {
-
-    // The alpha of a background is a number between 0 (fully transparent) to 255 (fully opaque).
-    // 204 is simply 255 * 0.8.
-    static final int BACKGROUND_DIM_ALPHA = 204;
+class LetterboxEduDialogLayout extends ConstraintLayout implements DialogContainerSupplier {
 
     private View mDialogContainer;
     private TextView mDialogTitle;
@@ -60,18 +57,20 @@
         super(context, attrs, defStyleAttr, defStyleRes);
     }
 
-    View getDialogContainer() {
+    @Override
+    public View getDialogContainerView() {
         return mDialogContainer;
     }
 
+    @Override
+    public Drawable getBackgroundDimDrawable() {
+        return mBackgroundDim;
+    }
+
     TextView getDialogTitle() {
         return mDialogTitle;
     }
 
-    Drawable getBackgroundDim() {
-        return mBackgroundDim;
-    }
-
     /**
      * Register a callback for the dismiss button and background dim.
      *
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
index 867d0ef..c14c009 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
@@ -37,6 +37,7 @@
 import com.android.wm.shell.common.DockStateReader;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.compatui.CompatUIWindowManagerAbstract;
+import com.android.wm.shell.compatui.DialogAnimationController;
 import com.android.wm.shell.transition.Transitions;
 
 /**
@@ -63,7 +64,7 @@
      */
     private final SharedPreferences mSharedPreferences;
 
-    private final LetterboxEduAnimationController mAnimationController;
+    private final DialogAnimationController<LetterboxEduDialogLayout> mAnimationController;
 
     private final Transitions mTransitions;
 
@@ -96,14 +97,17 @@
             DisplayLayout displayLayout, Transitions transitions,
             Runnable onDismissCallback, DockStateReader dockStateReader) {
         this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions,
-                onDismissCallback, new LetterboxEduAnimationController(context), dockStateReader);
+                onDismissCallback,
+                new DialogAnimationController<>(context, /* tag */ "LetterboxEduWindowManager"),
+                dockStateReader);
     }
 
     @VisibleForTesting
     LetterboxEduWindowManager(Context context, TaskInfo taskInfo,
             SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
             DisplayLayout displayLayout, Transitions transitions, Runnable onDismissCallback,
-            LetterboxEduAnimationController animationController, DockStateReader dockStateReader) {
+            DialogAnimationController<LetterboxEduDialogLayout> animationController,
+            DockStateReader dockStateReader) {
         super(context, taskInfo, syncQueue, taskListener, displayLayout);
         mTransitions = transitions;
         mOnDismissCallback = onDismissCallback;
@@ -160,7 +164,7 @@
         if (mLayout == null) {
             return;
         }
-        final View dialogContainer = mLayout.getDialogContainer();
+        final View dialogContainer = mLayout.getDialogContainerView();
         MarginLayoutParams marginParams = (MarginLayoutParams) dialogContainer.getLayoutParams();
 
         final Rect taskBounds = getTaskBounds();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
index 6b59e31..d7cb490 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
@@ -16,8 +16,6 @@
 
 package com.android.wm.shell.unfold;
 
-import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-
 import android.annotation.NonNull;
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.TaskInfo;
@@ -56,6 +54,12 @@
     private final SparseArray<SurfaceControl> mTaskSurfaces = new SparseArray<>();
     private final SparseArray<UnfoldTaskAnimator> mAnimatorsByTaskId = new SparseArray<>();
 
+    /**
+     * Indicates whether we're in stage change process. This should be set to {@code true} in
+     * {@link #onStateChangeStarted()} and {@code false} in {@link #onStateChangeFinished()}.
+     */
+    private boolean mIsInStageChange;
+
     public UnfoldAnimationController(
             @NonNull ShellInit shellInit,
             @NonNull TransactionPool transactionPool,
@@ -123,7 +127,7 @@
                 animator.onTaskChanged(taskInfo);
             } else {
                 // Became inapplicable
-                resetTask(animator, taskInfo);
+                maybeResetTask(animator, taskInfo);
                 animator.onTaskVanished(taskInfo);
                 mAnimatorsByTaskId.remove(taskInfo.taskId);
             }
@@ -154,7 +158,7 @@
         final boolean isCurrentlyApplicable = animator != null;
 
         if (isCurrentlyApplicable) {
-            resetTask(animator, taskInfo);
+            maybeResetTask(animator, taskInfo);
             animator.onTaskVanished(taskInfo);
             mAnimatorsByTaskId.remove(taskInfo.taskId);
         }
@@ -166,6 +170,7 @@
             return;
         }
 
+        mIsInStageChange = true;
         SurfaceControl.Transaction transaction = null;
         for (int i = 0; i < mAnimators.size(); i++) {
             final UnfoldTaskAnimator animator = mAnimators.get(i);
@@ -219,11 +224,12 @@
         transaction.apply();
 
         mTransactionPool.release(transaction);
+        mIsInStageChange = false;
     }
 
-    private void resetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) {
-        if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) {
-            // PiP task has its own cleanup path, ignore surface reset to avoid conflict.
+    private void maybeResetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) {
+        if (!mIsInStageChange) {
+            // No need to resetTask if there is no ongoing state change.
             return;
         }
         final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java
index 1dee88c..a58620d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java
@@ -68,11 +68,11 @@
 
     @Test
     public void testOnFinishInflate() {
-        assertEquals(mLayout.getDialogContainer(),
+        assertEquals(mLayout.getDialogContainerView(),
                 mLayout.findViewById(R.id.letterbox_education_dialog_container));
         assertEquals(mLayout.getDialogTitle(),
                 mLayout.findViewById(R.id.letterbox_education_dialog_title));
-        assertEquals(mLayout.getBackgroundDim(), mLayout.getBackground());
+        assertEquals(mLayout.getBackgroundDimDrawable(), mLayout.getBackground());
         assertEquals(mLayout.getBackground().getAlpha(), 0);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
index 16517c0..14190f1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
@@ -56,6 +56,7 @@
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.DockStateReader;
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.compatui.DialogAnimationController;
 import com.android.wm.shell.transition.Transitions;
 
 import org.junit.After;
@@ -98,7 +99,7 @@
     @Captor
     private ArgumentCaptor<Runnable> mRunOnIdleCaptor;
 
-    @Mock private LetterboxEduAnimationController mAnimationController;
+    @Mock private DialogAnimationController<LetterboxEduDialogLayout> mAnimationController;
     @Mock private SyncTransactionQueue mSyncTransactionQueue;
     @Mock private ShellTaskOrganizer.TaskListener mTaskListener;
     @Mock private SurfaceControlViewHost mViewHost;
@@ -366,7 +367,7 @@
         assertThat(params.width).isEqualTo(expectedWidth);
         assertThat(params.height).isEqualTo(expectedHeight);
         MarginLayoutParams dialogParams =
-                (MarginLayoutParams) layout.getDialogContainer().getLayoutParams();
+                (MarginLayoutParams) layout.getDialogContainerView().getLayoutParams();
         int verticalMargin = (int) mContext.getResources().getDimension(
                 R.dimen.letterbox_education_dialog_margin);
         assertThat(dialogParams.topMargin).isEqualTo(verticalMargin + expectedExtraTopMargin);
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index fdd6233..19610a93 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -1841,8 +1841,7 @@
      * {@link #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
      * {@link #setPreferredDevicesForStrategy(AudioProductStrategy, List<AudioDeviceAttributes>)}
      * @param strategy the strategy to query
-     * @return the preferred device for that strategy, or null if none was ever set or if the
-     *    strategy is invalid
+     * @return list of the preferred devices for that strategy
      */
     @SystemApi
     @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
@@ -1859,6 +1858,76 @@
 
     /**
      * @hide
+     * Set a device as non-default for a given strategy, i.e. the audio routing to be avoided by
+     * this audio strategy.
+     * <p>Use
+     * {@link #removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
+     * to cancel setting this preference for this strategy.</p>
+     * @param strategy the audio strategy whose routing will be affected
+     * @param device the audio device to not route to when available
+     * @return true if the operation was successful, false otherwise
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public boolean setDeviceAsNonDefaultForStrategy(@NonNull AudioProductStrategy strategy,
+                                                    @NonNull AudioDeviceAttributes device) {
+        Objects.requireNonNull(strategy);
+        Objects.requireNonNull(device);
+        try {
+            final int status =
+                    getService().setDeviceAsNonDefaultForStrategy(strategy.getId(), device);
+            return status == AudioSystem.SUCCESS;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
+     * Removes the audio device(s) from the non-default device list previously set with
+     * {@link #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
+     * @param strategy the audio strategy whose routing will be affected
+     * @param device the audio device to remove from the non-default device list
+     * @return true if the operation was successful, false otherwise (invalid strategy, or no
+     *     device set for example)
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public boolean removeDeviceAsNonDefaultForStrategy(@NonNull AudioProductStrategy strategy,
+                                                       @NonNull AudioDeviceAttributes device) {
+        Objects.requireNonNull(strategy);
+        Objects.requireNonNull(device);
+        try {
+            final int status =
+                    getService().removeDeviceAsNonDefaultForStrategy(strategy.getId(), device);
+            return status == AudioSystem.SUCCESS;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
+     * Gets the audio device(s) from the non-default device list previously set with
+     * {@link #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
+     * @param strategy the audio strategy to query
+     * @return list of non-default devices for the strategy
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    @NonNull
+    public List<AudioDeviceAttributes> getNonDefaultDevicesForStrategy(
+            @NonNull AudioProductStrategy strategy) {
+        Objects.requireNonNull(strategy);
+        try {
+            return getService().getNonDefaultDevicesForStrategy(strategy.getId());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
      * Interface to be notified of changes in the preferred audio device set for a given audio
      * strategy.
      * <p>Note that this listener will only be invoked whenever
@@ -1892,9 +1961,11 @@
      * Interface to be notified of changes in the preferred audio devices set for a given audio
      * strategy.
      * <p>Note that this listener will only be invoked whenever
-     * {@link #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)} or
-     * {@link #setPreferredDevicesForStrategy(AudioProductStrategy, List<AudioDeviceAttributes>)}
-     * {@link #removePreferredDeviceForStrategy(AudioProductStrategy)} causes a change in
+     * {@link #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)},
+     * {@link #setPreferredDevicesForStrategy(AudioProductStrategy, List<AudioDeviceAttributes>)},
+     * {@link #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)},
+     * {@link #removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
+     * or {@link #removePreferredDeviceForStrategy(AudioProductStrategy)} causes a change in
      * preferred device(s). It will not be invoked directly after registration with
      * {@link #addOnPreferredDevicesForStrategyChangedListener(
      * Executor, OnPreferredDevicesForStrategyChangedListener)}
@@ -1902,7 +1973,6 @@
      * @see #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)
      * @see #setPreferredDevicesForStrategy(AudioProductStrategy, List)
      * @see #removePreferredDeviceForStrategy(AudioProductStrategy)
-     * @see #getPreferredDeviceForStrategy(AudioProductStrategy)
      * @see #getPreferredDevicesForStrategy(AudioProductStrategy)
      */
     @SystemApi
@@ -1966,30 +2036,9 @@
             throws SecurityException {
         Objects.requireNonNull(executor);
         Objects.requireNonNull(listener);
-        synchronized (mPrefDevListenerLock) {
-            if (hasPrefDevListener(listener)) {
-                throw new IllegalArgumentException(
-                        "attempt to call addOnPreferredDevicesForStrategyChangedListener() "
-                                + "on a previously registered listener");
-            }
-            // lazy initialization of the list of strategy-preferred device listener
-            if (mPrefDevListeners == null) {
-                mPrefDevListeners = new ArrayList<>();
-            }
-            final int oldCbCount = mPrefDevListeners.size();
-            mPrefDevListeners.add(new PrefDevListenerInfo(listener, executor));
-            if (oldCbCount == 0 && mPrefDevListeners.size() > 0) {
-                // register binder for callbacks
-                if (mPrefDevDispatcherStub == null) {
-                    mPrefDevDispatcherStub = new StrategyPreferredDevicesDispatcherStub();
-                }
-                try {
-                    getService().registerStrategyPreferredDevicesDispatcher(mPrefDevDispatcherStub);
-                } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
-                }
-            }
-        }
+        mPrefDevListenerMgr.addListener(
+                executor, listener, "addOnPreferredDevicesForStrategyChangedListener",
+                () -> new StrategyPreferredDevicesDispatcherStub());
     }
 
     /**
@@ -2002,106 +2051,145 @@
     public void removeOnPreferredDevicesForStrategyChangedListener(
             @NonNull OnPreferredDevicesForStrategyChangedListener listener) {
         Objects.requireNonNull(listener);
-        synchronized (mPrefDevListenerLock) {
-            if (!removePrefDevListener(listener)) {
-                throw new IllegalArgumentException(
-                        "attempt to call removeOnPreferredDeviceForStrategyChangedListener() "
-                                + "on an unregistered listener");
-            }
-            if (mPrefDevListeners.size() == 0) {
-                // unregister binder for callbacks
-                try {
-                    getService().unregisterStrategyPreferredDevicesDispatcher(
-                            mPrefDevDispatcherStub);
-                } catch (RemoteException e) {
-                    throw e.rethrowFromSystemServer();
-                } finally {
-                    mPrefDevDispatcherStub = null;
-                    mPrefDevListeners = null;
-                }
-            }
-        }
+        mPrefDevListenerMgr.removeListener(
+                listener, "removeOnPreferredDevicesForStrategyChangedListener");
     }
 
-
-    private final Object mPrefDevListenerLock = new Object();
     /**
-     * List of listeners for preferred device for strategy and their associated Executor.
-     * List is lazy-initialized on first registration
+     * @hide
+     * Interface to be notified of changes in the non-default audio devices set for a given audio
+     * strategy.
+     * <p>Note that this listener will only be invoked whenever
+     * {@link #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)},
+     * {@link #setPreferredDevicesForStrategy(AudioProductStrategy, List<AudioDeviceAttributes>)},
+     * {@link #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)},
+     * {@link #removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}
+     * or {@link #removePreferredDeviceForStrategy(AudioProductStrategy)} causes a change in
+     * non-default device(s). It will not be invoked directly after registration with
+     * {@link #addOnNonDefaultDevicesForStrategyChangedListener(
+     * Executor, OnNonDefaultDevicesForStrategyChangedListener)}
+     * to indicate which strategies had preferred devices at the time of registration.</p>
+     * @see #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)
+     * @see #removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)
      */
-    @GuardedBy("mPrefDevListenerLock")
-    private @Nullable ArrayList<PrefDevListenerInfo> mPrefDevListeners;
-
-    private static class PrefDevListenerInfo {
-        final @NonNull OnPreferredDevicesForStrategyChangedListener mListener;
-        final @NonNull Executor mExecutor;
-        PrefDevListenerInfo(OnPreferredDevicesForStrategyChangedListener listener, Executor exe) {
-            mListener = listener;
-            mExecutor = exe;
-        }
+    @SystemApi
+    public interface OnNonDefaultDevicesForStrategyChangedListener {
+        /**
+         * Called on the listener to indicate that the non-default audio devices for the given
+         * strategy has changed.
+         * @param strategy the {@link AudioProductStrategy} whose non-default device changed
+         * @param devices a list of newly set non-default audio devices
+         */
+        void onNonDefaultDevicesForStrategyChanged(@NonNull AudioProductStrategy strategy,
+                                                   @NonNull List<AudioDeviceAttributes> devices);
     }
 
-    @GuardedBy("mPrefDevListenerLock")
-    private StrategyPreferredDevicesDispatcherStub mPrefDevDispatcherStub;
+    /**
+     * @hide
+     * Adds a listener for being notified of changes to the non-default audio devices for
+     * strategies.
+     * @param executor
+     * @param listener
+     * @throws SecurityException if the caller doesn't hold the required permission
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public void addOnNonDefaultDevicesForStrategyChangedListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnNonDefaultDevicesForStrategyChangedListener listener)
+            throws SecurityException {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(listener);
+
+        mNonDefDevListenerMgr.addListener(
+                executor, listener, "addOnNonDefaultDevicesForStrategyChangedListener",
+                () -> new StrategyNonDefaultDevicesDispatcherStub());
+    }
+
+    /**
+     * @hide
+     * Removes a previously added listener of changes to the non-default audio device for
+     * strategies.
+     * @param listener
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public void removeOnNonDefaultDevicesForStrategyChangedListener(
+            @NonNull OnNonDefaultDevicesForStrategyChangedListener listener) {
+        Objects.requireNonNull(listener);
+        mNonDefDevListenerMgr.removeListener(
+                listener, "removeOnNonDefaultDevicesForStrategyChangedListener");
+    }
+
+    /**
+     * Manages the OnPreferredDevicesForStrategyChangedListener listeners and the
+     * StrategyPreferredDevicesDispatcherStub
+     */
+    private final CallbackUtil.LazyListenerManager<OnPreferredDevicesForStrategyChangedListener>
+            mPrefDevListenerMgr = new CallbackUtil.LazyListenerManager();
+
+    /**
+     * Manages the OnNonDefaultDevicesForStrategyChangedListener listeners and the
+     * StrategyNonDefaultDevicesDispatcherStub
+     */
+    private final CallbackUtil.LazyListenerManager<OnNonDefaultDevicesForStrategyChangedListener>
+            mNonDefDevListenerMgr = new CallbackUtil.LazyListenerManager();
 
     private final class StrategyPreferredDevicesDispatcherStub
-            extends IStrategyPreferredDevicesDispatcher.Stub {
+            extends IStrategyPreferredDevicesDispatcher.Stub
+            implements CallbackUtil.DispatcherStub {
 
         @Override
         public void dispatchPrefDevicesChanged(int strategyId,
                                                @NonNull List<AudioDeviceAttributes> devices) {
-            // make a shallow copy of listeners so callback is not executed under lock
-            final ArrayList<PrefDevListenerInfo> prefDevListeners;
-            synchronized (mPrefDevListenerLock) {
-                if (mPrefDevListeners == null || mPrefDevListeners.size() == 0) {
-                    return;
-                }
-                prefDevListeners = (ArrayList<PrefDevListenerInfo>) mPrefDevListeners.clone();
-            }
             final AudioProductStrategy strategy =
                     AudioProductStrategy.getAudioProductStrategyWithId(strategyId);
-            final long ident = Binder.clearCallingIdentity();
+
+            mPrefDevListenerMgr.callListeners(
+                    (listener) -> listener.onPreferredDevicesForStrategyChanged(strategy, devices));
+        }
+
+        @Override
+        public void register(boolean register) {
             try {
-                for (PrefDevListenerInfo info : prefDevListeners) {
-                    info.mExecutor.execute(() ->
-                            info.mListener.onPreferredDevicesForStrategyChanged(strategy, devices));
+                if (register) {
+                    getService().registerStrategyPreferredDevicesDispatcher(this);
+                } else {
+                    getService().unregisterStrategyPreferredDevicesDispatcher(this);
                 }
-            } finally {
-                Binder.restoreCallingIdentity(ident);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
             }
         }
     }
 
-    @GuardedBy("mPrefDevListenerLock")
-    private @Nullable PrefDevListenerInfo getPrefDevListenerInfo(
-            OnPreferredDevicesForStrategyChangedListener listener) {
-        if (mPrefDevListeners == null) {
-            return null;
+    private final class StrategyNonDefaultDevicesDispatcherStub
+            extends IStrategyNonDefaultDevicesDispatcher.Stub
+            implements CallbackUtil.DispatcherStub {
+
+        @Override
+        public void dispatchNonDefDevicesChanged(int strategyId,
+                                                 @NonNull List<AudioDeviceAttributes> devices) {
+            final AudioProductStrategy strategy =
+                    AudioProductStrategy.getAudioProductStrategyWithId(strategyId);
+
+            mNonDefDevListenerMgr.callListeners(
+                    (listener) -> listener.onNonDefaultDevicesForStrategyChanged(
+                            strategy, devices));
         }
-        for (PrefDevListenerInfo info : mPrefDevListeners) {
-            if (info.mListener == listener) {
-                return info;
+
+        @Override
+        public void register(boolean register) {
+            try {
+                if (register) {
+                    getService().registerStrategyNonDefaultDevicesDispatcher(this);
+                } else {
+                    getService().unregisterStrategyNonDefaultDevicesDispatcher(this);
+                }
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
             }
         }
-        return null;
-    }
-
-    @GuardedBy("mPrefDevListenerLock")
-    private boolean hasPrefDevListener(OnPreferredDevicesForStrategyChangedListener listener) {
-        return getPrefDevListenerInfo(listener) != null;
-    }
-
-    @GuardedBy("mPrefDevListenerLock")
-    /**
-     * @return true if the listener was removed from the list
-     */
-    private boolean removePrefDevListener(OnPreferredDevicesForStrategyChangedListener listener) {
-        final PrefDevListenerInfo infoToRemove = getPrefDevListenerInfo(listener);
-        if (infoToRemove != null) {
-            mPrefDevListeners.remove(infoToRemove);
-            return true;
-        }
-        return false;
     }
 
     //====================================================================
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index a743586..9339c3d 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -2063,12 +2063,46 @@
 
     /**
      * @hide
+     * Remove device as role for product strategy.
+     * @param strategy the id of the strategy to configure
+     * @param role the role of the devices
+     * @param devices the list of devices to be removed as role for the given strategy
+     * @return {@link #SUCCESS} if successfully set
+     */
+    public static int removeDevicesRoleForStrategy(
+            int strategy, int role, @NonNull List<AudioDeviceAttributes> devices) {
+        if (devices.isEmpty()) {
+            return BAD_VALUE;
+        }
+        int[] types = new int[devices.size()];
+        String[] addresses = new String[devices.size()];
+        for (int i = 0; i < devices.size(); ++i) {
+            types[i] = devices.get(i).getInternalType();
+            addresses[i] = devices.get(i).getAddress();
+        }
+        return removeDevicesRoleForStrategy(strategy, role, types, addresses);
+    }
+
+    /**
+     * @hide
      * Remove devices as role for the strategy
      * @param strategy the id of the strategy to configure
      * @param role the role of the devices
+     * @param types all device types
+     * @param addresses all device addresses
+     * @return {@link #SUCCESS} if successfully removed
+     */
+    public static native int removeDevicesRoleForStrategy(
+            int strategy, int role, @NonNull int[] types, @NonNull String[] addresses);
+
+    /**
+     * @hide
+     * Remove all devices as role for the strategy
+     * @param strategy the id of the strategy to configure
+     * @param role the role of the devices
      * @return {@link #SUCCESS} if successfully removed
      */
-    public static native int removeDevicesRoleForStrategy(int strategy, int role);
+    public static native int clearDevicesRoleForStrategy(int strategy, int role);
 
     /**
      * @hide
diff --git a/media/java/android/media/CallbackUtil.java b/media/java/android/media/CallbackUtil.java
index 2b5fd25..f0280da 100644
--- a/media/java/android/media/CallbackUtil.java
+++ b/media/java/android/media/CallbackUtil.java
@@ -183,7 +183,7 @@
 
         if (!removeListener(listener, listeners)) {
             throw new IllegalArgumentException("attempt to call " + methodName
-                    + "on an unregistered listener");
+                    + " on an unregistered listener");
         }
         if (listeners.size() == 0) {
             unregisterStub.accept(dispatchStub);
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 0f63cc4..4b36237 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -42,6 +42,7 @@
 import android.media.IRecordingConfigDispatcher;
 import android.media.IRingtonePlayer;
 import android.media.IStrategyPreferredDevicesDispatcher;
+import android.media.IStrategyNonDefaultDevicesDispatcher;
 import android.media.ISpatializerCallback;
 import android.media.ISpatializerHeadTrackerAvailableCallback;
 import android.media.ISpatializerHeadTrackingModeCallback;
@@ -330,7 +331,8 @@
 
     boolean isCallScreeningModeSupported();
 
-    int setPreferredDevicesForStrategy(in int strategy, in List<AudioDeviceAttributes> device);
+    @EnforcePermission("MODIFY_AUDIO_ROUTING")
+    int setPreferredDevicesForStrategy(in int strategy, in List<AudioDeviceAttributes> devices);
 
     @EnforcePermission("MODIFY_AUDIO_ROUTING")
     int removePreferredDevicesForStrategy(in int strategy);
@@ -338,6 +340,15 @@
     @EnforcePermission("MODIFY_AUDIO_ROUTING")
     List<AudioDeviceAttributes> getPreferredDevicesForStrategy(in int strategy);
 
+    @EnforcePermission("MODIFY_AUDIO_ROUTING")
+    int setDeviceAsNonDefaultForStrategy(in int strategy, in AudioDeviceAttributes device);
+
+    @EnforcePermission("MODIFY_AUDIO_ROUTING")
+    int removeDeviceAsNonDefaultForStrategy(in int strategy, in AudioDeviceAttributes device);
+
+    @EnforcePermission("MODIFY_AUDIO_ROUTING")
+    List<AudioDeviceAttributes> getNonDefaultDevicesForStrategy(in int strategy);
+
     List<AudioDeviceAttributes> getDevicesForAttributes(in AudioAttributes attributes);
 
     List<AudioDeviceAttributes> getDevicesForAttributesUnprotected(in AudioAttributes attributes);
@@ -351,6 +362,12 @@
     oneway void unregisterStrategyPreferredDevicesDispatcher(
             IStrategyPreferredDevicesDispatcher dispatcher);
 
+    void registerStrategyNonDefaultDevicesDispatcher(
+            IStrategyNonDefaultDevicesDispatcher dispatcher);
+
+    oneway void unregisterStrategyNonDefaultDevicesDispatcher(
+            IStrategyNonDefaultDevicesDispatcher dispatcher);
+
     oneway void setRttEnabled(in boolean rttEnabled);
 
     @EnforcePermission("MODIFY_AUDIO_ROUTING")
diff --git a/media/java/android/media/IStrategyNonDefaultDevicesDispatcher.aidl b/media/java/android/media/IStrategyNonDefaultDevicesDispatcher.aidl
new file mode 100644
index 0000000..59239cb
--- /dev/null
+++ b/media/java/android/media/IStrategyNonDefaultDevicesDispatcher.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 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.media;
+
+import android.media.AudioDeviceAttributes;
+
+/**
+ * AIDL for AudioService to signal non-daefault devices updates for audio strategies.
+ *
+ * {@hide}
+ */
+oneway interface IStrategyNonDefaultDevicesDispatcher {
+
+    void dispatchNonDefDevicesChanged(int strategyId, in List<AudioDeviceAttributes> devices);
+
+}
+
diff --git a/media/java/android/media/RouteListingPreference.java b/media/java/android/media/RouteListingPreference.java
index d74df7a..6a5b290 100644
--- a/media/java/android/media/RouteListingPreference.java
+++ b/media/java/android/media/RouteListingPreference.java
@@ -17,6 +17,7 @@
 package android.media;
 
 import android.annotation.IntDef;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -198,19 +199,22 @@
         @NonNull private final String mRouteId;
         @Flags private final int mFlags;
         @DisableReason private final int mDisableReason;
+        private final int mSessionParticipantCount;
 
         private Item(@NonNull Builder builder) {
             mRouteId = builder.mRouteId;
             mFlags = builder.mFlags;
             mDisableReason = builder.mDisableReason;
+            mSessionParticipantCount = builder.mSessionParticipantCount;
         }
 
         private Item(Parcel in) {
-            String routeId = in.readString();
-            Preconditions.checkArgument(!TextUtils.isEmpty(routeId));
-            mRouteId = routeId;
+            mRouteId = in.readString();
+            Preconditions.checkArgument(!TextUtils.isEmpty(mRouteId));
             mFlags = in.readInt();
             mDisableReason = in.readInt();
+            mSessionParticipantCount = in.readInt();
+            Preconditions.checkArgument(mSessionParticipantCount >= 0);
         }
 
         /** Returns the id of the route that corresponds to this route listing preference item. */
@@ -244,6 +248,17 @@
             return mDisableReason;
         }
 
+        /**
+         * Returns a non-negative number of participants in the ongoing session (if any) on the
+         * corresponding route.
+         *
+         * <p>The system ignores this value if zero, or if {@link #getFlags()} does not include
+         * {@link #FLAG_ONGOING_SESSION}.
+         */
+        public int getSessionParticipantCount() {
+            return mSessionParticipantCount;
+        }
+
         // Item Parcelable implementation.
 
         @Override
@@ -256,6 +271,7 @@
             dest.writeString(mRouteId);
             dest.writeInt(mFlags);
             dest.writeInt(mDisableReason);
+            dest.writeInt(mSessionParticipantCount);
         }
 
         // Equals and hashCode.
@@ -271,12 +287,13 @@
             Item item = (Item) other;
             return mRouteId.equals(item.mRouteId)
                     && mFlags == item.mFlags
-                    && mDisableReason == item.mDisableReason;
+                    && mDisableReason == item.mDisableReason
+                    && mSessionParticipantCount == item.mSessionParticipantCount;
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(mRouteId, mFlags, mDisableReason);
+            return Objects.hash(mRouteId, mFlags, mDisableReason, mSessionParticipantCount);
         }
 
         /** Builder for {@link Item}. */
@@ -285,6 +302,7 @@
             private final String mRouteId;
             private int mFlags;
             private int mDisableReason;
+            private int mSessionParticipantCount;
 
             /**
              * Constructor.
@@ -311,6 +329,17 @@
                 return this;
             }
 
+            /** See {@link Item#getSessionParticipantCount()}. */
+            @NonNull
+            public Builder setSessionParticipantCount(
+                    @IntRange(from = 0) int sessionParticipantCount) {
+                Preconditions.checkArgument(
+                        sessionParticipantCount >= 0,
+                        "sessionParticipantCount must be non-negative.");
+                mSessionParticipantCount = sessionParticipantCount;
+                return this;
+            }
+
             /** Creates and returns a new {@link Item} with the given parameters. */
             @NonNull
             public Item build() {
diff --git a/media/java/android/media/tv/tuner/filter/Filter.java b/media/java/android/media/tv/tuner/filter/Filter.java
index 8568c43..7e9443b 100644
--- a/media/java/android/media/tv/tuner/filter/Filter.java
+++ b/media/java/android/media/tv/tuner/filter/Filter.java
@@ -154,7 +154,8 @@
 
     /** @hide */
     @IntDef(prefix = "STATUS_",
-            value = {STATUS_DATA_READY, STATUS_LOW_WATER, STATUS_HIGH_WATER, STATUS_OVERFLOW})
+            value = {STATUS_DATA_READY, STATUS_LOW_WATER, STATUS_HIGH_WATER, STATUS_OVERFLOW,
+                    STATUS_NO_DATA})
     @Retention(RetentionPolicy.SOURCE)
     public @interface Status {}
 
@@ -183,6 +184,10 @@
      * discarded.
      */
     public static final int STATUS_OVERFLOW = DemuxFilterStatus.OVERFLOW;
+    /**
+     * The status of a filter that the filter buffer is empty and no filtered data is coming.
+     */
+    public static final int STATUS_NO_DATA = DemuxFilterStatus.NO_DATA;
 
     /** @hide */
     @IntDef(prefix = "SCRAMBLING_STATUS_",
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java
index 3e710e4..28353ab 100644
--- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java
@@ -22,6 +22,7 @@
 import android.os.Bundle;
 import android.os.PowerManager;
 import android.os.UserHandle;
+import android.provider.Settings;
 import android.provider.Settings.Global;
 import android.provider.Settings.Secure;
 import android.util.KeyValueListParser;
@@ -55,6 +56,10 @@
     public static final String EXTRA_POWER_SAVE_MODE_TRIGGER_LEVEL =
             "extra_power_save_mode_trigger_level";
 
+    /** Battery saver schedule keys. */
+    public static final String KEY_NO_SCHEDULE = "key_battery_saver_no_schedule";
+    public static final String KEY_PERCENTAGE = "key_battery_saver_percentage";
+
     private BatterySaverUtils() {
     }
 
@@ -108,7 +113,6 @@
      * - If it's 4th time through 8th time, show the schedule suggestion notification.
      *
      * @param enable true to enable battery saver.
-     *
      * @return true if the request succeeded.
      */
     public static synchronized boolean setPowerSaveMode(Context context,
@@ -154,10 +158,10 @@
      * Shows the battery saver confirmation warning if it hasn't been acknowledged by the user in
      * the past before. Various extras can be provided that will change the behavior of this
      * notification as well as the ui for it.
-     * @param context A valid context
-     * @param extras Any extras to include in the intent to trigger this confirmation that will
-     * help the system disambiguate what to show/do
      *
+     * @param context A valid context
+     * @param extras  Any extras to include in the intent to trigger this confirmation that will
+     *                help the system disambiguate what to show/do
      * @return True if it showed the notification because it has not been previously acknowledged.
      * @see #EXTRA_CONFIRM_TEXT_ONLY
      * @see #EXTRA_POWER_SAVE_MODE_TRIGGER
@@ -221,6 +225,7 @@
 
     /**
      * Reverts battery saver schedule mode to none if routine mode is selected.
+     *
      * @param context a valid context
      */
     public static void revertScheduleToNoneIfNeeded(Context context) {
@@ -233,4 +238,50 @@
                     PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
         }
     }
+
+    /**
+     * Gets battery saver schedule mode.
+     *
+     * @param context a valid context
+     * @return battery saver schedule key
+     */
+    public static String getBatterySaverScheduleKey(Context context) {
+        final ContentResolver resolver = context.getContentResolver();
+        final int mode = Settings.Global.getInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+                PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+        if (mode == PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE) {
+            final int threshold =
+                    Settings.Global.getInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
+            return threshold <= 0 ? KEY_NO_SCHEDULE : KEY_PERCENTAGE;
+        }
+        revertScheduleToNoneIfNeeded(context);
+        return KEY_NO_SCHEDULE;
+    }
+
+    /**
+     * Sets battery saver schedule mode.
+     *
+     * @param context      a valid context
+     * @param scheduleKey  {@link #KEY_NO_SCHEDULE} and {@link #KEY_PERCENTAGE}
+     * @param triggerLevel for automatic battery saver trigger level
+     */
+    public static void setBatterySaverScheduleMode(Context context, String scheduleKey,
+            int triggerLevel) {
+        final ContentResolver resolver = context.getContentResolver();
+        switch (scheduleKey) {
+            case KEY_NO_SCHEDULE:
+                Settings.Global.putInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+                        PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+                Settings.Global.putInt(resolver, Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
+                break;
+            case KEY_PERCENTAGE:
+                Settings.Global.putInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+                        PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+                Settings.Global.putInt(resolver,
+                        Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, triggerLevel);
+                break;
+            default:
+                throw new IllegalStateException("Not a valid schedule key");
+        }
+    }
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java
index 2bb3c2a..a15fe9f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java
@@ -16,6 +16,9 @@
 
 package com.android.settingslib.fuelgauge;
 
+import static com.android.settingslib.fuelgauge.BatterySaverUtils.KEY_NO_SCHEDULE;
+import static com.android.settingslib.fuelgauge.BatterySaverUtils.KEY_PERCENTAGE;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
@@ -186,4 +189,46 @@
         assertThat(Secure.getInt(mMockResolver, Secure.SUPPRESS_AUTO_BATTERY_SAVER_SUGGESTION, -1))
                 .isEqualTo(1);
     }
+
+    @Test
+    public void testGetBatterySaverScheduleKey_returnExpectedKey() {
+        Global.putInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
+        Global.putInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+                PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+
+        assertThat(BatterySaverUtils.getBatterySaverScheduleKey(mMockContext)).isEqualTo(
+                KEY_NO_SCHEDULE);
+
+        Global.putInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 20);
+        Global.putInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+                PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+
+        assertThat(BatterySaverUtils.getBatterySaverScheduleKey(mMockContext)).isEqualTo(
+                KEY_PERCENTAGE);
+
+        Global.putInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 20);
+        Global.putInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE,
+                PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC);
+
+        assertThat(BatterySaverUtils.getBatterySaverScheduleKey(mMockContext)).isEqualTo(
+                KEY_NO_SCHEDULE);
+    }
+
+    @Test
+    public void testSetBatterySaverScheduleMode_setSchedule() {
+        BatterySaverUtils.setBatterySaverScheduleMode(mMockContext, KEY_NO_SCHEDULE, -1);
+
+        assertThat(Global.getInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE, -1))
+                .isEqualTo(PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+        assertThat(Global.getInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, -1))
+                .isEqualTo(0);
+
+        BatterySaverUtils.setBatterySaverScheduleMode(mMockContext, KEY_PERCENTAGE, 20);
+
+        assertThat(Global.getInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE, -1))
+                .isEqualTo(PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
+        assertThat(Global.getInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, -1))
+                .isEqualTo(20);
+
+    }
 }
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index e1000e0..220c16a 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -31,6 +31,52 @@
     ],
 }
 
+// Opt-in configuration for code depending on Jetpack Compose.
+soong_config_module_type {
+    name: "systemui_compose_java_defaults",
+    module_type: "java_defaults",
+    config_namespace: "ANDROID",
+    bool_variables: ["SYSTEMUI_USE_COMPOSE"],
+    properties: [
+        "srcs",
+        "static_libs",
+    ],
+}
+
+systemui_compose_java_defaults {
+    name: "SystemUI_compose_defaults",
+    soong_config_variables: {
+        SYSTEMUI_USE_COMPOSE: {
+            // Because files in compose/features/ depend on SystemUI
+            // code, we compile those files when compiling SystemUI-core.
+            // We also compile the ComposeFacade in
+            // compose/facade/enabled/.
+            srcs: [
+                "compose/features/src/**/*.kt",
+                "compose/facade/enabled/src/**/*.kt",
+            ],
+
+            // The dependencies needed by SystemUIComposeFeatures,
+            // except for SystemUI-core.
+            // Copied from compose/features/Android.bp.
+            static_libs: [
+                "SystemUIComposeCore",
+
+                "androidx.compose.runtime_runtime",
+                "androidx.compose.material3_material3",
+                "androidx.activity_activity-compose",
+            ],
+
+            // By default, Compose is disabled and we compile the ComposeFacade
+            // in compose/facade/disabled/.
+            conditions_default: {
+                srcs: ["compose/facade/disabled/src/**/*.kt"],
+                static_libs: [],
+            },
+        },
+    },
+}
+
 java_library {
     name: "SystemUI-proto",
 
@@ -68,6 +114,9 @@
 
 android_library {
     name: "SystemUI-core",
+    defaults: [
+        "SystemUI_compose_defaults",
+    ],
     srcs: [
         "src/**/*.kt",
         "src/**/*.java",
@@ -228,6 +277,9 @@
 
 android_library {
     name: "SystemUI-tests",
+    defaults: [
+        "SystemUI_compose_defaults",
+    ],
     manifest: "tests/AndroidManifest-base.xml",
     additional_manifests: ["tests/AndroidManifest.xml"],
     srcs: [
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/runtime/MovableContent.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/runtime/MovableContent.kt
new file mode 100644
index 0000000..3f2f96b
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/runtime/MovableContent.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 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.compose.runtime
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.InternalComposeApi
+import androidx.compose.runtime.MovableContent
+import androidx.compose.runtime.currentComposer
+
+/**
+ * An overload of [androidx.compose.runtime.movableContentOf] with 5 parameters.
+ *
+ * @see androidx.compose.runtime.movableContentOf
+ */
+@OptIn(InternalComposeApi::class)
+fun <P1, P2, P3, P4, P5> movableContentOf(
+    content: @Composable (P1, P2, P3, P4, P5) -> Unit
+): @Composable (P1, P2, P3, P4, P5) -> Unit {
+    val movableContent =
+        MovableContent<Pair<Triple<P1, P2, P3>, Pair<P4, P5>>> {
+            content(
+                it.first.first,
+                it.first.second,
+                it.first.third,
+                it.second.first,
+                it.second.second,
+            )
+        }
+    return { p1, p2, p3, p4, p5 ->
+        currentComposer.insertMovableContent(movableContent, Triple(p1, p2, p3) to (p4 to p5))
+    }
+}
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
new file mode 100644
index 0000000..6e728ce
--- /dev/null
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 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.compose
+
+import androidx.activity.ComponentActivity
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+
+/** The Compose facade, when Compose is *not* available. */
+object ComposeFacade : BaseComposeFacade {
+    override fun isComposeAvailable(): Boolean = false
+
+    override fun setPeopleSpaceActivityContent(
+        activity: ComponentActivity,
+        viewModel: PeopleViewModel,
+        onResult: (PeopleViewModel.Result) -> Unit,
+    ) {
+        throwComposeUnavailableError()
+    }
+
+    private fun throwComposeUnavailableError() {
+        error(
+            "Compose is not available. Make sure to check isComposeAvailable() before calling any" +
+                " other function on ComposeFacade."
+        )
+    }
+}
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
new file mode 100644
index 0000000..16294d9
--- /dev/null
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 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.compose
+
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import com.android.systemui.compose.theme.SystemUITheme
+import com.android.systemui.people.ui.compose.PeopleScreen
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+
+/** The Compose facade, when Compose is available. */
+object ComposeFacade : BaseComposeFacade {
+    override fun isComposeAvailable(): Boolean = true
+
+    override fun setPeopleSpaceActivityContent(
+        activity: ComponentActivity,
+        viewModel: PeopleViewModel,
+        onResult: (PeopleViewModel.Result) -> Unit,
+    ) {
+        activity.setContent { SystemUITheme { PeopleScreen(viewModel, onResult) } }
+    }
+}
diff --git a/packages/SystemUI/compose/features/Android.bp b/packages/SystemUI/compose/features/Android.bp
index 325ede6..4533330 100644
--- a/packages/SystemUI/compose/features/Android.bp
+++ b/packages/SystemUI/compose/features/Android.bp
@@ -35,6 +35,7 @@
 
         "androidx.compose.runtime_runtime",
         "androidx.compose.material3_material3",
+        "androidx.activity_activity-compose",
     ],
 
     kotlincflags: ["-Xjvm-default=all"],
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
index 2aac46e..4a56b02 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
@@ -139,11 +139,20 @@
                     bottom = PeopleSpacePadding,
                     start = 8.dp,
                     end = 8.dp,
-                )
+                ),
         ) {
-            ConversationList(R.string.priority_conversations, priorityTiles, onTileClicked)
-            item { Spacer(Modifier.height(35.dp)) }
-            ConversationList(R.string.recent_conversations, recentTiles, onTileClicked)
+            val hasPriorityConversations = priorityTiles.isNotEmpty()
+            if (hasPriorityConversations) {
+                ConversationList(R.string.priority_conversations, priorityTiles, onTileClicked)
+            }
+
+            if (recentTiles.isNotEmpty()) {
+                if (hasPriorityConversations) {
+                    item { Spacer(Modifier.height(35.dp)) }
+                }
+
+                ConversationList(R.string.recent_conversations, recentTiles, onTileClicked)
+            }
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
new file mode 100644
index 0000000..e5ec727
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2022 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.compose
+
+import androidx.activity.ComponentActivity
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+
+/**
+ * A facade to interact with Compose, when it is available.
+ *
+ * You should access this facade by calling the static methods on
+ * [com.android.systemui.compose.ComposeFacade] directly.
+ */
+interface BaseComposeFacade {
+    /**
+     * Whether Compose is currently available. This function should be checked before calling any
+     * other functions on this facade.
+     *
+     * This value will never change at runtime.
+     */
+    fun isComposeAvailable(): Boolean
+
+    /** Bind the content of [activity] to [viewModel]. */
+    fun setPeopleSpaceActivityContent(
+        activity: ComponentActivity,
+        viewModel: PeopleViewModel,
+        onResult: (PeopleViewModel.Result) -> Unit,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 96707f4..59f68f7 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -44,6 +44,7 @@
 import com.android.systemui.statusbar.notification.fsi.FsiChromeRepo
 import com.android.systemui.statusbar.notification.InstantAppNotifier
 import com.android.systemui.statusbar.notification.fsi.FsiChromeViewModelFactory
+import com.android.systemui.statusbar.notification.fsi.FsiChromeViewBinder
 import com.android.systemui.statusbar.phone.KeyguardLiftController
 import com.android.systemui.stylus.StylusUsiPowerStartable
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
@@ -94,6 +95,12 @@
     @ClassKey(FsiChromeViewModelFactory::class)
     abstract fun bindFSIChromeWindowViewModel(sysui: FsiChromeViewModelFactory): CoreStartable
 
+    /** Inject into FsiChromeWindowBinder.  */
+    @Binds
+    @IntoMap
+    @ClassKey(FsiChromeViewBinder::class)
+    abstract fun bindFsiChromeWindowBinder(sysui: FsiChromeViewBinder): CoreStartable
+
     /** Inject into GarbageMonitor.Service.  */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
index 7cc95a1..fba5f63 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
@@ -27,11 +27,15 @@
 import androidx.activity.ComponentActivity;
 import androidx.lifecycle.ViewModelProvider;
 
+import com.android.systemui.compose.ComposeFacade;
 import com.android.systemui.people.ui.view.PeopleViewBinder;
 import com.android.systemui.people.ui.viewmodel.PeopleViewModel;
 
 import javax.inject.Inject;
 
+import kotlin.Unit;
+import kotlin.jvm.functions.Function1;
+
 /** People Tile Widget configuration activity that shows the user their conversation tiles. */
 public class PeopleSpaceActivity extends ComponentActivity {
 
@@ -58,13 +62,18 @@
         int widgetId = getIntent().getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID);
         viewModel.onWidgetIdChanged(widgetId);
 
-        ViewGroup view = PeopleViewBinder.create(this);
-        PeopleViewBinder.bind(view, viewModel, /* lifecycleOwner= */ this,
-                (result) -> {
-                    finishActivity(result);
-                    return null;
-                });
-        setContentView(view);
+        Function1<PeopleViewModel.Result, Unit> onResult = (result) -> {
+            finishActivity(result);
+            return null;
+        };
+
+        if (ComposeFacade.INSTANCE.isComposeAvailable()) {
+            ComposeFacade.INSTANCE.setPeopleSpaceActivityContent(this, viewModel, onResult);
+        } else {
+            ViewGroup view = PeopleViewBinder.create(this);
+            PeopleViewBinder.bind(view, viewModel, /* lifecycleOwner= */ this, onResult);
+            setContentView(view);
+        }
     }
 
     private void finishActivity(PeopleViewModel.Result result) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewBinder.kt
new file mode 100644
index 0000000..1a3927b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewBinder.kt
@@ -0,0 +1,99 @@
+package com.android.systemui.statusbar.notification.fsi
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.WindowManager
+import com.android.systemui.CoreStartable
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.statusbar.notification.fsi.FsiDebug.Companion.log
+import com.android.systemui.statusbar.phone.CentralSurfaces
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+@SysUISingleton
+class FsiChromeViewBinder
+@Inject
+constructor(
+    val context: Context,
+    val windowManager: WindowManager,
+    val viewModelFactory: FsiChromeViewModelFactory,
+    val layoutInflater: LayoutInflater,
+    val centralSurfaces: CentralSurfaces,
+    @Main val mainExecutor: Executor,
+    @Application val scope: CoroutineScope,
+) : CoreStartable {
+
+    companion object {
+        private const val classTag = "FsiChromeViewBinder"
+    }
+
+    private val fsiChromeView =
+        layoutInflater.inflate(R.layout.fsi_chrome_view, null /* root */, false /* attachToRoot */)
+            as FsiChromeView
+
+    var addedToWindowManager = false
+    var cornerRadius: Int = context.resources.getDimensionPixelSize(
+            R.dimen.notification_corner_radius)
+
+    override fun start() {
+        val methodTag = "start"
+        log("$classTag $methodTag ")
+
+        scope.launch {
+            log("$classTag $methodTag launch ")
+            viewModelFactory.viewModelFlow.collect { vm -> updateForViewModel(vm) }
+        }
+    }
+
+    private fun updateForViewModel(vm: FsiChromeViewModel?) {
+        val methodTag = "updateForViewModel"
+
+        if (vm == null) {
+            log("$classTag $methodTag viewModel is null, removing from window manager")
+
+            if (addedToWindowManager) {
+                windowManager.removeView(fsiChromeView)
+                addedToWindowManager = false
+            }
+            return
+        }
+
+        bindViewModel(vm, windowManager)
+
+        if (addedToWindowManager) {
+            log("$classTag $methodTag already addedToWindowManager")
+        } else {
+            windowManager.addView(fsiChromeView, FsiTaskViewConfig.getWmLayoutParams("PackageName"))
+            addedToWindowManager = true
+        }
+    }
+
+    private fun bindViewModel(
+        vm: FsiChromeViewModel,
+        windowManager: WindowManager,
+    ) {
+        log("$classTag bindViewModel")
+
+        fsiChromeView.appIconImageView.setImageDrawable(vm.appIcon)
+        fsiChromeView.appNameTextView.text = vm.appName
+
+        fsiChromeView.dismissButton.setOnClickListener { vm.onDismiss() }
+        fsiChromeView.fullscreenButton.setOnClickListener { vm.onFullscreen() }
+
+        vm.taskView.cornerRadius = cornerRadius.toFloat()
+        vm.taskView.startActivity(
+            vm.fsi,
+            FsiTaskViewConfig.getFillInIntent(),
+            FsiTaskViewConfig.getActivityOptions(context, windowManager),
+            FsiTaskViewConfig.getLaunchBounds(windowManager)
+        )
+
+        log("$classTag bindViewModel started taskview activity")
+        fsiChromeView.addView(vm.taskView)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiTaskViewConfig.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiTaskViewConfig.kt
new file mode 100644
index 0000000..034ab56
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiTaskViewConfig.kt
@@ -0,0 +1,75 @@
+package com.android.systemui.statusbar.notification.fsi
+
+import android.app.ActivityOptions
+import android.content.Context
+import android.content.Intent
+import android.graphics.PixelFormat
+import android.graphics.Rect
+import android.os.Binder
+import android.view.ViewGroup
+import android.view.WindowManager
+
+/**
+ * Config for adding the FsiChromeView window to WindowManager and starting the FSI activity.
+ */
+class FsiTaskViewConfig {
+
+    companion object {
+
+        private const val classTag = "FsiTaskViewConfig"
+
+        fun getWmLayoutParams(packageName: String): WindowManager.LayoutParams {
+            val params: WindowManager.LayoutParams?
+            params =
+                WindowManager.LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.MATCH_PARENT,
+                    WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+                    WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
+                        WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED or
+                            WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER,
+                    PixelFormat.TRANSLUCENT
+                )
+            params.setTrustedOverlay()
+            params.fitInsetsTypes = 0
+            params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+            params.token = Binder()
+            params.packageName = packageName
+            params.layoutInDisplayCutoutMode =
+                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+            params.privateFlags =
+                params.privateFlags or WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
+            return params
+        }
+
+        fun getFillInIntent(): Intent {
+            val fillInIntent = Intent()
+            fillInIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
+            fillInIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
+            // FLAG_ACTIVITY_NEW_TASK is auto-applied because
+            // we're starting the FSI activity from a non-Activity context
+            return fillInIntent
+        }
+
+        fun getLaunchBounds(windowManager: WindowManager): Rect {
+            // TODO(b/243421660) check this works for non-resizeable activity
+            return Rect()
+        }
+
+        fun getActivityOptions(context: Context, windowManager: WindowManager): ActivityOptions {
+            // Custom options so there is no activity transition animation
+            val options =
+                ActivityOptions.makeCustomAnimation(context, 0 /* enterResId */, 0 /* exitResId */)
+
+            options.taskAlwaysOnTop = true
+
+            options.pendingIntentLaunchFlags =
+                Intent.FLAG_ACTIVITY_NEW_DOCUMENT or
+                    Intent.FLAG_ACTIVITY_MULTIPLE_TASK or
+                    Intent.FLAG_ACTIVITY_NEW_TASK
+
+            options.launchBounds = getLaunchBounds(windowManager)
+            return options
+        }
+    }
+}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 4d173d6..cdd5471 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -22,9 +22,9 @@
 import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
 
+import android.annotation.EnforcePermission;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.RequiresPermission;
 import android.annotation.StringRes;
 import android.app.Activity;
 import android.app.ActivityOptions;
@@ -62,6 +62,7 @@
 import android.os.IBinder;
 import android.os.LocaleList;
 import android.os.Looper;
+import android.os.PermissionEnforcer;
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
@@ -198,6 +199,7 @@
             IVirtualDeviceActivityListener activityListener,
             Consumer<ArraySet<Integer>> runningAppsChangedCallback,
             VirtualDeviceParams params) {
+        super(PermissionEnforcer.fromContext(context));
         UserHandle ownerUserHandle = UserHandle.getUserHandleForUid(ownerUid);
         mContext = context.createContextAsUser(ownerUserHandle, 0);
         mAssociationInfo = associationInfo;
@@ -337,11 +339,9 @@
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void close() {
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
-                "Permission required to close the virtual device");
-
+        super.close_enforcePermission();
         synchronized (mVirtualDeviceLock) {
             if (!mPerDisplayWakelocks.isEmpty()) {
                 mPerDisplayWakelocks.forEach((displayId, wakeLock) -> {
@@ -389,14 +389,12 @@
         return mWindowPolicyControllers;
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void onAudioSessionStarting(int displayId,
             @NonNull IAudioRoutingCallback routingCallback,
             @Nullable IAudioConfigChangedCallback configChangedCallback) {
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
-                "Permission required to start audio session");
+        super.onAudioSessionStarting_enforcePermission();
         synchronized (mVirtualDeviceLock) {
             if (!mVirtualDisplayIds.contains(displayId)) {
                 throw new SecurityException(
@@ -413,12 +411,10 @@
         }
     }
 
-    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void onAudioSessionEnded() {
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
-                "Permission required to stop audio session");
+        super.onAudioSessionEnded_enforcePermission();
         synchronized (mVirtualDeviceLock) {
             if (mVirtualAudioController != null) {
                 mVirtualAudioController.stopListening();
@@ -428,9 +424,9 @@
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void createVirtualDpad(VirtualDpadConfig config, @NonNull IBinder deviceToken) {
-        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
-                "Permission required to create a virtual dpad");
+        super.createVirtualDpad_enforcePermission();
         synchronized (mVirtualDeviceLock) {
             if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
                 throw new SecurityException(
@@ -448,9 +444,9 @@
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void createVirtualKeyboard(VirtualKeyboardConfig config, @NonNull IBinder deviceToken) {
-        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
-                "Permission required to create a virtual keyboard");
+        super.createVirtualKeyboard_enforcePermission();
         synchronized (mVirtualDeviceLock) {
             if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
                 throw new SecurityException(
@@ -470,9 +466,9 @@
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void createVirtualMouse(VirtualMouseConfig config, @NonNull IBinder deviceToken) {
-        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
-                "Permission required to create a virtual mouse");
+        super.createVirtualMouse_enforcePermission();
         synchronized (mVirtualDeviceLock) {
             if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
                 throw new SecurityException(
@@ -490,10 +486,10 @@
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void createVirtualTouchscreen(VirtualTouchscreenConfig config,
             @NonNull IBinder deviceToken) {
-        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
-                "Permission required to create a virtual touchscreen");
+        super.createVirtualTouchscreen_enforcePermission();
         synchronized (mVirtualDeviceLock) {
             if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
                 throw new SecurityException(
@@ -501,30 +497,29 @@
                                 + "this virtual device");
             }
         }
-        int screenHeightPixels = config.getHeightInPixels();
-        int screenWidthPixels = config.getWidthInPixels();
-        if (screenHeightPixels <= 0 || screenWidthPixels <= 0) {
+        int screenHeight = config.getHeight();
+        int screenWidth = config.getWidth();
+        if (screenHeight <= 0 || screenWidth <= 0) {
             throw new IllegalArgumentException(
                     "Cannot create a virtual touchscreen, screen dimensions must be positive. Got: "
-                            + "(" + screenWidthPixels + ", " + screenHeightPixels + ")");
+                            + "(" + screenWidth + ", " + screenHeight + ")");
         }
 
         final long ident = Binder.clearCallingIdentity();
         try {
             mInputController.createTouchscreen(config.getInputDeviceName(), config.getVendorId(),
                     config.getProductId(), deviceToken, config.getAssociatedDisplayId(),
-                    screenHeightPixels, screenWidthPixels);
+                    screenHeight, screenWidth);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void createVirtualNavigationTouchpad(VirtualNavigationTouchpadConfig config,
             @NonNull IBinder deviceToken) {
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
-                "Permission required to create a virtual navigation touchpad");
+        super.createVirtualNavigationTouchpad_enforcePermission();
         synchronized (mVirtualDeviceLock) {
             if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) {
                 throw new SecurityException(
@@ -552,11 +547,9 @@
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void unregisterInputDevice(IBinder token) {
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
-                "Permission required to unregister this input device");
-
+        super.unregisterInputDevice_enforcePermission();
         final long ident = Binder.clearCallingIdentity();
         try {
             mInputController.unregisterInputDevice(token);
@@ -577,7 +570,9 @@
 
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendDpadKeyEvent(IBinder token, VirtualKeyEvent event) {
+        super.sendDpadKeyEvent_enforcePermission();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendDpadKeyEvent(token, event);
@@ -587,7 +582,9 @@
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendKeyEvent(IBinder token, VirtualKeyEvent event) {
+        super.sendKeyEvent_enforcePermission();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendKeyEvent(token, event);
@@ -597,7 +594,9 @@
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendButtonEvent(IBinder token, VirtualMouseButtonEvent event) {
+        super.sendButtonEvent_enforcePermission();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendButtonEvent(token, event);
@@ -607,7 +606,9 @@
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendTouchEvent(IBinder token, VirtualTouchEvent event) {
+        super.sendTouchEvent_enforcePermission();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendTouchEvent(token, event);
@@ -617,7 +618,9 @@
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendRelativeEvent(IBinder token, VirtualMouseRelativeEvent event) {
+        super.sendRelativeEvent_enforcePermission();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendRelativeEvent(token, event);
@@ -627,7 +630,9 @@
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendScrollEvent(IBinder token, VirtualMouseScrollEvent event) {
+        super.sendScrollEvent_enforcePermission();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mInputController.sendScrollEvent(token, event);
@@ -647,11 +652,9 @@
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void setShowPointerIcon(boolean showPointerIcon) {
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
-                "Permission required to unregister this input device");
-
+        super.setShowPointerIcon_enforcePermission();
         final long ident = Binder.clearCallingIdentity();
         try {
             synchronized (mVirtualDeviceLock) {
@@ -666,12 +669,11 @@
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void createVirtualSensor(
             @NonNull IBinder deviceToken,
             @NonNull VirtualSensorConfig config) {
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
-                "Permission required to create a virtual sensor");
+        super.createVirtualSensor_enforcePermission();
         Objects.requireNonNull(config);
         Objects.requireNonNull(deviceToken);
         final long ident = Binder.clearCallingIdentity();
@@ -683,10 +685,9 @@
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void unregisterSensor(@NonNull IBinder token) {
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
-                "Permission required to unregister a virtual sensor");
+        super.unregisterSensor_enforcePermission();
         final long ident = Binder.clearCallingIdentity();
         try {
             mSensorController.unregisterSensor(token);
@@ -696,10 +697,9 @@
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public boolean sendSensorEvent(@NonNull IBinder token, @NonNull VirtualSensorEvent event) {
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
-                "Permission required to send a virtual sensor event");
+        super.sendSensorEvent_enforcePermission();
         final long ident = Binder.clearCallingIdentity();
         try {
             return mSensorController.sendSensorEvent(token, event);
@@ -709,25 +709,23 @@
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void registerIntentInterceptor(IVirtualDeviceIntentInterceptor intentInterceptor,
             IntentFilter filter) {
+        super.registerIntentInterceptor_enforcePermission();
         Objects.requireNonNull(intentInterceptor);
         Objects.requireNonNull(filter);
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
-                "Permission required to register intent interceptor");
         synchronized (mVirtualDeviceLock) {
             mIntentInterceptors.put(intentInterceptor.asBinder(), filter);
         }
     }
 
     @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public void unregisterIntentInterceptor(
             @NonNull IVirtualDeviceIntentInterceptor intentInterceptor) {
+        super.unregisterIntentInterceptor_enforcePermission();
         Objects.requireNonNull(intentInterceptor);
-        mContext.enforceCallingOrSelfPermission(
-                android.Manifest.permission.CREATE_VIRTUAL_DEVICE,
-                "Permission required to unregister intent interceptor");
         synchronized (mVirtualDeviceLock) {
             mIntentInterceptors.remove(intentInterceptor.asBinder());
         }
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index d53f8fb..b89084c 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -42,6 +42,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayDeque;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Objects;
 
 /**
@@ -124,6 +125,12 @@
     private final ArrayDeque<SomeArgs> mPendingOffload = new ArrayDeque<>(4);
 
     /**
+     * List of all queues holding broadcasts that are waiting to be dispatched.
+     */
+    private final List<ArrayDeque<SomeArgs>> mPendingQueues = List.of(
+            mPendingUrgent, mPending, mPendingOffload);
+
+    /**
      * Broadcast actively being dispatched to this process.
      */
     private @Nullable BroadcastRecord mActive;
@@ -218,11 +225,11 @@
      * given count of other receivers have reached a terminal state; typically
      * used for ordered broadcasts and priority traunches.
      */
-    public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex) {
+    public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex,
+            @NonNull BroadcastConsumer replacedBroadcastConsumer) {
         if (record.isReplacePending()) {
-            boolean didReplace = replaceBroadcastInQueue(mPending, record, recordIndex)
-                    || replaceBroadcastInQueue(mPendingUrgent, record, recordIndex)
-                    || replaceBroadcastInQueue(mPendingOffload, record, recordIndex);
+            final boolean didReplace = replaceBroadcast(record, recordIndex,
+                    replacedBroadcastConsumer);
             if (didReplace) {
                 return;
             }
@@ -243,6 +250,26 @@
     }
 
     /**
+     * Searches from newest to oldest in the pending broadcast queues, and at the first matching
+     * pending broadcast it finds, replaces it in-place and returns -- does not attempt to handle
+     * "duplicate" broadcasts in the queue.
+     * <p>
+     * @return {@code true} if it found and replaced an existing record in the queue;
+     * {@code false} otherwise.
+     */
+    private boolean replaceBroadcast(@NonNull BroadcastRecord record, int recordIndex,
+            @NonNull BroadcastConsumer replacedBroadcastConsumer) {
+        final int count = mPendingQueues.size();
+        for (int i = 0; i < count; ++i) {
+            final ArrayDeque<SomeArgs> queue = mPendingQueues.get(i);
+            if (replaceBroadcastInQueue(queue, record, recordIndex, replacedBroadcastConsumer)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * Searches from newest to oldest, and at the first matching pending broadcast
      * it finds, replaces it in-place and returns -- does not attempt to handle
      * "duplicate" broadcasts in the queue.
@@ -251,7 +278,8 @@
      * {@code false} otherwise.
      */
     private boolean replaceBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue,
-            @NonNull BroadcastRecord record, int recordIndex) {
+            @NonNull BroadcastRecord record, int recordIndex,
+            @NonNull BroadcastConsumer replacedBroadcastConsumer) {
         final Iterator<SomeArgs> it = queue.descendingIterator();
         final Object receiver = record.receivers.get(recordIndex);
         while (it.hasNext()) {
@@ -262,12 +290,14 @@
             if ((record.callingUid == testRecord.callingUid)
                     && (record.userId == testRecord.userId)
                     && record.intent.filterEquals(testRecord.intent)
-                    && isReceiverEquals(receiver, testReceiver)) {
+                    && isReceiverEquals(receiver, testReceiver)
+                    && testRecord.allReceiversPending()) {
                 // Exact match found; perform in-place swap
                 args.arg1 = record;
                 args.argi1 = recordIndex;
                 onBroadcastDequeued(testRecord, testRecordIndex);
                 onBroadcastEnqueued(record, recordIndex);
+                replacedBroadcastConsumer.accept(testRecord, testRecordIndex);
                 return true;
             }
         }
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index a850c8a..8f241b2 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -64,6 +64,7 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.text.format.DateUtils;
+import android.util.ArraySet;
 import android.util.IndentingPrintWriter;
 import android.util.MathUtils;
 import android.util.Pair;
@@ -629,30 +630,26 @@
 
         applyDeliveryGroupPolicy(r);
 
-        if (r.isReplacePending()) {
-            // Leave the skipped broadcasts intact in queue, so that we can
-            // replace them at their current position during enqueue below
-            forEachMatchingBroadcast(QUEUE_PREDICATE_ANY, (testRecord, testIndex) -> {
-                // We only allow caller to replace broadcasts they enqueued
-                return (r.callingUid == testRecord.callingUid)
-                        && (r.userId == testRecord.userId)
-                        && r.intent.filterEquals(testRecord.intent);
-            }, mBroadcastConsumerSkipAndCanceled, false);
-        }
-
         r.enqueueTime = SystemClock.uptimeMillis();
         r.enqueueRealTime = SystemClock.elapsedRealtime();
         r.enqueueClockTime = System.currentTimeMillis();
 
+        final ArraySet<BroadcastRecord> replacedBroadcasts = new ArraySet<>();
+        final BroadcastConsumer replacedBroadcastConsumer =
+                (record, i) -> replacedBroadcasts.add(record);
         for (int i = 0; i < r.receivers.size(); i++) {
             final Object receiver = r.receivers.get(i);
             final BroadcastProcessQueue queue = getOrCreateProcessQueue(
                     getReceiverProcessName(receiver), getReceiverUid(receiver));
-            queue.enqueueOrReplaceBroadcast(r, i);
+            queue.enqueueOrReplaceBroadcast(r, i, replacedBroadcastConsumer);
             updateRunnableList(queue);
             enqueueUpdateRunningList();
         }
 
+        // Skip any broadcasts that have been replaced by newer broadcasts with
+        // FLAG_RECEIVER_REPLACE_PENDING.
+        skipAndCancelReplacedBroadcasts(replacedBroadcasts);
+
         // If nothing to dispatch, send any pending result immediately
         if (r.receivers.isEmpty()) {
             scheduleResultTo(r);
@@ -662,6 +659,17 @@
         traceEnd(cookie);
     }
 
+    private void skipAndCancelReplacedBroadcasts(ArraySet<BroadcastRecord> replacedBroadcasts) {
+        for (int i = 0; i < replacedBroadcasts.size(); ++i) {
+            final BroadcastRecord r = replacedBroadcasts.valueAt(i);
+            r.resultCode = Activity.RESULT_CANCELED;
+            r.resultData = null;
+            r.resultExtras = null;
+            scheduleResultTo(r);
+            notifyFinishBroadcast(r);
+        }
+    }
+
     private void applyDeliveryGroupPolicy(@NonNull BroadcastRecord r) {
         if (mService.shouldIgnoreDeliveryGroupPolicy(r.intent.getAction())) {
             return;
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index 24cf3d2..37225d1 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -907,6 +907,17 @@
         return record.options == null ? null : record.options.getDeliveryGroupMatchingFilter();
     }
 
+    /**
+     * Returns {@code true} if all the receivers are still waiting to receive the broadcast.
+     * Otherwise {@code false}.
+     */
+    boolean allReceiversPending() {
+        // We could also count the number of receivers with deliver state DELIVERY_PENDING, but
+        // checking how many receivers have finished (either skipped or cancelled) and whether or
+        // not the dispatch has been started should be sufficient.
+        return (terminalCount == 0 && dispatchTime <= 0);
+    }
+
     @Override
     public String toString() {
         if (mCachedToString == null) {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 418027f..9877ed3 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -36,6 +36,7 @@
 import android.media.IAudioRoutesObserver;
 import android.media.ICapturePresetDevicesRoleDispatcher;
 import android.media.ICommunicationDeviceDispatcher;
+import android.media.IStrategyNonDefaultDevicesDispatcher;
 import android.media.IStrategyPreferredDevicesDispatcher;
 import android.media.MediaMetrics;
 import android.media.audiopolicy.AudioProductStrategy;
@@ -871,6 +872,16 @@
         return mDeviceInventory.removePreferredDevicesForStrategySync(strategy);
     }
 
+    /*package*/ int setDeviceAsNonDefaultForStrategySync(int strategy,
+            @NonNull AudioDeviceAttributes device) {
+        return mDeviceInventory.setDeviceAsNonDefaultForStrategySync(strategy, device);
+    }
+
+    /*package*/ int removeDeviceAsNonDefaultForStrategySync(int strategy,
+            @NonNull AudioDeviceAttributes device) {
+        return mDeviceInventory.removeDeviceAsNonDefaultForStrategySync(strategy, device);
+    }
+
     /*package*/ void registerStrategyPreferredDevicesDispatcher(
             @NonNull IStrategyPreferredDevicesDispatcher dispatcher) {
         mDeviceInventory.registerStrategyPreferredDevicesDispatcher(dispatcher);
@@ -881,6 +892,16 @@
         mDeviceInventory.unregisterStrategyPreferredDevicesDispatcher(dispatcher);
     }
 
+    /*package*/ void registerStrategyNonDefaultDevicesDispatcher(
+            @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) {
+        mDeviceInventory.registerStrategyNonDefaultDevicesDispatcher(dispatcher);
+    }
+
+    /*package*/ void unregisterStrategyNonDefaultDevicesDispatcher(
+            @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) {
+        mDeviceInventory.unregisterStrategyNonDefaultDevicesDispatcher(dispatcher);
+    }
+
     /*package*/ int setPreferredDevicesForCapturePresetSync(int capturePreset,
             @NonNull List<AudioDeviceAttributes> devices) {
         return mDeviceInventory.setPreferredDevicesForCapturePresetSync(capturePreset, devices);
@@ -1039,6 +1060,17 @@
         sendIMsgNoDelay(MSG_I_SAVE_REMOVE_PREF_DEVICES_FOR_STRATEGY, SENDMSG_QUEUE, strategy);
     }
 
+    /*package*/ void postSaveSetDeviceAsNonDefaultForStrategy(
+            int strategy, AudioDeviceAttributes device) {
+        sendILMsgNoDelay(MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY, SENDMSG_QUEUE, strategy, device);
+    }
+
+    /*package*/ void postSaveRemoveDeviceAsNonDefaultForStrategy(
+            int strategy, AudioDeviceAttributes device) {
+        sendILMsgNoDelay(
+                MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY, SENDMSG_QUEUE, strategy, device);
+    }
+
     /*package*/ void postSaveSetPreferredDevicesForCapturePreset(
             int capturePreset, List<AudioDeviceAttributes> devices) {
         sendILMsgNoDelay(
@@ -1508,6 +1540,16 @@
                     final int strategy = msg.arg1;
                     mDeviceInventory.onSaveRemovePreferredDevices(strategy);
                 } break;
+                case MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY: {
+                    final int strategy = msg.arg1;
+                    final AudioDeviceAttributes device = (AudioDeviceAttributes) msg.obj;
+                    mDeviceInventory.onSaveSetDeviceAsNonDefault(strategy, device);
+                } break;
+                case MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY: {
+                    final int strategy = msg.arg1;
+                    final AudioDeviceAttributes device = (AudioDeviceAttributes) msg.obj;
+                    mDeviceInventory.onSaveRemoveDeviceAsNonDefault(strategy, device);
+                } break;
                 case MSG_CHECK_MUTE_MUSIC:
                     checkMessagesMuteMusic(0);
                     break;
@@ -1593,6 +1635,9 @@
     // process set volume for Le Audio, obj is BleVolumeInfo
     private static final int MSG_II_SET_LE_AUDIO_OUT_VOLUME = 46;
 
+    private static final int MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY = 47;
+    private static final int MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY = 48;
+
     private static boolean isMessageHandledUnderWakelock(int msgId) {
         switch(msgId) {
             case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 34457b0..f9270c9 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -31,8 +31,11 @@
 import android.media.AudioSystem;
 import android.media.IAudioRoutesObserver;
 import android.media.ICapturePresetDevicesRoleDispatcher;
+import android.media.IStrategyNonDefaultDevicesDispatcher;
 import android.media.IStrategyPreferredDevicesDispatcher;
 import android.media.MediaMetrics;
+import android.media.permission.ClearCallingIdentityContext;
+import android.media.permission.SafeCloseable;
 import android.os.Binder;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
@@ -142,6 +145,10 @@
     private final ArrayMap<Integer, List<AudioDeviceAttributes>> mPreferredDevices =
             new ArrayMap<>();
 
+    // List of non-default devices for strategies
+    private final ArrayMap<Integer, List<AudioDeviceAttributes>> mNonDefaultDevices =
+            new ArrayMap<>();
+
     // List of preferred devices of capture preset
     private final ArrayMap<Integer, List<AudioDeviceAttributes>> mPreferredDevicesForCapturePreset =
             new ArrayMap<>();
@@ -156,10 +163,14 @@
     final RemoteCallbackList<IAudioRoutesObserver> mRoutesObservers =
             new RemoteCallbackList<IAudioRoutesObserver>();
 
-    // Monitoring of strategy-preferred device
+    // Monitoring of preferred device for strategies
     final RemoteCallbackList<IStrategyPreferredDevicesDispatcher> mPrefDevDispatchers =
             new RemoteCallbackList<IStrategyPreferredDevicesDispatcher>();
 
+    // Monitoring of non-default device for strategies
+    final RemoteCallbackList<IStrategyNonDefaultDevicesDispatcher> mNonDefDevDispatchers =
+            new RemoteCallbackList<IStrategyNonDefaultDevicesDispatcher>();
+
     // Monitoring of devices for role and capture preset
     final RemoteCallbackList<ICapturePresetDevicesRoleDispatcher> mDevRoleCapturePresetDispatchers =
             new RemoteCallbackList<ICapturePresetDevicesRoleDispatcher>();
@@ -254,6 +265,9 @@
         pw.println("\n" + prefix + "Preferred devices for strategy:");
         mPreferredDevices.forEach((strategy, device) -> {
             pw.println("  " + prefix + "strategy:" + strategy + " device:" + device); });
+        pw.println("\n" + prefix + "Non-default devices for strategy:");
+        mNonDefaultDevices.forEach((strategy, device) -> {
+            pw.println("  " + prefix + "strategy:" + strategy + " device:" + device); });
         pw.println("\n" + prefix + "Connected devices:");
         mConnectedDevices.forEach((key, deviceInfo) -> {
             pw.println("  " + prefix + deviceInfo.toString()); });
@@ -291,6 +305,11 @@
                 mAudioSystem.setDevicesRoleForStrategy(
                         strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices); });
         }
+        synchronized (mNonDefaultDevices) {
+            mNonDefaultDevices.forEach((strategy, devices) -> {
+                mAudioSystem.setDevicesRoleForStrategy(
+                        strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices); });
+        }
         synchronized (mPreferredDevicesForCapturePreset) {
             // TODO: call audiosystem to restore
         }
@@ -608,6 +627,18 @@
     /*package*/ void onSaveSetPreferredDevices(int strategy,
                                                @NonNull List<AudioDeviceAttributes> devices) {
         mPreferredDevices.put(strategy, devices);
+        List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy);
+        if (nonDefaultDevices != null) {
+            nonDefaultDevices.removeAll(devices);
+
+            if (nonDefaultDevices.isEmpty()) {
+                mNonDefaultDevices.remove(strategy);
+            } else {
+                mNonDefaultDevices.put(strategy, nonDefaultDevices);
+            }
+            dispatchNonDefaultDevice(strategy, nonDefaultDevices);
+        }
+
         dispatchPreferredDevice(strategy, devices);
     }
 
@@ -616,6 +647,40 @@
         dispatchPreferredDevice(strategy, new ArrayList<AudioDeviceAttributes>());
     }
 
+    /*package*/ void onSaveSetDeviceAsNonDefault(int strategy,
+                                                 @NonNull AudioDeviceAttributes device) {
+        List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy);
+        if (nonDefaultDevices == null) {
+            nonDefaultDevices = new ArrayList<>();
+        }
+
+        if (!nonDefaultDevices.contains(device)) {
+            nonDefaultDevices.add(device);
+        }
+
+        mNonDefaultDevices.put(strategy, nonDefaultDevices);
+        dispatchNonDefaultDevice(strategy, nonDefaultDevices);
+
+        List<AudioDeviceAttributes> preferredDevices = mPreferredDevices.get(strategy);
+
+        if (preferredDevices != null) {
+            preferredDevices.remove(device);
+            mPreferredDevices.put(strategy, preferredDevices);
+
+            dispatchPreferredDevice(strategy, preferredDevices);
+        }
+    }
+
+    /*package*/ void onSaveRemoveDeviceAsNonDefault(int strategy,
+                                                    @NonNull AudioDeviceAttributes device) {
+        List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy);
+        if (nonDefaultDevices != null) {
+            nonDefaultDevices.remove(device);
+            mNonDefaultDevices.put(strategy, nonDefaultDevices);
+            dispatchNonDefaultDevice(strategy, nonDefaultDevices);
+        }
+    }
+
     /*package*/ void onSaveSetPreferredDevicesForCapturePreset(
             int capturePreset, @NonNull List<AudioDeviceAttributes> devices) {
         mPreferredDevicesForCapturePreset.put(capturePreset, devices);
@@ -631,18 +696,19 @@
     }
 
     //------------------------------------------------------------
-    // preferred device(s)
+    // preferred/non-default device(s)
 
     /*package*/ int setPreferredDevicesForStrategySync(int strategy,
             @NonNull List<AudioDeviceAttributes> devices) {
-        final long identity = Binder.clearCallingIdentity();
+        int status = AudioSystem.ERROR;
 
-        AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
-                                "setPreferredDevicesForStrategySync, strategy: " + strategy
-                                + " devices: " + devices)).printLog(TAG));
-        final int status = mAudioSystem.setDevicesRoleForStrategy(
-                strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
-        Binder.restoreCallingIdentity(identity);
+        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+            AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
+                            "setPreferredDevicesForStrategySync, strategy: " + strategy
+                            + " devices: " + devices)).printLog(TAG));
+            status = mAudioSystem.setDevicesRoleForStrategy(
+                    strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
+        }
 
         if (status == AudioSystem.SUCCESS) {
             mDeviceBroker.postSaveSetPreferredDevicesForStrategy(strategy, devices);
@@ -651,15 +717,16 @@
     }
 
     /*package*/ int removePreferredDevicesForStrategySync(int strategy) {
-        final long identity = Binder.clearCallingIdentity();
+        int status = AudioSystem.ERROR;
 
-        AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
-                "removePreferredDevicesForStrategySync, strategy: "
-                + strategy)).printLog(TAG));
+        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+            AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
+                            "removePreferredDevicesForStrategySync, strategy: "
+                            + strategy)).printLog(TAG));
 
-        final int status = mAudioSystem.removeDevicesRoleForStrategy(
-                strategy, AudioSystem.DEVICE_ROLE_PREFERRED);
-        Binder.restoreCallingIdentity(identity);
+            status = mAudioSystem.clearDevicesRoleForStrategy(
+                    strategy, AudioSystem.DEVICE_ROLE_PREFERRED);
+        }
 
         if (status == AudioSystem.SUCCESS) {
             mDeviceBroker.postSaveRemovePreferredDevicesForStrategy(strategy);
@@ -667,6 +734,50 @@
         return status;
     }
 
+    /*package*/ int setDeviceAsNonDefaultForStrategySync(int strategy,
+            @NonNull AudioDeviceAttributes device) {
+        int status = AudioSystem.ERROR;
+
+        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+            List<AudioDeviceAttributes> devices = new ArrayList<>();
+            devices.add(device);
+
+            AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
+                            "setDeviceAsNonDefaultForStrategySync, strategy: " + strategy
+                            + " device: " + device)).printLog(TAG));
+            status = mAudioSystem.setDevicesRoleForStrategy(
+                    strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices);
+        }
+
+        if (status == AudioSystem.SUCCESS) {
+            mDeviceBroker.postSaveSetDeviceAsNonDefaultForStrategy(strategy, device);
+        }
+        return status;
+    }
+
+    /*package*/ int removeDeviceAsNonDefaultForStrategySync(int strategy,
+            @NonNull AudioDeviceAttributes device) {
+        int status = AudioSystem.ERROR;
+
+        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+            List<AudioDeviceAttributes> devices = new ArrayList<>();
+            devices.add(device);
+
+            AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
+                            "removeDeviceAsNonDefaultForStrategySync, strategy: "
+                            + strategy + " devices: " + device)).printLog(TAG));
+
+            status = mAudioSystem.removeDevicesRoleForStrategy(
+                    strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices);
+        }
+
+        if (status == AudioSystem.SUCCESS) {
+            mDeviceBroker.postSaveRemoveDeviceAsNonDefaultForStrategy(strategy, device);
+        }
+        return status;
+    }
+
+
     /*package*/ void registerStrategyPreferredDevicesDispatcher(
             @NonNull IStrategyPreferredDevicesDispatcher dispatcher) {
         mPrefDevDispatchers.register(dispatcher);
@@ -677,12 +788,24 @@
         mPrefDevDispatchers.unregister(dispatcher);
     }
 
+    /*package*/ void registerStrategyNonDefaultDevicesDispatcher(
+            @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) {
+        mNonDefDevDispatchers.register(dispatcher);
+    }
+
+    /*package*/ void unregisterStrategyNonDefaultDevicesDispatcher(
+            @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) {
+        mNonDefDevDispatchers.unregister(dispatcher);
+    }
+
     /*package*/ int setPreferredDevicesForCapturePresetSync(
             int capturePreset, @NonNull List<AudioDeviceAttributes> devices) {
-        final long identity = Binder.clearCallingIdentity();
-        final int status = mAudioSystem.setDevicesRoleForCapturePreset(
-                capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
-        Binder.restoreCallingIdentity(identity);
+        int status = AudioSystem.ERROR;
+
+        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+            status = mAudioSystem.setDevicesRoleForCapturePreset(
+                    capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
+        }
 
         if (status == AudioSystem.SUCCESS) {
             mDeviceBroker.postSaveSetPreferredDevicesForCapturePreset(capturePreset, devices);
@@ -691,10 +814,12 @@
     }
 
     /*package*/ int clearPreferredDevicesForCapturePresetSync(int capturePreset) {
-        final long identity = Binder.clearCallingIdentity();
-        final int status = mAudioSystem.clearDevicesRoleForCapturePreset(
-                capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED);
-        Binder.restoreCallingIdentity(identity);
+        int status  = AudioSystem.ERROR;
+
+        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+            status = mAudioSystem.clearDevicesRoleForCapturePreset(
+                    capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED);
+        }
 
         if (status == AudioSystem.SUCCESS) {
             mDeviceBroker.postSaveClearPreferredDevicesForCapturePreset(capturePreset);
@@ -1523,6 +1648,19 @@
         mPrefDevDispatchers.finishBroadcast();
     }
 
+    private void dispatchNonDefaultDevice(int strategy,
+                                          @NonNull List<AudioDeviceAttributes> devices) {
+        final int nbDispatchers = mNonDefDevDispatchers.beginBroadcast();
+        for (int i = 0; i < nbDispatchers; i++) {
+            try {
+                mNonDefDevDispatchers.getBroadcastItem(i).dispatchNonDefDevicesChanged(
+                        strategy, devices);
+            } catch (RemoteException e) {
+            }
+        }
+        mNonDefDevDispatchers.finishBroadcast();
+    }
+
     private void dispatchDevicesRoleForCapturePreset(
             int capturePreset, int role, @NonNull List<AudioDeviceAttributes> devices) {
         final int nbDispatchers = mDevRoleCapturePresetDispatchers.beginBroadcast();
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 43c8032d..24c7d2c 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -116,6 +116,7 @@
 import android.media.ISpatializerHeadTrackerAvailableCallback;
 import android.media.ISpatializerHeadTrackingModeCallback;
 import android.media.ISpatializerOutputCallback;
+import android.media.IStrategyNonDefaultDevicesDispatcher;
 import android.media.IStrategyPreferredDevicesDispatcher;
 import android.media.IVolumeController;
 import android.media.MediaMetrics;
@@ -2800,11 +2801,12 @@
      * @see AudioManager#setPreferredDevicesForStrategy(AudioProductStrategy,
      *                                                  List<AudioDeviceAttributes>)
      */
+    @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
     public int setPreferredDevicesForStrategy(int strategy, List<AudioDeviceAttributes> devices) {
+        super.setPreferredDevicesForStrategy_enforcePermission();
         if (devices == null) {
             return AudioSystem.ERROR;
         }
-        enforceModifyAudioRoutingPermission();
         final String logString = String.format(
                 "setPreferredDeviceForStrategy u/pid:%d/%d strat:%d dev:%s",
                 Binder.getCallingUid(), Binder.getCallingPid(), strategy,
@@ -2862,6 +2864,81 @@
         }
     }
 
+    /**
+     * @see AudioManager#setDeviceAsNonDefaultForStrategy(AudioProductStrategy,
+     *                                                    AudioDeviceAttributes)
+     * @see AudioManager#setDeviceAsNonDefaultForStrategy(AudioProductStrategy,
+     *                                                     List<AudioDeviceAttributes>)
+     */
+    @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public int setDeviceAsNonDefaultForStrategy(int strategy,
+                                                @NonNull AudioDeviceAttributes device) {
+        super.setDeviceAsNonDefaultForStrategy_enforcePermission();
+        Objects.requireNonNull(device);
+        final String logString = String.format(
+                "setDeviceAsNonDefaultForStrategy u/pid:%d/%d strat:%d dev:%s",
+                Binder.getCallingUid(), Binder.getCallingPid(), strategy, device.toString());
+        sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG));
+        if (device.getRole() == AudioDeviceAttributes.ROLE_INPUT) {
+            Log.e(TAG, "Unsupported input routing in " + logString);
+            return AudioSystem.ERROR;
+        }
+
+        final int status = mDeviceBroker.setDeviceAsNonDefaultForStrategySync(strategy, device);
+        if (status != AudioSystem.SUCCESS) {
+            Log.e(TAG, String.format("Error %d in %s)", status, logString));
+        }
+
+        return status;
+    }
+
+    /**
+     * @see AudioManager#removeDeviceAsNonDefaultForStrategy(AudioProductStrategy,
+     *                                                       AudioDeviceAttributes)
+     */
+    @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public int removeDeviceAsNonDefaultForStrategy(int strategy,
+                                                   AudioDeviceAttributes device) {
+        super.removeDeviceAsNonDefaultForStrategy_enforcePermission();
+        Objects.requireNonNull(device);
+        final String logString = String.format(
+                "removeDeviceAsNonDefaultForStrategy strat:%d dev:%s", strategy, device.toString());
+        sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG));
+        if (device.getRole() == AudioDeviceAttributes.ROLE_INPUT) {
+            Log.e(TAG, "Unsupported input routing in " + logString);
+            return AudioSystem.ERROR;
+        }
+
+        final int status = mDeviceBroker.removeDeviceAsNonDefaultForStrategySync(strategy, device);
+        if (status != AudioSystem.SUCCESS) {
+            Log.e(TAG, String.format("Error %d in %s)", status, logString));
+        }
+        return status;
+    }
+
+    /**
+     * @see AudioManager#getNonDefaultDevicesForStrategy(AudioProductStrategy)
+     */
+    @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    public List<AudioDeviceAttributes> getNonDefaultDevicesForStrategy(int strategy) {
+        super.getNonDefaultDevicesForStrategy_enforcePermission();
+        List<AudioDeviceAttributes> devices = new ArrayList<>();
+        int status = AudioSystem.ERROR;
+
+        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+            status = AudioSystem.getDevicesForRoleAndStrategy(
+                    strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices);
+        }
+
+        if (status != AudioSystem.SUCCESS) {
+            Log.e(TAG, String.format("Error %d in getNonDefaultDeviceForStrategy(%d)",
+                    status, strategy));
+            return new ArrayList<AudioDeviceAttributes>();
+        } else {
+            return devices;
+        }
+    }
+
     /** @see AudioManager#addOnPreferredDevicesForStrategyChangedListener(
      *               Executor, AudioManager.OnPreferredDevicesForStrategyChangedListener)
      */
@@ -2886,6 +2963,30 @@
         mDeviceBroker.unregisterStrategyPreferredDevicesDispatcher(dispatcher);
     }
 
+    /** @see AudioManager#addOnNonDefaultDevicesForStrategyChangedListener(
+     *               Executor, AudioManager.OnNonDefaultDevicesForStrategyChangedListener)
+     */
+    public void registerStrategyNonDefaultDevicesDispatcher(
+            @Nullable IStrategyNonDefaultDevicesDispatcher dispatcher) {
+        if (dispatcher == null) {
+            return;
+        }
+        enforceModifyAudioRoutingPermission();
+        mDeviceBroker.registerStrategyNonDefaultDevicesDispatcher(dispatcher);
+    }
+
+    /** @see AudioManager#removeOnNonDefaultDevicesForStrategyChangedListener(
+     *               AudioManager.OnNonDefaultDevicesForStrategyChangedListener)
+     */
+    public void unregisterStrategyNonDefaultDevicesDispatcher(
+            @Nullable IStrategyNonDefaultDevicesDispatcher dispatcher) {
+        if (dispatcher == null) {
+            return;
+        }
+        enforceModifyAudioRoutingPermission();
+        mDeviceBroker.unregisterStrategyNonDefaultDevicesDispatcher(dispatcher);
+    }
+
     /**
      * @see AudioManager#setPreferredDeviceForCapturePreset(int, AudioDeviceAttributes)
      */
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index c176f29..7fefc55 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -279,14 +279,27 @@
     }
 
     /**
-     * Same as {@link AudioSystem#removeDevicesRoleForStrategy(int, int)}
+     * Same as {@link AudioSystem#removeDevicesRoleForStrategy(int, int, List)}
+     * @param strategy
+     * @param role
+     * @param devices
+     * @return
+     */
+    public int removeDevicesRoleForStrategy(int strategy, int role,
+                                            @NonNull List<AudioDeviceAttributes> devices) {
+        invalidateRoutingCache();
+        return AudioSystem.removeDevicesRoleForStrategy(strategy, role, devices);
+    }
+
+    /**
+     * Same as {@link AudioSystem#clearDevicesRoleForStrategy(int, int)}
      * @param strategy
      * @param role
      * @return
      */
-    public int removeDevicesRoleForStrategy(int strategy, int role) {
+    public int clearDevicesRoleForStrategy(int strategy, int role) {
         invalidateRoutingCache();
-        return AudioSystem.removeDevicesRoleForStrategy(strategy, role);
+        return AudioSystem.clearDevicesRoleForStrategy(strategy, role);
     }
 
     /**
diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java
index 5353092..6b5af88 100644
--- a/services/core/java/com/android/server/hdmi/Constants.java
+++ b/services/core/java/com/android/server/hdmi/Constants.java
@@ -619,6 +619,8 @@
     })
     @interface HpdSignalType {}
 
+    static final String DEVICE_CONFIG_FEATURE_FLAG_SOUNDBAR_MODE = "soundbar_mode";
+
     private Constants() {
         /* cannot be instantiated */
     }
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index f66f8ea..85477b3 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -86,6 +86,7 @@
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
+import android.provider.DeviceConfig;
 import android.provider.Settings.Global;
 import android.sysprop.HdmiProperties;
 import android.text.TextUtils;
@@ -450,6 +451,9 @@
     private boolean mWakeUpMessageReceived = false;
 
     @ServiceThreadOnly
+    private boolean mSoundbarModeFeatureFlagEnabled = false;
+
+    @ServiceThreadOnly
     private int mActivePortId = Constants.INVALID_PORT_ID;
 
     // Set to true while the input change by MHL is allowed.
@@ -763,14 +767,6 @@
                         }
                     }
                 }, mServiceThreadExecutor);
-        mHdmiCecConfig.registerChangeListener(HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
-                new HdmiCecConfig.SettingChangeListener() {
-                    @Override
-                    public void onChange(String setting) {
-                        setSoundbarMode(mHdmiCecConfig.getIntValue(
-                                HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE));
-                    }
-                }, mServiceThreadExecutor);
         mHdmiCecConfig.registerChangeListener(
                 HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL,
                 new HdmiCecConfig.SettingChangeListener() {
@@ -819,9 +815,39 @@
                                 HdmiControlManager.SETTING_NAME_EARC_ENABLED);
                         setEarcEnabled(enabled);
                     }
+                },
+                mServiceThreadExecutor);
+
+        mSoundbarModeFeatureFlagEnabled = DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_HDMI_CONTROL,
+                Constants.DEVICE_CONFIG_FEATURE_FLAG_SOUNDBAR_MODE, false);
+
+        DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_HDMI_CONTROL,
+                getContext().getMainExecutor(), new DeviceConfig.OnPropertiesChangedListener() {
+                    @Override
+                    public void onPropertiesChanged(DeviceConfig.Properties properties) {
+                        mSoundbarModeFeatureFlagEnabled = properties.getBoolean(
+                                Constants.DEVICE_CONFIG_FEATURE_FLAG_SOUNDBAR_MODE,
+                                false);
+                        boolean soundbarModeSetting = mHdmiCecConfig.getIntValue(
+                                HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE)
+                                == SOUNDBAR_MODE_ENABLED;
+                        setSoundbarMode(soundbarModeSetting && mSoundbarModeFeatureFlagEnabled
+                                ? SOUNDBAR_MODE_ENABLED : SOUNDBAR_MODE_DISABLED);
+                    }
+                });
+        mHdmiCecConfig.registerChangeListener(HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE,
+                new HdmiCecConfig.SettingChangeListener() {
+                    @Override
+                    public void onChange(String setting) {
+                        boolean soundbarModeSetting = mHdmiCecConfig.getIntValue(
+                                HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE)
+                                == SOUNDBAR_MODE_ENABLED;
+                        setSoundbarMode(soundbarModeSetting && mSoundbarModeFeatureFlagEnabled
+                                ? SOUNDBAR_MODE_ENABLED : SOUNDBAR_MODE_DISABLED);
+                    }
                 }, mServiceThreadExecutor);
     }
-
     /** Returns true if the device screen is off */
     boolean isScreenOff() {
         return mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY).getState() == Display.STATE_OFF;
@@ -937,6 +963,11 @@
         return mPowerManagerInternal;
     }
 
+    @VisibleForTesting
+    protected void enableAllFeatureFlags() {
+        mSoundbarModeFeatureFlagEnabled = true;
+    }
+
     /**
      * Triggers the address allocation that states the presence of a local device audio system in
      * the network.
@@ -1151,7 +1182,8 @@
         if (mHdmiCecConfig.getIntValue(HdmiControlManager.CEC_SETTING_NAME_SOUNDBAR_MODE)
                 == SOUNDBAR_MODE_ENABLED
                 && !allLocalDeviceTypes.contains(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM)
-                && SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true)) {
+                && SystemProperties.getBoolean(Constants.PROPERTY_ARC_SUPPORT, true)
+                && mSoundbarModeFeatureFlagEnabled) {
             allLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
         }
         return allLocalDeviceTypes;
diff --git a/services/core/java/com/android/server/pm/AppStateHelper.java b/services/core/java/com/android/server/pm/AppStateHelper.java
index 2ef193c..32479ee 100644
--- a/services/core/java/com/android/server/pm/AppStateHelper.java
+++ b/services/core/java/com/android/server/pm/AppStateHelper.java
@@ -23,6 +23,7 @@
 import android.app.usage.NetworkStats;
 import android.app.usage.NetworkStatsManager;
 import android.content.Context;
+import android.content.pm.PackageManagerInternal;
 import android.media.AudioManager;
 import android.media.IAudioService;
 import android.net.ConnectivityManager;
@@ -36,6 +37,7 @@
 import com.android.server.LocalServices;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
@@ -157,6 +159,56 @@
         return false;
     }
 
+    private static boolean containsAny(Collection<String> list, Collection<String> which) {
+        if (list.isEmpty()) {
+            return false;
+        }
+        for (var element : which) {
+            if (list.contains(element)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void addLibraryDependency(ArraySet<String> results, List<String> libPackageNames) {
+        var pmInternal = LocalServices.getService(PackageManagerInternal.class);
+
+        var libraryNames = new ArraySet<String>();
+        var staticSharedLibraryNames = new ArraySet<String>();
+        var sdkLibraryNames = new ArraySet<String>();
+        for (var packageName : libPackageNames) {
+            var pkg = pmInternal.getAndroidPackage(packageName);
+            if (pkg == null) {
+                continue;
+            }
+            libraryNames.addAll(pkg.getLibraryNames());
+            var libraryName = pkg.getStaticSharedLibraryName();
+            if (libraryName != null) {
+                staticSharedLibraryNames.add(libraryName);
+            }
+            libraryName = pkg.getSdkLibraryName();
+            if (libraryName != null) {
+                sdkLibraryNames.add(libraryName);
+            }
+        }
+
+        if (libraryNames.isEmpty()
+                && staticSharedLibraryNames.isEmpty()
+                && sdkLibraryNames.isEmpty()) {
+            return;
+        }
+
+        pmInternal.forEachPackage(pkg -> {
+            if (containsAny(pkg.getUsesLibraries(), libraryNames)
+                    || containsAny(pkg.getUsesOptionalLibraries(), libraryNames)
+                    || containsAny(pkg.getUsesStaticLibraries(), staticSharedLibraryNames)
+                    || containsAny(pkg.getUsesSdkLibraries(), sdkLibraryNames)) {
+                results.add(pkg.getPackageName());
+            }
+        });
+    }
+
     /**
      * True if any app has sent or received network data over the past
      * {@link #ACTIVE_NETWORK_DURATION_MILLIS} milliseconds.
@@ -225,6 +277,7 @@
      */
     public List<String> getDependencyPackages(List<String> packageNames) {
         var results = new ArraySet<String>();
+        // Include packages sharing the same process
         var am = mContext.getSystemService(ActivityManager.class);
         for (var info : am.getRunningAppProcesses()) {
             for (var packageName : packageNames) {
@@ -236,10 +289,14 @@
                 }
             }
         }
+        // Include packages using bounded services
         var amInternal = LocalServices.getService(ActivityManagerInternal.class);
         for (var packageName : packageNames) {
             results.addAll(amInternal.getClientPackages(packageName));
         }
+        // Include packages using libraries
+        addLibraryDependency(results, packageNames);
+
         return new ArrayList<>(results);
     }
 }
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index ff020eb..321c5c6 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -89,6 +89,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.BackgroundThread;
+import com.android.internal.security.VerityUtils;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.IndentingPrintWriter;
@@ -370,6 +371,8 @@
 
     // Current settings file.
     private final File mSettingsFilename;
+    // Reserve copy of the current settings file.
+    private final File mSettingsReserveCopyFilename;
     // Previous settings file.
     // Removed when the current settings file successfully stored.
     private final File mPreviousSettingsFilename;
@@ -640,6 +643,7 @@
         mRuntimePermissionsPersistence = null;
         mPermissionDataProvider = null;
         mSettingsFilename = null;
+        mSettingsReserveCopyFilename = null;
         mPreviousSettingsFilename = null;
         mPackageListFilename = null;
         mStoppedPackagesFilename = null;
@@ -711,6 +715,7 @@
                 |FileUtils.S_IROTH|FileUtils.S_IXOTH,
                 -1, -1);
         mSettingsFilename = new File(mSystemDir, "packages.xml");
+        mSettingsReserveCopyFilename = new File(mSystemDir, "packages.xml.reservecopy");
         mPreviousSettingsFilename = new File(mSystemDir, "packages-backup.xml");
         mPackageListFilename = new File(mSystemDir, "packages.list");
         FileUtils.setPermissions(mPackageListFilename, 0640, SYSTEM_UID, PACKAGE_INFO_GID);
@@ -752,6 +757,7 @@
         mLock = null;
         mRuntimePermissionsPersistence = r.mRuntimePermissionsPersistence;
         mSettingsFilename = null;
+        mSettingsReserveCopyFilename = null;
         mPreviousSettingsFilename = null;
         mPackageListFilename = null;
         mStoppedPackagesFilename = null;
@@ -2681,12 +2687,25 @@
 
             // New settings successfully written, old ones are no longer needed.
             mPreviousSettingsFilename.delete();
+            mSettingsReserveCopyFilename.delete();
 
             FileUtils.setPermissions(mSettingsFilename.toString(),
-                    FileUtils.S_IRUSR|FileUtils.S_IWUSR
-                    |FileUtils.S_IRGRP|FileUtils.S_IWGRP,
+                    FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP | FileUtils.S_IWGRP,
                     -1, -1);
 
+            try {
+                FileUtils.copy(mSettingsFilename, mSettingsReserveCopyFilename);
+            } catch (IOException e) {
+                Slog.e(TAG, "Failed to backup settings", e);
+            }
+
+            try {
+                VerityUtils.setUpFsverity(mSettingsFilename.getAbsolutePath());
+                VerityUtils.setUpFsverity(mSettingsReserveCopyFilename.getAbsolutePath());
+            } catch (IOException e) {
+                Slog.e(TAG, "Failed to verity-protect settings", e);
+            }
+
             writeKernelMappingLPr();
             writePackageListLPr();
             writeAllUsersPackageRestrictionsLPr(sync);
@@ -3117,49 +3136,62 @@
         }
     }
 
-    boolean readLPw(@NonNull Computer computer, @NonNull List<UserInfo> users) {
-        FileInputStream str = null;
-        if (mPreviousSettingsFilename.exists()) {
-            try {
-                str = new FileInputStream(mPreviousSettingsFilename);
-                mReadMessages.append("Reading from backup settings file\n");
-                PackageManagerService.reportSettingsProblem(Log.INFO,
-                        "Need to read from backup settings file");
-                if (mSettingsFilename.exists()) {
-                    // If both the previous and current settings files exist,
-                    // we ignore the current since it might have been corrupted.
-                    Slog.w(PackageManagerService.TAG, "Cleaning up settings file "
-                            + mSettingsFilename);
-                    mSettingsFilename.delete();
-                }
-            } catch (java.io.IOException e) {
-                // We'll try for the normal settings file.
-            }
-        }
-
+    boolean readSettingsLPw(@NonNull Computer computer, @NonNull List<UserInfo> users,
+            ArrayMap<String, Long> originalFirstInstallTimes) {
         mPendingPackages.clear();
         mPastSignatures.clear();
         mKeySetRefs.clear();
         mInstallerPackages.clear();
+        originalFirstInstallTimes.clear();
 
-        // If any user state doesn't have a first install time, e.g., after an OTA,
-        // use the pre OTA firstInstallTime timestamp. This is because we migrated from per package
-        // firstInstallTime to per user-state. Without this, OTA can cause this info to be lost.
-        final ArrayMap<String, Long> originalFirstInstallTimes = new ArrayMap<>();
+        File file = null;
+        FileInputStream str = null;
 
         try {
-            if (str == null) {
-                if (!mSettingsFilename.exists()) {
-                    mReadMessages.append("No settings file found\n");
+            // Check if the previous write was incomplete.
+            if (mPreviousSettingsFilename.exists()) {
+                try {
+                    file = mPreviousSettingsFilename;
+                    str = new FileInputStream(file);
+                    mReadMessages.append("Reading from backup settings file\n");
                     PackageManagerService.reportSettingsProblem(Log.INFO,
-                            "No settings file; creating initial state");
-                    // It's enough to just touch version details to create them
-                    // with default values
-                    findOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL).forceCurrent();
-                    findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL).forceCurrent();
-                    return false;
+                            "Need to read from backup settings file");
+                    if (mSettingsFilename.exists()) {
+                        // If both the previous and current settings files exist,
+                        // we ignore the current since it might have been corrupted.
+                        Slog.w(PackageManagerService.TAG, "Cleaning up settings file "
+                                + mSettingsFilename);
+                        mSettingsFilename.delete();
+                    }
+                    // Ignore reserve copy as well.
+                    mSettingsReserveCopyFilename.delete();
+                } catch (java.io.IOException e) {
+                    // We'll try for the normal settings file.
                 }
-                str = new FileInputStream(mSettingsFilename);
+            }
+            if (str == null) {
+                if (mSettingsFilename.exists()) {
+                    // Using packages.xml.
+                    file = mSettingsFilename;
+                    str = new FileInputStream(file);
+                } else if (mSettingsReserveCopyFilename.exists()) {
+                    // Using reserve copy.
+                    file = mSettingsReserveCopyFilename;
+                    str = new FileInputStream(file);
+                    mReadMessages.append("Reading from reserve copy settings file\n");
+                    PackageManagerService.reportSettingsProblem(Log.INFO,
+                            "Need to read from reserve copy settings file");
+                }
+            }
+            if (str == null) {
+                // No available data sources.
+                mReadMessages.append("No settings file found\n");
+                PackageManagerService.reportSettingsProblem(Log.INFO,
+                        "No settings file; creating initial state");
+                // Not necessary, but will avoid wtf-s in the "finally" section.
+                findOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL).forceCurrent();
+                findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL).forceCurrent();
+                return false;
             }
             final TypedXmlPullParser parser = Xml.resolvePullParser(str);
 
@@ -3280,6 +3312,33 @@
             mReadMessages.append("Error reading: " + e.toString());
             PackageManagerService.reportSettingsProblem(Log.ERROR, "Error reading settings: " + e);
             Slog.wtf(PackageManagerService.TAG, "Error reading package manager settings", e);
+
+            // Remove corrupted file and retry.
+            Slog.e(TAG,
+                    "Error reading package manager settings, removing " + file + " and retrying.",
+                    e);
+            file.delete();
+
+            // Ignore the result to not mark this as a "first boot".
+            readSettingsLPw(computer, users, originalFirstInstallTimes);
+        }
+
+        return true;
+    }
+
+    /**
+     * @return false if settings file is missing (i.e. during first boot), true otherwise
+     */
+    boolean readLPw(@NonNull Computer computer, @NonNull List<UserInfo> users) {
+        // If any user state doesn't have a first install time, e.g., after an OTA,
+        // use the pre OTA firstInstallTime timestamp. This is because we migrated from per package
+        // firstInstallTime to per user-state. Without this, OTA can cause this info to be lost.
+        final ArrayMap<String, Long> originalFirstInstallTimes = new ArrayMap<>();
+
+        try {
+            if (!readSettingsLPw(computer, users, originalFirstInstallTimes)) {
+                return false;
+            }
         } finally {
             if (!mVersion.containsKey(StorageManager.UUID_PRIVATE_INTERNAL)) {
                 Slog.wtf(PackageManagerService.TAG,
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 6b45ba2..5b9460a 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1052,6 +1052,17 @@
     private void startBootstrapServices(@NonNull TimingsTraceAndSlog t) {
         t.traceBegin("startBootstrapServices");
 
+        t.traceBegin("ArtModuleServiceInitializer");
+        // This needs to happen before DexUseManagerLocal init. We do it here to avoid colliding
+        // with a GC. ArtModuleServiceInitializer is a class from a separate dex file
+        // "service-art.jar", so referencing it involves the class linker. The class linker and the
+        // GC are mutually exclusive (b/263486535). Therefore, we do this here to force trigger the
+        // class linker earlier. If we did this later, especially after PackageManagerService init,
+        // the class linker would be consistently blocked by a GC because PackageManagerService
+        // allocates a lot of memory and almost certainly triggers a GC.
+        ArtModuleServiceInitializer.setArtModuleServiceManager(new ArtModuleServiceManager());
+        t.traceEnd();
+
         // Start the watchdog as early as possible so we can crash the system server
         // if we deadlock during early boot
         t.traceBegin("StartWatchdog");
@@ -1238,8 +1249,6 @@
         t.traceBegin("DexUseManagerLocal");
         // DexUseManagerLocal needs to be loaded after PackageManagerLocal has been registered, but
         // before PackageManagerService starts processing binder calls to notifyDexLoad.
-        // DexUseManagerLocal may also call artd, so ensure ArtModuleServiceManager is instantiated.
-        ArtModuleServiceInitializer.setArtModuleServiceManager(new ArtModuleServiceManager());
         LocalManagerRegistry.addManager(
                 DexUseManagerLocal.class, DexUseManagerLocal.createInstance());
         t.traceEnd();
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
index 3727d66..b20e1dd 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@@ -92,8 +92,11 @@
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.security.PublicKey;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.PriorityQueue;
@@ -141,8 +144,7 @@
 
     /** make sure our initialized KeySetManagerService metadata matches packages.xml */
     @Test
-    public void testReadKeySetSettings()
-            throws ReflectiveOperationException, IllegalAccessException {
+    public void testReadKeySetSettings() throws Exception {
         /* write out files and read */
         writeOldFiles();
         Settings settings = makeSettings();
@@ -150,6 +152,29 @@
         verifyKeySetMetaData(settings);
     }
 
+    // Same as above but use the reserve copy.
+    @Test
+    public void testReadReserveCopyKeySetSettings() throws Exception {
+        /* write out files and read */
+        writeReserveCopyOldFiles();
+        Settings settings = makeSettings();
+        assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+        verifyKeySetMetaData(settings);
+    }
+
+    // Same as above but packages.xml is malformed.
+    @Test
+    public void testReadMalformedPackagesXmlKeySetSettings() throws Exception {
+        // write out files
+        writeReserveCopyOldFiles();
+        // write corrupted packages.xml
+        writeCorruptedPackagesXml();
+
+        Settings settings = makeSettings();
+        assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+        verifyKeySetMetaData(settings);
+    }
+
     /** read in data, write it out, and read it back in.  Verify same. */
     @Test
     public void testWriteKeySetSettings()
@@ -165,6 +190,39 @@
         verifyKeySetMetaData(settings);
     }
 
+    // Same as above, but corrupt the primary.xml in process.
+    @Test
+    public void testWriteCorruptReadKeySetSettings() throws Exception {
+        // write out files and read
+        writeOldFiles();
+        Settings settings = makeSettings();
+        assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+
+        // write out
+        settings.writeLPr(computer, /*sync=*/true);
+
+        File filesDir = InstrumentationRegistry.getContext().getFilesDir();
+        File packageXml = new File(filesDir, "system/packages.xml");
+        File packagesReserveCopyXml = new File(filesDir, "system/packages.xml.reservecopy");
+        // Primary.
+        assertTrue(packageXml.exists());
+        // Reserve copy.
+        assertTrue(packagesReserveCopyXml.exists());
+        // Temporary backup.
+        assertFalse(new File(filesDir, "packages-backup.xml").exists());
+
+        // compare two copies, make sure they are the same
+        assertTrue(Arrays.equals(Files.readAllBytes(Path.of(packageXml.getAbsolutePath())),
+                Files.readAllBytes(Path.of(packagesReserveCopyXml.getAbsolutePath()))));
+
+        // write corrupted packages.xml
+        writeCorruptedPackagesXml();
+
+        // read back in and verify the same
+        assertThat(settings.readLPw(computer, createFakeUsers()), is(true));
+        verifyKeySetMetaData(settings);
+    }
+
     @Test
     public void testSettingsReadOld() {
         // Write delegateshellthe package files and make sure they're parsed properly the first time
@@ -1572,9 +1630,19 @@
         }
     }
 
-    private void writePackagesXml() {
+    private void writeCorruptedPackagesXml() {
         writeFile(new File(InstrumentationRegistry.getContext().getFilesDir(), "system/packages.xml"),
                 ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>"
+                        + "<packages>"
+                        + "<last-platform-version internal=\"15\" external=\"0\" />"
+                        + "<permission-trees>"
+                        + "<item name=\"com.google.android.permtree\""
+                ).getBytes());
+    }
+
+    private void writePackagesXml(String fileName) {
+        writeFile(new File(InstrumentationRegistry.getContext().getFilesDir(), fileName),
+                ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>"
                 + "<packages>"
                 + "<last-platform-version internal=\"15\" external=\"0\" fingerprint=\"foo\" />"
                 + "<permission-trees>"
@@ -1715,7 +1783,14 @@
 
     private void writeOldFiles() {
         deleteSystemFolder();
-        writePackagesXml();
+        writePackagesXml("system/packages.xml");
+        writeStoppedPackagesXml();
+        writePackagesList();
+    }
+
+    private void writeReserveCopyOldFiles() {
+        deleteSystemFolder();
+        writePackagesXml("system/packages.xml.reservecopy");
         writeStoppedPackagesXml();
         writePackagesList();
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index 8d78cd6..f56b0b4 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -197,7 +197,7 @@
 
     private void enqueueOrReplaceBroadcast(BroadcastProcessQueue queue,
             BroadcastRecord record, int recordIndex, long enqueueTime) {
-        queue.enqueueOrReplaceBroadcast(record, recordIndex);
+        queue.enqueueOrReplaceBroadcast(record, recordIndex, null /* replacedBroadcastConsumer */);
         record.enqueueTime = enqueueTime;
     }
 
@@ -327,7 +327,7 @@
         final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
         final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane,
                 List.of(makeMockRegisteredReceiver()));
-        queue.enqueueOrReplaceBroadcast(airplaneRecord, 0);
+        queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */);
 
         queue.setProcessCached(false);
         final long notCachedRunnableAt = queue.getRunnableAt();
@@ -349,12 +349,12 @@
         // enqueue a bg-priority broadcast then a fg-priority one
         final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED);
         final BroadcastRecord timezoneRecord = makeBroadcastRecord(timezone);
-        queue.enqueueOrReplaceBroadcast(timezoneRecord, 0);
+        queue.enqueueOrReplaceBroadcast(timezoneRecord, 0, null /* replacedBroadcastConsumer */);
 
         final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
         airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
         final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane);
-        queue.enqueueOrReplaceBroadcast(airplaneRecord, 0);
+        queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */);
 
         // verify that:
         // (a) the queue is immediately runnable by existence of a fg-priority broadcast
@@ -385,7 +385,7 @@
         final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane, null,
                 List.of(withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10),
                         withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 0)), true);
-        queue.enqueueOrReplaceBroadcast(airplaneRecord, 1);
+        queue.enqueueOrReplaceBroadcast(airplaneRecord, 1, null /* replacedBroadcastConsumer */);
 
         assertFalse(queue.isRunnable());
         assertEquals(BroadcastProcessQueue.REASON_BLOCKED, queue.getRunnableAtReason());
@@ -408,7 +408,7 @@
         final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
         final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane,
                 List.of(makeMockRegisteredReceiver()));
-        queue.enqueueOrReplaceBroadcast(airplaneRecord, 0);
+        queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */);
 
         mConstants.MAX_PENDING_BROADCASTS = 128;
         queue.invalidateRunnableAt();
@@ -434,11 +434,11 @@
                 new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED),
                 List.of(makeMockRegisteredReceiver()));
 
-        queue.enqueueOrReplaceBroadcast(lazyRecord, 0);
+        queue.enqueueOrReplaceBroadcast(lazyRecord, 0, null /* replacedBroadcastConsumer */);
         assertThat(queue.getRunnableAt()).isGreaterThan(lazyRecord.enqueueTime);
         assertThat(queue.getRunnableAtReason()).isNotEqualTo(testRunnableAtReason);
 
-        queue.enqueueOrReplaceBroadcast(testRecord, 0);
+        queue.enqueueOrReplaceBroadcast(testRecord, 0, null /* replacedBroadcastConsumer */);
         assertThat(queue.getRunnableAt()).isAtMost(testRecord.enqueueTime);
         assertThat(queue.getRunnableAtReason()).isEqualTo(testRunnableAtReason);
     }
@@ -507,20 +507,26 @@
 
         queue.enqueueOrReplaceBroadcast(
                 makeBroadcastRecord(new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED)
-                        .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0);
+                        .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0,
+                null /* replacedBroadcastConsumer */);
         queue.enqueueOrReplaceBroadcast(
-                makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)), 0);
+                makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)), 0,
+                null /* replacedBroadcastConsumer */);
         queue.enqueueOrReplaceBroadcast(
                 makeBroadcastRecord(new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED)
-                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0);
+                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0,
+                null /* replacedBroadcastConsumer */);
         queue.enqueueOrReplaceBroadcast(
                 makeBroadcastRecord(new Intent(Intent.ACTION_ALARM_CHANGED)
-                        .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0);
+                        .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0,
+                null /* replacedBroadcastConsumer */);
         queue.enqueueOrReplaceBroadcast(
-                makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)), 0);
+                makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)), 0,
+                null /* replacedBroadcastConsumer */);
         queue.enqueueOrReplaceBroadcast(
                 makeBroadcastRecord(new Intent(Intent.ACTION_LOCALE_CHANGED)
-                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0);
+                        .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0,
+                null /* replacedBroadcastConsumer */);
 
         queue.makeActiveNextPending();
         assertEquals(Intent.ACTION_LOCKED_BOOT_COMPLETED, queue.getActive().intent.getAction());
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index d79c4d8..6800d52 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -154,6 +154,7 @@
 
     private ActivityManagerService mAms;
     private BroadcastQueue mQueue;
+    BroadcastConstants mConstants;
 
     /**
      * Desired behavior of the next
@@ -277,10 +278,9 @@
         }).when(mAms).getProcessRecordLocked(any(), anyInt());
         doNothing().when(mAms).appNotResponding(any(), any());
 
-        final BroadcastConstants constants = new BroadcastConstants(
-                Settings.Global.BROADCAST_FG_CONSTANTS);
-        constants.TIMEOUT = 100;
-        constants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
+        mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
+        mConstants.TIMEOUT = 100;
+        mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
         final BroadcastSkipPolicy emptySkipPolicy = new BroadcastSkipPolicy(mAms) {
             public boolean shouldSkip(BroadcastRecord r, Object o) {
                 // Ignored
@@ -291,7 +291,7 @@
                 return null;
             }
         };
-        final BroadcastHistory emptyHistory = new BroadcastHistory(constants) {
+        final BroadcastHistory emptyHistory = new BroadcastHistory(mConstants) {
             public void addBroadcastToHistoryLocked(BroadcastRecord original) {
                 // Ignored
             }
@@ -299,13 +299,13 @@
 
         if (mImpl == Impl.DEFAULT) {
             var q = new BroadcastQueueImpl(mAms, mHandlerThread.getThreadHandler(), TAG,
-                    constants, emptySkipPolicy, emptyHistory, false,
+                    mConstants, emptySkipPolicy, emptyHistory, false,
                     ProcessList.SCHED_GROUP_DEFAULT);
             q.mReceiverBatch.mDeepReceiverCopy = true;
             mQueue = q;
         } else if (mImpl == Impl.MODERN) {
             var q = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(),
-                    constants, constants, emptySkipPolicy, emptyHistory);
+                    mConstants, mConstants, emptySkipPolicy, emptyHistory);
             q.mReceiverBatch.mDeepReceiverCopy = true;
             mQueue = q;
         } else {
@@ -1703,6 +1703,28 @@
     }
 
     @Test
+    public void testReplacePending_withPrioritizedBroadcasts() throws Exception {
+        mConstants.MAX_RUNNING_ACTIVE_BROADCASTS = 1;
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_GREEN);
+
+        final Intent userPresent = new Intent(Intent.ACTION_USER_PRESENT)
+                .addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+
+        final List receivers = List.of(
+                withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 100),
+                withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_RED), 50),
+                withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_YELLOW), 10),
+                withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_BLUE), 0));
+
+        // Enqueue the broadcast a few times and verify that broadcast queues are not stuck
+        // and are emptied eventually.
+        for (int i = 0; i < 6; ++i) {
+            enqueueBroadcast(makeBroadcastRecord(userPresent, callerApp, receivers));
+        }
+        waitForIdle();
+    }
+
+    @Test
     public void testIdleAndBarrier() throws Exception {
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
         final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN);
diff --git a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
index ee9d59b..08a0878 100644
--- a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
+++ b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java
@@ -75,7 +75,13 @@
     }
 
     @Override
-    public int removeDevicesRoleForStrategy(int strategy, int role) {
+    public int removeDevicesRoleForStrategy(int strategy, int role,
+            @NonNull List<AudioDeviceAttributes> devices) {
+        return AudioSystem.AUDIO_STATUS_OK;
+    }
+
+    @Override
+    public int clearDevicesRoleForStrategy(int strategy, int role) {
         return AudioSystem.AUDIO_STATUS_OK;
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index ac880ce..89eaa2c 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -35,7 +35,6 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.nullable;
 import static org.mockito.Mockito.argThat;
-import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -97,8 +96,9 @@
 import android.view.KeyEvent;
 import android.view.WindowManager;
 
-import androidx.test.InstrumentationRegistry;
+import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.compatibility.common.util.AdoptShellPermissionsRule;
 import com.android.internal.app.BlockedAppStreamingActivity;
 import com.android.server.LocalServices;
 import com.android.server.input.InputManagerInternal;
@@ -107,6 +107,7 @@
 import com.google.android.collect.Sets;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -177,17 +178,14 @@
                     .setAssociatedDisplayId(DISPLAY_ID_1)
                     .build();
     private static final VirtualTouchscreenConfig TOUCHSCREEN_CONFIG =
-            new VirtualTouchscreenConfig.Builder()
+            new VirtualTouchscreenConfig.Builder(WIDTH, HEIGHT)
                     .setVendorId(VENDOR_ID)
                     .setProductId(PRODUCT_ID)
                     .setInputDeviceName(DEVICE_NAME)
                     .setAssociatedDisplayId(DISPLAY_ID_1)
-                    .setWidthInPixels(WIDTH)
-                    .setHeightInPixels(HEIGHT)
                     .build();
     private static final VirtualNavigationTouchpadConfig NAVIGATION_TOUCHPAD_CONFIG =
-            new VirtualNavigationTouchpadConfig.Builder(
-                    /* touchpadHeight= */ HEIGHT, /* touchpadWidth= */ WIDTH)
+            new VirtualNavigationTouchpadConfig.Builder(WIDTH, HEIGHT)
                     .setVendorId(VENDOR_ID)
                     .setProductId(PRODUCT_ID)
                     .setInputDeviceName(DEVICE_NAME)
@@ -195,6 +193,11 @@
                     .build();
     private static final String TEST_SITE = "http://test";
 
+    @Rule
+    public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule(
+            InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+            Manifest.permission.CREATE_VIRTUAL_DEVICE);
+
     private Context mContext;
     private InputManagerMockHelper mInputManagerMockHelper;
     private VirtualDeviceImpl mDeviceImpl;
@@ -304,10 +307,9 @@
         LocalServices.removeServiceForTest(DisplayManagerInternal.class);
         LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
 
-        mContext = Mockito.spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
+        mContext = Mockito.spy(new ContextWrapper(
+                InstrumentationRegistry.getInstrumentation().getTargetContext()));
         doReturn(mContext).when(mContext).createContextAsUser(eq(Process.myUserHandle()), anyInt());
-        doNothing().when(mContext).enforceCallingOrSelfPermission(
-                eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
         when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn(
                 mDevicePolicyManagerMock);
 
@@ -726,48 +728,28 @@
 
     @Test
     public void createVirtualTouchscreen_zeroDisplayDimension_failsIllegalArgumentException() {
-        mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
-        final VirtualTouchscreenConfig zeroConfig =
-                new VirtualTouchscreenConfig.Builder()
-                        .setVendorId(VENDOR_ID)
-                        .setProductId(PRODUCT_ID)
-                        .setInputDeviceName(DEVICE_NAME)
-                        .setAssociatedDisplayId(DISPLAY_ID_1)
-                        .setWidthInPixels(0)
-                        .setHeightInPixels(0)
-                        .build();
         assertThrows(IllegalArgumentException.class,
-                () -> mDeviceImpl.createVirtualTouchscreen(zeroConfig, BINDER));
+                () -> new VirtualTouchscreenConfig.Builder(
+                        /* touchscrenWidth= */ 0, /* touchscreenHeight= */ 0));
     }
 
     @Test
     public void createVirtualTouchscreen_negativeDisplayDimension_failsIllegalArgumentException() {
-        mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
-        final VirtualTouchscreenConfig negativeConfig =
-                new VirtualTouchscreenConfig.Builder()
-                        .setVendorId(VENDOR_ID)
-                        .setProductId(PRODUCT_ID)
-                        .setInputDeviceName(DEVICE_NAME)
-                        .setAssociatedDisplayId(DISPLAY_ID_1)
-                        .setWidthInPixels(-100)
-                        .setHeightInPixels(-100)
-                        .build();
         assertThrows(IllegalArgumentException.class,
-                () -> mDeviceImpl.createVirtualTouchscreen(negativeConfig, BINDER));
-
+                () -> new VirtualTouchscreenConfig.Builder(
+                        /* touchscrenWidth= */ -100, /* touchscreenHeight= */ -100));
     }
 
     @Test
     public void createVirtualTouchscreen_positiveDisplayDimension_successful() {
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
         VirtualTouchscreenConfig positiveConfig =
-                new VirtualTouchscreenConfig.Builder()
+                new VirtualTouchscreenConfig.Builder(
+                        /* touchscrenWidth= */ 600, /* touchscreenHeight= */ 800)
                         .setVendorId(VENDOR_ID)
                         .setProductId(PRODUCT_ID)
                         .setInputDeviceName(DEVICE_NAME)
                         .setAssociatedDisplayId(DISPLAY_ID_1)
-                        .setWidthInPixels(600)
-                        .setHeightInPixels(800)
                         .build();
         mDeviceImpl.createVirtualTouchscreen(positiveConfig, BINDER);
         assertWithMessage(
@@ -784,36 +766,16 @@
 
     @Test
     public void createVirtualNavigationTouchpad_zeroDisplayDimension_failsWithException() {
-        mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
         assertThrows(IllegalArgumentException.class,
-                () -> {
-                    final VirtualNavigationTouchpadConfig zeroConfig =
-                            new VirtualNavigationTouchpadConfig.Builder(
-                                    /* touchpadHeight= */ 0, /* touchpadWidth= */ 0)
-                                    .setVendorId(VENDOR_ID)
-                                    .setProductId(PRODUCT_ID)
-                                    .setInputDeviceName(DEVICE_NAME)
-                                    .setAssociatedDisplayId(DISPLAY_ID_1)
-                                    .build();
-                    mDeviceImpl.createVirtualNavigationTouchpad(zeroConfig, BINDER);
-                });
+                () -> new VirtualNavigationTouchpadConfig.Builder(
+                        /* touchpadHeight= */ 0, /* touchpadWidth= */ 0));
     }
 
     @Test
     public void createVirtualNavigationTouchpad_negativeDisplayDimension_failsWithException() {
-        mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
         assertThrows(IllegalArgumentException.class,
-                () -> {
-                    final VirtualNavigationTouchpadConfig negativeConfig =
-                            new VirtualNavigationTouchpadConfig.Builder(
-                                    /* touchpadHeight= */ -50, /* touchpadWidth= */ 50)
-                                    .setVendorId(VENDOR_ID)
-                                    .setProductId(PRODUCT_ID)
-                                    .setInputDeviceName(DEVICE_NAME)
-                                    .setAssociatedDisplayId(DISPLAY_ID_1)
-                                    .build();
-                    mDeviceImpl.createVirtualNavigationTouchpad(negativeConfig, BINDER);
-                });
+                () -> new VirtualNavigationTouchpadConfig.Builder(
+                        /* touchpadHeight= */ -50, /* touchpadWidth= */ 50));
     }
 
     @Test
@@ -844,76 +806,76 @@
     @Test
     public void createVirtualDpad_noPermission_failsSecurityException() {
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
-        doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
-                eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
-        assertThrows(SecurityException.class,
-                () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER));
+        try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+            assertThrows(SecurityException.class,
+                    () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER));
+        }
     }
 
     @Test
     public void createVirtualKeyboard_noPermission_failsSecurityException() {
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
-        doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
-                eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
-        assertThrows(SecurityException.class,
-                () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER));
+        try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+            assertThrows(SecurityException.class,
+                    () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER));
+        }
     }
 
     @Test
     public void createVirtualMouse_noPermission_failsSecurityException() {
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
-        doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
-                eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
-        assertThrows(SecurityException.class,
-                () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER));
+        try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+            assertThrows(SecurityException.class,
+                    () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER));
+        }
     }
 
     @Test
     public void createVirtualTouchscreen_noPermission_failsSecurityException() {
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
-        doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
-                eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
-        assertThrows(SecurityException.class,
-                () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER));
+        try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+            assertThrows(SecurityException.class,
+                    () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER));
+        }
     }
 
     @Test
     public void createVirtualNavigationTouchpad_noPermission_failsSecurityException() {
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
-        doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
-                eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
-        assertThrows(SecurityException.class,
-                () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG,
-                        BINDER));
+        try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+            assertThrows(SecurityException.class,
+                    () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG,
+                            BINDER));
+        }
     }
 
     @Test
     public void createVirtualSensor_noPermission_failsSecurityException() {
-        doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
-                eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
-        assertThrows(
-                SecurityException.class,
-                () -> mDeviceImpl.createVirtualSensor(
-                        BINDER,
-                        new VirtualSensorConfig.Builder(
-                                Sensor.TYPE_ACCELEROMETER, DEVICE_NAME).build()));
+        try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+            assertThrows(
+                    SecurityException.class,
+                    () -> mDeviceImpl.createVirtualSensor(
+                            BINDER,
+                            new VirtualSensorConfig.Builder(
+                                    Sensor.TYPE_ACCELEROMETER, DEVICE_NAME).build()));
+        }
     }
 
     @Test
     public void onAudioSessionStarting_noPermission_failsSecurityException() {
         mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1);
-        doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
-                eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
-        assertThrows(SecurityException.class,
-                () -> mDeviceImpl.onAudioSessionStarting(
-                        DISPLAY_ID_1, mRoutingCallback, mConfigChangedCallback));
+        try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+            assertThrows(SecurityException.class,
+                    () -> mDeviceImpl.onAudioSessionStarting(
+                            DISPLAY_ID_1, mRoutingCallback, mConfigChangedCallback));
+        }
     }
 
     @Test
     public void onAudioSessionEnded_noPermission_failsSecurityException() {
-        doCallRealMethod().when(mContext).enforceCallingOrSelfPermission(
-                eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString());
-        assertThrows(SecurityException.class, () -> mDeviceImpl.onAudioSessionEnded());
+        try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) {
+            assertThrows(SecurityException.class, () -> mDeviceImpl.onAudioSessionEnded());
+        }
     }
 
     @Test
@@ -1593,4 +1555,18 @@
         mVdms.addVirtualDevice(virtualDeviceImpl);
         return virtualDeviceImpl;
     }
+
+    /** Helper class to drop permissions temporarily and restore them at the end of a test. */
+    static final class DropShellPermissionsTemporarily implements AutoCloseable {
+        DropShellPermissionsTemporarily() {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                    .dropShellPermissionIdentity();
+        }
+
+        @Override
+        public void close() {
+            InstrumentationRegistry.getInstrumentation().getUiAutomation()
+                    .adoptShellPermissionIdentity();
+        }
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
index 112db76..68ab671 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
@@ -94,6 +94,7 @@
         mHdmiControlService.setCecController(hdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
         mHdmiControlService.initService();
+        mHdmiControlService.enableAllFeatureFlags();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
index e4eecc6..1b811ab 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
@@ -111,6 +111,7 @@
         hdmiControlService.setCecController(hdmiCecController);
         hdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(hdmiControlService));
         hdmiControlService.initService();
+        hdmiControlService.enableAllFeatureFlags();
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         hdmiControlService.setPowerManager(mPowerManager);
         mAction = new ArcInitiationActionFromAvr(mHdmiCecLocalDeviceAudioSystem);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
index 2cb46da..53be896 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
@@ -107,6 +107,7 @@
         hdmiControlService.setCecController(hdmiCecController);
         hdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(hdmiControlService));
         hdmiControlService.initService();
+        hdmiControlService.enableAllFeatureFlags();
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         hdmiControlService.setPowerManager(mPowerManager);
         mHdmiCecLocalDeviceAudioSystem = new HdmiCecLocalDeviceAudioSystem(hdmiControlService) {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java
index 8ff87e3..0e1545e 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java
@@ -165,6 +165,7 @@
         mHdmiControlService.setHdmiMhlController(
                 HdmiMhlControllerStub.create(mHdmiControlService));
         mHdmiControlService.initService();
+        mHdmiControlService.enableAllFeatureFlags();
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
index 3a57db9..3045ea0 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
@@ -110,6 +110,7 @@
         mHdmiControlService.setCecController(hdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
         mHdmiControlService.initService();
+        mHdmiControlService.enableAllFeatureFlags();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
index 6a899e8..d610676 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
@@ -133,6 +133,7 @@
         mHdmiControlService.setHdmiCecNetwork(mHdmiCecNetwork);
 
         mHdmiControlService.initService();
+        mHdmiControlService.enableAllFeatureFlags();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mNativeWrapper.setPhysicalAddress(0x0000);
         mPowerManager = new FakePowerManagerWrapper(context);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
index 0419768..14a83e0 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
@@ -143,6 +143,7 @@
                                  true, false, false);
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mHdmiControlService.initService();
+        mHdmiControlService.enableAllFeatureFlags();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
index d2fe6da..e6e479f 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
@@ -122,6 +122,7 @@
         mNativeWrapper.setPortConnectionStatus(1, true);
 
         mHdmiControlServiceSpy.initService();
+        mHdmiControlServiceSpy.enableAllFeatureFlags();
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlServiceSpy.setPowerManager(mPowerManager);
         mHdmiControlServiceSpy.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
index 367f41d..9b2e227 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
@@ -127,6 +127,7 @@
         localDevices.add(playbackDevice);
 
         mHdmiControlServiceSpy.initService();
+        mHdmiControlServiceSpy.enableAllFeatureFlags();
         mHdmiControlServiceSpy.allocateLogicalAddress(localDevices,
                 HdmiControlService.INITIATED_BY_ENABLE_CEC);
         mHdmiControlServiceSpy.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
index de2c218..22f7c28 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
@@ -209,6 +209,7 @@
                 4, HdmiPortInfo.PORT_INPUT, HDMI_3_PHYSICAL_ADDRESS, true, false, false);
         mNativeWrapper.setPortInfo(mHdmiPortInfo);
         mHdmiControlService.initService();
+        mHdmiControlService.enableAllFeatureFlags();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
index 3ed8983..c25d7c0 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
@@ -153,6 +153,7 @@
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mNativeWrapper.setPortConnectionStatus(1, true);
         mHdmiControlService.initService();
+        mHdmiControlService.enableAllFeatureFlags();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
index b30118c..4ff1b31 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
@@ -192,6 +192,7 @@
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mNativeWrapper.setPortConnectionStatus(1, true);
         mHdmiControlService.initService();
+        mHdmiControlService.enableAllFeatureFlags();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mNativeWrapper.setPhysicalAddress(0x2000);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 5dd29fd..c8f2afb 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -192,6 +192,7 @@
                 new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, true, true);
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mHdmiControlService.initService();
+        mHdmiControlService.enableAllFeatureFlags();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
index 4e5336e..23edee5 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
@@ -102,6 +102,7 @@
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mNativeWrapper.setPortConnectionStatus(1, true);
         mHdmiControlService.initService();
+        mHdmiControlService.enableAllFeatureFlags();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(contextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index aa49a62..b7cb347 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -141,6 +141,7 @@
                 new HdmiPortInfo(4, HdmiPortInfo.PORT_INPUT, 0x3000, true, false, false, false);
         mNativeWrapper.setPortInfo(mHdmiPortInfo);
         mHdmiControlServiceSpy.initService();
+        mHdmiControlServiceSpy.enableAllFeatureFlags();
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlServiceSpy.setPowerManager(mPowerManager);
         mHdmiControlServiceSpy.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
@@ -1203,6 +1204,7 @@
         mTestLooper.dispatchAll();
         Mockito.clearInvocations(mHdmiControlServiceSpy);
         mHdmiControlServiceSpy.initService();
+        mHdmiControlServiceSpy.enableAllFeatureFlags();
         mTestLooper.dispatchAll();
         verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(true, false);
         verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(eq(false), anyBoolean());
@@ -1216,6 +1218,7 @@
         mTestLooper.dispatchAll();
         Mockito.clearInvocations(mHdmiControlServiceSpy);
         mHdmiControlServiceSpy.initService();
+        mHdmiControlServiceSpy.enableAllFeatureFlags();
         mTestLooper.dispatchAll();
         verify(mHdmiControlServiceSpy, times(1)).setEarcEnabledInHal(false, false);
         verify(mHdmiControlServiceSpy, times(0)).setEarcEnabledInHal(eq(true), anyBoolean());
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java
index bf44e09..ab95c1a 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java
@@ -127,6 +127,7 @@
         mHdmiControlService.setCecController(mHdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
         mHdmiControlService.initService();
+        mHdmiControlService.enableAllFeatureFlags();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
index f72ac71..9d0faba 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
@@ -106,6 +106,7 @@
                 new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, false);
         mNativeWrapper.setPortInfo(hdmiPortInfo);
         mHdmiControlService.initService();
+        mHdmiControlService.enableAllFeatureFlags();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
index f719ca1..a2bf3c0 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
@@ -122,6 +122,7 @@
         mHdmiControlService.setCecController(mHdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
         mHdmiControlService.initService();
+        mHdmiControlService.enableAllFeatureFlags();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
index be62df8..f78bddc 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
@@ -183,6 +183,7 @@
                         true, false, false);
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mHdmiControlService.initService();
+        mHdmiControlService.enableAllFeatureFlags();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
index e3c8939..26f1517 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
@@ -99,6 +99,7 @@
         mHdmiControlServiceSpy.setHdmiMhlController(
                 HdmiMhlControllerStub.create(mHdmiControlServiceSpy));
         mHdmiControlServiceSpy.initService();
+        mHdmiControlServiceSpy.enableAllFeatureFlags();
         mHdmiControlServiceSpy.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlServiceSpy.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
index e7557fe..c1c43ae 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
@@ -106,6 +106,7 @@
                 new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, true);
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mHdmiControlService.initService();
+        mHdmiControlService.enableAllFeatureFlags();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
index c2f706a..a183d36 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
@@ -173,6 +173,7 @@
                 hdmiControlService, nativeWrapper, hdmiControlService.getAtomWriter());
         hdmiControlService.setCecController(hdmiCecController);
         hdmiControlService.initService();
+        hdmiControlService.enableAllFeatureFlags();
         mPowerManager = new FakePowerManagerWrapper(context);
         hdmiControlService.setPowerManager(mPowerManager);
         mHdmiCecLocalDeviceAudioSystem =
diff --git a/tests/Internal/src/com/android/internal/app/AppLocaleCollectorTest.java b/tests/Internal/src/com/android/internal/app/AppLocaleCollectorTest.java
new file mode 100644
index 0000000..b24ac3c
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/app/AppLocaleCollectorTest.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.app;
+
+import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyObject;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.app.LocaleStore.LocaleInfo;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Unit tests for the {@link AppLocaleCollector}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AppLocaleCollectorTest {
+    private static final String TAG = "AppLocaleCollectorTest";
+    private AppLocaleCollector mAppLocaleCollector;
+    private LocaleStore.LocaleInfo mAppCurrentLocale;
+    private Set<LocaleInfo> mAllAppActiveLocales;
+    private Set<LocaleInfo> mImeLocales;
+    private List<LocaleInfo> mSystemCurrentLocales;
+    private Set<LocaleInfo> mSystemSupportedLocales;
+    private AppLocaleStore.AppLocaleResult mResult;
+    private static final String PKG1 = "pkg1";
+    private static final int NONE = LocaleInfo.SUGGESTION_TYPE_NONE;
+    private static final int SIM = LocaleInfo.SUGGESTION_TYPE_SIM;
+    private static final int CFG = LocaleInfo.SUGGESTION_TYPE_CFG;
+    private static final int SIM_CFG = SIM | CFG;
+    private static final int CURRENT = LocaleInfo.SUGGESTION_TYPE_CURRENT;
+    private static final int SYSTEM = LocaleInfo.SUGGESTION_TYPE_SYSTEM_LANGUAGE;
+    private static final int OTHERAPP = LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE;
+
+    @Before
+    public void setUp() throws Exception {
+        mAppLocaleCollector = spy(
+                new AppLocaleCollector(InstrumentationRegistry.getContext(), PKG1));
+
+        mAppCurrentLocale = createLocaleInfo("en-US", CURRENT);
+        mAllAppActiveLocales = initAllAppActivatedLocales();
+        mImeLocales = initImeLocales();
+        mSystemSupportedLocales = initSystemSupportedLocales();
+        mSystemCurrentLocales = initSystemCurrentLocales();
+        mResult = new AppLocaleStore.AppLocaleResult(GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG,
+                initAppSupportedLocale());
+    }
+
+    @Test
+    public void testGetSupportedLocaleList() {
+        doReturn(mAppCurrentLocale).when(mAppLocaleCollector).getAppCurrentLocale();
+        doReturn(mResult).when(mAppLocaleCollector).getAppSupportedLocales();
+        doReturn(mAllAppActiveLocales).when(mAppLocaleCollector).getAllAppActiveLocales();
+        doReturn(mImeLocales).when(mAppLocaleCollector).getActiveImeLocales();
+        doReturn(mSystemSupportedLocales).when(mAppLocaleCollector).getSystemSupportedLocale(
+                anyObject(), eq(null), eq(true));
+        doReturn(mSystemCurrentLocales).when(mAppLocaleCollector).getSystemCurrentLocale();
+
+        Set<LocaleInfo> result = mAppLocaleCollector.getSupportedLocaleList(null, true, false);
+
+        HashMap<String, Integer> expectedResult = getExpectedResult();
+        assertEquals(result.size(), expectedResult.size());
+        for (LocaleInfo source : result) {
+            int suggestionFlags = expectedResult.getOrDefault(source.getId(), -1);
+            assertEquals(source.mSuggestionFlags, suggestionFlags);
+        }
+    }
+
+    private HashMap<String, Integer> getExpectedResult() {
+        HashMap<String, Integer> map = new HashMap<>();
+        map.put("en-US", CURRENT); // The locale current App activates.
+        map.put("fr", NONE); // The locale App and system support.
+        map.put("zu", NONE); // The locale App and system support.
+        map.put("en", NONE); // Use en because System supports en while APP supports en-CA, en-GB.
+        map.put("ko", NONE); // The locale App and system support.
+        map.put("en-AU", OTHERAPP); // The locale other App activates and current App supports.
+        map.put("en-CA", OTHERAPP); // The locale other App activates and current App supports.
+        map.put("ja-JP", OTHERAPP); // The locale other App activates and current App supports.
+        map.put("zh-Hant-TW", SIM);  // The locale system activates.
+        map.put(createLocaleInfo("", SYSTEM).getId(), SYSTEM); // System language title
+        return map;
+    }
+
+    private Set<LocaleInfo> initSystemSupportedLocales() {
+        return Set.of(
+                createLocaleInfo("en", NONE),
+                createLocaleInfo("fr", NONE),
+                createLocaleInfo("zu", NONE),
+                createLocaleInfo("ko", NONE),
+                // will be filtered because current App doesn't support.
+                createLocaleInfo("es-US", SIM_CFG)
+        );
+    }
+
+    private List<LocaleInfo> initSystemCurrentLocales() {
+        return List.of(createLocaleInfo("zh-Hant-TW", SIM),
+                // will be filtered because current App activates this locale.
+                createLocaleInfo("en-US", SIM));
+    }
+
+    private Set<LocaleInfo> initAllAppActivatedLocales() {
+        return Set.of(
+                createLocaleInfo("en-CA", OTHERAPP),
+                createLocaleInfo("en-AU", OTHERAPP),
+                createLocaleInfo("ja-JP", OTHERAPP),
+                // will be filtered because current App activates this locale.
+                createLocaleInfo("en-US", OTHERAPP));
+    }
+
+    private Set<LocaleInfo> initImeLocales() {
+        return Set.of(
+                // will be filtered because system activates zh-Hant-TW.
+                createLocaleInfo("zh-TW", OTHERAPP),
+                // will be filtered because current App's activats this locale.
+                createLocaleInfo("en-US", OTHERAPP));
+    }
+
+    private HashSet<Locale> initAppSupportedLocale() {
+        HashSet<Locale> hs = new HashSet();
+        hs.add(Locale.forLanguageTag("en-US"));
+        hs.add(Locale.forLanguageTag("en-CA"));
+        hs.add(Locale.forLanguageTag("en-GB"));
+        hs.add(Locale.forLanguageTag("zh-TW"));
+        hs.add(Locale.forLanguageTag("ja"));
+        hs.add(Locale.forLanguageTag("fr"));
+        hs.add(Locale.forLanguageTag("zu"));
+        hs.add(Locale.forLanguageTag("ko"));
+        // will be filtered because it's not in the system language.
+        hs.add(Locale.forLanguageTag("mn"));
+        return hs;
+    }
+
+    private LocaleInfo createLocaleInfo(String languageTag, int suggestionFlag) {
+        LocaleInfo localeInfo = LocaleStore.fromLocale(Locale.forLanguageTag(languageTag));
+        localeInfo.mSuggestionFlags = suggestionFlag;
+        localeInfo.setTranslated(true);
+        return localeInfo;
+    }
+}
diff --git a/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java b/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java
new file mode 100644
index 0000000..bf6ece1
--- /dev/null
+++ b/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.app;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.view.inputmethod.InputMethodSubtype;
+import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.app.LocaleStore.LocaleInfo;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Unit tests for the {@link LocaleStore}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LocaleStoreTest {
+    @Before
+    public void setUp() {
+    }
+
+    @Test
+    public void testTransformImeLanguageTagToLocaleInfo() {
+        List<InputMethodSubtype> list = List.of(
+                new InputMethodSubtypeBuilder().setLanguageTag("en-US").build(),
+                new InputMethodSubtypeBuilder().setLanguageTag("zh-TW").build(),
+                new InputMethodSubtypeBuilder().setLanguageTag("ja-JP").build());
+
+        Set<LocaleInfo> localeSet = LocaleStore.transformImeLanguageTagToLocaleInfo(list);
+
+        Set<String> expectedLanguageTag = Set.of("en-US", "zh-TW", "ja-JP");
+        assertEquals(localeSet.size(), expectedLanguageTag.size());
+        for (LocaleInfo info : localeSet) {
+            assertEquals(info.mSuggestionFlags, LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE);
+            assertTrue(expectedLanguageTag.contains(info.getId()));
+        }
+    }
+}