Merge "Don't expand notifications when bouncer visible" into main
diff --git a/Android.bp b/Android.bp
index f1a3af2..57a5a3c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -621,7 +621,6 @@
     "--api-lint-ignore-prefix org. " +
     "--error NoSettingsProvider " +
     "--error UnhiddenSystemApi " +
-    "--error UnflaggedApi " +
     "--force-convert-to-warning-nullability-annotations +*:-android.*:+android.icu.*:-dalvik.* " +
     "--hide BroadcastBehavior " +
     "--hide CallbackInterface " +
diff --git a/OWNERS b/OWNERS
index 6c25324..4e5c7d8 100644
--- a/OWNERS
+++ b/OWNERS
@@ -28,7 +28,7 @@
 # Support bulk translation updates
 per-file */res*/values*/*.xml = byi@google.com, delphij@google.com
 
-per-file **.bp,**.mk = hansson@google.com, joeo@google.com
+per-file **.bp,**.mk = hansson@google.com, joeo@google.com, lamontjones@google.com
 per-file TestProtoLibraries.bp = file:platform/platform_testing:/libraries/health/OWNERS
 per-file TestProtoLibraries.bp = file:platform/tools/tradefederation:/OWNERS
 
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
index dcc324d..5c60562 100644
--- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
+++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java
@@ -75,7 +75,7 @@
     private static final String ALARM_TAG_AFFORDABILITY_CHECK = "*tare.affordability_check*";
 
     private final Object mLock;
-    private final Handler mHandler;
+    private final AgentHandler mHandler;
     private final Analyst mAnalyst;
     private final InternalResourceService mIrs;
     private final Scribe mScribe;
@@ -992,6 +992,7 @@
     void tearDownLocked() {
         mCurrentOngoingEvents.clear();
         mBalanceThresholdAlarmQueue.removeAllAlarms();
+        mHandler.removeAllMessages();
     }
 
     @VisibleForTesting
@@ -1290,6 +1291,11 @@
                 break;
             }
         }
+
+        void removeAllMessages() {
+            removeMessages(MSG_CHECK_ALL_AFFORDABILITY);
+            removeMessages(MSG_CHECK_INDIVIDUAL_AFFORDABILITY);
+        }
     }
 
     @GuardedBy("mLock")
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index c59a833..2f84df7 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -33,7 +33,7 @@
         "android-non-updatable-stubs-defaults",
         "module-classpath-stubs-defaults",
     ],
-    args: metalava_framework_docs_args,
+    args: metalava_framework_docs_args + "--error UnflaggedApi ",
     check_api: {
         current: {
             api_file: ":non-updatable-current.txt",
diff --git a/core/api/current.txt b/core/api/current.txt
index 8de8ab8..d92c693 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -15096,7 +15096,7 @@
     method public int getByteCount();
     method @NonNull public android.graphics.Color getColor(int, int);
     method @Nullable public android.graphics.ColorSpace getColorSpace();
-    method @NonNull public android.graphics.Bitmap.Config getConfig();
+    method @Nullable public android.graphics.Bitmap.Config getConfig();
     method public int getDensity();
     method @Nullable public android.graphics.Gainmap getGainmap();
     method public int getGenerationId();
@@ -38645,7 +38645,7 @@
   public final class FileIntegrityManager {
     method @FlaggedApi(Flags.FLAG_FSVERITY_API) @Nullable public byte[] getFsVerityDigest(@NonNull java.io.File) throws java.io.IOException;
     method public boolean isApkVeritySupported();
-    method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public boolean isAppSourceCertificateTrusted(@NonNull java.security.cert.X509Certificate) throws java.security.cert.CertificateEncodingException;
+    method @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public boolean isAppSourceCertificateTrusted(@NonNull java.security.cert.X509Certificate) throws java.security.cert.CertificateEncodingException;
     method @FlaggedApi(Flags.FLAG_FSVERITY_API) public void setupFsVerity(@NonNull java.io.File) throws java.io.IOException;
   }
 
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index ff44a1b..eba1fbe 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3192,6 +3192,7 @@
 
   public static class VirtualDeviceManager.VirtualDevice implements java.lang.AutoCloseable {
     method public void addActivityListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
+    method @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY) @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void addActivityPolicyExemption(@NonNull android.content.ComponentName);
     method public void addSoundEffectListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.SoundEffectListener);
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
     method @NonNull public android.content.Context createContext();
@@ -3212,6 +3213,7 @@
     method public void launchPendingIntent(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void registerIntentInterceptor(@NonNull android.content.IntentFilter, @NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback);
     method public void removeActivityListener(@NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
+    method @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY) @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void removeActivityPolicyExemption(@NonNull android.content.ComponentName);
     method public void removeSoundEffectListener(@NonNull android.companion.virtual.VirtualDeviceManager.SoundEffectListener);
     method @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY) @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setDevicePolicy(int, int);
     method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setShowPointerIcon(boolean);
@@ -3243,6 +3245,7 @@
     field public static final int LOCK_STATE_DEFAULT = 0; // 0x0
     field public static final int NAVIGATION_POLICY_DEFAULT_ALLOWED = 0; // 0x0
     field public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1; // 0x1
+    field @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY) public static final int POLICY_TYPE_ACTIVITY = 3; // 0x3
     field public static final int POLICY_TYPE_AUDIO = 1; // 0x1
     field public static final int POLICY_TYPE_RECENTS = 2; // 0x2
     field public static final int POLICY_TYPE_SENSORS = 0; // 0x0
@@ -12654,7 +12657,7 @@
     method @NonNull public android.service.voice.HotwordTrainingAudio build();
     method @NonNull public android.service.voice.HotwordTrainingAudio.Builder setAudioFormat(@NonNull android.media.AudioFormat);
     method @NonNull public android.service.voice.HotwordTrainingAudio.Builder setAudioType(@NonNull int);
-    method @NonNull public android.service.voice.HotwordTrainingAudio.Builder setHotwordAudio(@NonNull byte...);
+    method @NonNull public android.service.voice.HotwordTrainingAudio.Builder setHotwordAudio(@NonNull byte[]);
     method @NonNull public android.service.voice.HotwordTrainingAudio.Builder setHotwordOffsetMillis(int);
   }
 
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 39589fa..f28b4b4 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -36,7 +36,6 @@
 import static android.window.ConfigurationHelper.freeTextLayoutCachesIfNeeded;
 import static android.window.ConfigurationHelper.isDifferentDisplay;
 import static android.window.ConfigurationHelper.shouldUpdateResources;
-
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 import static com.android.internal.os.SafeZipPathValidatorCallback.VALIDATE_ZIP_PATH_FOR_PATH_TRAVERSAL;
 
@@ -1286,8 +1285,13 @@
         }
 
         private void updateCompatOverrideScale(CompatibilityInfo info) {
-            CompatibilityInfo.setOverrideInvertedScale(
-                    info.hasOverrideScaling() ? info.applicationInvertedScale : 1f);
+            if (info.hasOverrideScaling()) {
+                CompatibilityInfo.setOverrideInvertedScale(info.applicationInvertedScale,
+                        info.applicationDensityInvertedScale);
+            } else {
+                CompatibilityInfo.setOverrideInvertedScale(/* invertScale */ 1f,
+                        /* densityInvertScale */1f);
+            }
         }
 
         public final void runIsolatedEntryPoint(String entryPoint, String[] entryPointArgs) {
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index c58561d..bf00a5a 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -23,6 +23,7 @@
 import android.companion.virtual.sensor.VirtualSensor;
 import android.companion.virtual.sensor.VirtualSensorConfig;
 import android.companion.virtual.sensor.VirtualSensorEvent;
+import android.content.ComponentName;
 import android.content.IntentFilter;
 import android.graphics.Point;
 import android.graphics.PointF;
@@ -86,6 +87,18 @@
     void setDevicePolicy(int policyType, int devicePolicy);
 
     /**
+     * Adds an exemption to the default activity launch policy.
+     */
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
+    void addActivityPolicyExemption(in ComponentName exemption);
+
+    /**
+     * Removes an exemption to the default activity launch policy.
+     */
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
+    void removeActivityPolicyExemption(in ComponentName exemption);
+
+    /**
      * Notifies that an audio session being started.
      */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index 2e5c0f7..7bf2e91 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -247,6 +247,22 @@
         }
     }
 
+    void addActivityPolicyExemption(@NonNull ComponentName componentName) {
+        try {
+            mVirtualDevice.addActivityPolicyExemption(componentName);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    void removeActivityPolicyExemption(@NonNull ComponentName componentName) {
+        try {
+            mVirtualDevice.removeActivityPolicyExemption(componentName);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     @NonNull
     VirtualDpad createVirtualDpad(@NonNull VirtualDpadConfig config) {
         try {
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 7b81031..d338d17 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -62,7 +62,6 @@
 import android.view.Surface;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.AnnotationValidations;
 
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
@@ -624,19 +623,62 @@
          * @param devicePolicy the value of the policy, i.e. how to interpret the device behavior.
          *
          * @see VirtualDeviceParams#POLICY_TYPE_RECENTS
+         * @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY
          */
         @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY)
         @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
         public void setDevicePolicy(@VirtualDeviceParams.DynamicPolicyType int policyType,
                 @VirtualDeviceParams.DevicePolicy int devicePolicy) {
-            AnnotationValidations.validate(
-                    VirtualDeviceParams.DynamicPolicyType.class, null, policyType);
-            AnnotationValidations.validate(
-                    VirtualDeviceParams.DevicePolicy.class, null, devicePolicy);
             mVirtualDeviceInternal.setDevicePolicy(policyType, devicePolicy);
         }
 
         /**
+         * Specifies a component name to be exempt from the current activity launch policy.
+         *
+         * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVIY} allows activity
+         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT},
+         * then the specified component will be blocked from launching.
+         * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity
+         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}, then
+         * the specified component will be allowed to launch.</p>
+         *
+         * <p>Note that changing the activity launch policy will not affect current set of exempt
+         * components and it needs to be updated separately.</p>
+         *
+         * @see #removeActivityPolicyExemption
+         * @see #setDevicePolicy
+         */
+        @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY)
+        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+        public void addActivityPolicyExemption(@NonNull ComponentName componentName) {
+            mVirtualDeviceInternal.addActivityPolicyExemption(
+                    Objects.requireNonNull(componentName));
+        }
+
+        /**
+         * Makes the specified component name to adhere to the default activity launch policy.
+         *
+         * <p>If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVIY} allows activity
+         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_DEFAULT},
+         * then the specified component will be allowed to launch.
+         * If the current {@link VirtualDeviceParams#POLICY_TYPE_ACTIVITY} blocks activity
+         * launches by default, (i.e. it is {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM}, then
+         * the specified component will be blocked from launching.</p>
+         *
+         * <p>Note that changing the activity launch policy will not affect current set of exempt
+         * components and it needs to be updated separately.</p>
+         *
+         * @see #addActivityPolicyExemption
+         * @see #setDevicePolicy
+         */
+        @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY)
+        @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+        public void removeActivityPolicyExemption(@NonNull ComponentName componentName) {
+            mVirtualDeviceInternal.removeActivityPolicyExemption(
+                    Objects.requireNonNull(componentName));
+        }
+
+        /**
          * Creates a virtual dpad.
          *
          * @param config the configurations of the virtual dpad.
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index 51df257..b4c740ec 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -22,12 +22,14 @@
 import static java.util.concurrent.TimeUnit.MICROSECONDS;
 
 import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
 import android.companion.virtual.sensor.IVirtualSensorCallback;
 import android.companion.virtual.sensor.VirtualSensor;
 import android.companion.virtual.sensor.VirtualSensorCallback;
@@ -144,7 +146,7 @@
      * @hide
      */
     @IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_SENSORS, POLICY_TYPE_AUDIO,
-            POLICY_TYPE_RECENTS})
+            POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY})
     @Retention(RetentionPolicy.SOURCE)
     @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
     public @interface PolicyType {}
@@ -155,7 +157,7 @@
      * @see VirtualDeviceManager.VirtualDevice#setDevicePolicy
      * @hide
      */
-    @IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_RECENTS})
+    @IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY})
     @Retention(RetentionPolicy.SOURCE)
     @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
     public @interface DynamicPolicyType {}
@@ -195,19 +197,35 @@
      *     <li>{@link #DEVICE_POLICY_DEFAULT}: Activities launched on VirtualDisplays owned by this
      *     device will appear in the host device recents.
      *     <li>{@link #DEVICE_POLICY_CUSTOM}: Activities launched on VirtualDisplays owned by this
-     *      *     device will not appear in recents.
+     *     device will not appear in recents.
      * </ul>
      */
     public static final int POLICY_TYPE_RECENTS = 2;
 
+    /**
+     * Tells the activity manager what the default launch behavior for activities on this device is.
+     *
+     * <ul>
+     *     <li>{@link #DEVICE_POLICY_DEFAULT}: Activities are allowed to be launched on displays
+     *     owned by this device, unless explicitly blocked by the device.
+     *     <li>{@link #DEVICE_POLICY_CUSTOM}: Activities are blocked from launching on displays
+     *     owned by this device, unless explicitly allowed by the device.
+     * </ul>
+     *
+     * @see VirtualDeviceManager.VirtualDevice#addActivityPolicyExemption
+     * @see VirtualDeviceManager.VirtualDevice#removeActivityPolicyExemption
+     */
+    @FlaggedApi(Flags.FLAG_DYNAMIC_POLICY)
+    public static final int POLICY_TYPE_ACTIVITY = 3;
+
     private final int mLockState;
     @NonNull private final ArraySet<UserHandle> mUsersWithMatchingAccounts;
     @NavigationPolicy
     private final int mDefaultNavigationPolicy;
-    @NonNull private final ArraySet<ComponentName> mCrossTaskNavigationExceptions;
+    @NonNull private final ArraySet<ComponentName> mCrossTaskNavigationExemptions;
     @ActivityPolicy
     private final int mDefaultActivityPolicy;
-    @NonNull private final ArraySet<ComponentName> mActivityPolicyExceptions;
+    @NonNull private final ArraySet<ComponentName> mActivityPolicyExemptions;
     @Nullable private final String mName;
     // Mapping of @PolicyType to @DevicePolicy
     @NonNull private final SparseIntArray mDevicePolicies;
@@ -220,9 +238,9 @@
             @LockState int lockState,
             @NonNull Set<UserHandle> usersWithMatchingAccounts,
             @NavigationPolicy int defaultNavigationPolicy,
-            @NonNull Set<ComponentName> crossTaskNavigationExceptions,
+            @NonNull Set<ComponentName> crossTaskNavigationExemptions,
             @ActivityPolicy int defaultActivityPolicy,
-            @NonNull Set<ComponentName> activityPolicyExceptions,
+            @NonNull Set<ComponentName> activityPolicyExemptions,
             @Nullable String name,
             @NonNull SparseIntArray devicePolicies,
             @NonNull List<VirtualSensorConfig> virtualSensorConfigs,
@@ -233,11 +251,11 @@
         mUsersWithMatchingAccounts =
                 new ArraySet<>(Objects.requireNonNull(usersWithMatchingAccounts));
         mDefaultNavigationPolicy = defaultNavigationPolicy;
-        mCrossTaskNavigationExceptions =
-                new ArraySet<>(Objects.requireNonNull(crossTaskNavigationExceptions));
+        mCrossTaskNavigationExemptions =
+                new ArraySet<>(Objects.requireNonNull(crossTaskNavigationExemptions));
         mDefaultActivityPolicy = defaultActivityPolicy;
-        mActivityPolicyExceptions =
-                new ArraySet<>(Objects.requireNonNull(activityPolicyExceptions));
+        mActivityPolicyExemptions =
+                new ArraySet<>(Objects.requireNonNull(activityPolicyExemptions));
         mName = name;
         mDevicePolicies = Objects.requireNonNull(devicePolicies);
         mVirtualSensorConfigs = Objects.requireNonNull(virtualSensorConfigs);
@@ -251,9 +269,9 @@
         mLockState = parcel.readInt();
         mUsersWithMatchingAccounts = (ArraySet<UserHandle>) parcel.readArraySet(null);
         mDefaultNavigationPolicy = parcel.readInt();
-        mCrossTaskNavigationExceptions = (ArraySet<ComponentName>) parcel.readArraySet(null);
+        mCrossTaskNavigationExemptions = (ArraySet<ComponentName>) parcel.readArraySet(null);
         mDefaultActivityPolicy = parcel.readInt();
-        mActivityPolicyExceptions = (ArraySet<ComponentName>) parcel.readArraySet(null);
+        mActivityPolicyExemptions = (ArraySet<ComponentName>) parcel.readArraySet(null);
         mName = parcel.readString8();
         mDevicePolicies = parcel.readSparseIntArray();
         mVirtualSensorConfigs = new ArrayList<>();
@@ -295,7 +313,7 @@
     public Set<ComponentName> getAllowedCrossTaskNavigations() {
         return mDefaultNavigationPolicy == NAVIGATION_POLICY_DEFAULT_ALLOWED
                 ? Collections.emptySet()
-                : Collections.unmodifiableSet(mCrossTaskNavigationExceptions);
+                : Collections.unmodifiableSet(mCrossTaskNavigationExemptions);
     }
 
     /**
@@ -310,7 +328,7 @@
     public Set<ComponentName> getBlockedCrossTaskNavigations() {
         return mDefaultNavigationPolicy == NAVIGATION_POLICY_DEFAULT_BLOCKED
                 ? Collections.emptySet()
-                : Collections.unmodifiableSet(mCrossTaskNavigationExceptions);
+                : Collections.unmodifiableSet(mCrossTaskNavigationExemptions);
     }
 
     /**
@@ -336,7 +354,7 @@
     public Set<ComponentName> getAllowedActivities() {
         return mDefaultActivityPolicy == ACTIVITY_POLICY_DEFAULT_ALLOWED
                 ? Collections.emptySet()
-                : Collections.unmodifiableSet(mActivityPolicyExceptions);
+                : Collections.unmodifiableSet(mActivityPolicyExemptions);
     }
 
     /**
@@ -349,7 +367,7 @@
     public Set<ComponentName> getBlockedActivities() {
         return mDefaultActivityPolicy == ACTIVITY_POLICY_DEFAULT_BLOCKED
                 ? Collections.emptySet()
-                : Collections.unmodifiableSet(mActivityPolicyExceptions);
+                : Collections.unmodifiableSet(mActivityPolicyExemptions);
     }
 
     /**
@@ -440,9 +458,9 @@
         dest.writeInt(mLockState);
         dest.writeArraySet(mUsersWithMatchingAccounts);
         dest.writeInt(mDefaultNavigationPolicy);
-        dest.writeArraySet(mCrossTaskNavigationExceptions);
+        dest.writeArraySet(mCrossTaskNavigationExemptions);
         dest.writeInt(mDefaultActivityPolicy);
-        dest.writeArraySet(mActivityPolicyExceptions);
+        dest.writeArraySet(mActivityPolicyExemptions);
         dest.writeString8(mName);
         dest.writeSparseIntArray(mDevicePolicies);
         dest.writeTypedList(mVirtualSensorConfigs);
@@ -476,9 +494,9 @@
         return mLockState == that.mLockState
                 && mUsersWithMatchingAccounts.equals(that.mUsersWithMatchingAccounts)
                 && Objects.equals(
-                        mCrossTaskNavigationExceptions, that.mCrossTaskNavigationExceptions)
+                        mCrossTaskNavigationExemptions, that.mCrossTaskNavigationExemptions)
                 && mDefaultNavigationPolicy == that.mDefaultNavigationPolicy
-                && Objects.equals(mActivityPolicyExceptions, that.mActivityPolicyExceptions)
+                && Objects.equals(mActivityPolicyExemptions, that.mActivityPolicyExemptions)
                 && mDefaultActivityPolicy == that.mDefaultActivityPolicy
                 && Objects.equals(mName, that.mName)
                 && mAudioPlaybackSessionId == that.mAudioPlaybackSessionId
@@ -488,8 +506,8 @@
     @Override
     public int hashCode() {
         int hashCode = Objects.hash(
-                mLockState, mUsersWithMatchingAccounts, mCrossTaskNavigationExceptions,
-                mDefaultNavigationPolicy, mActivityPolicyExceptions, mDefaultActivityPolicy, mName,
+                mLockState, mUsersWithMatchingAccounts, mCrossTaskNavigationExemptions,
+                mDefaultNavigationPolicy, mActivityPolicyExemptions, mDefaultActivityPolicy, mName,
                 mDevicePolicies, mAudioPlaybackSessionId, mAudioRecordingSessionId);
         for (int i = 0; i < mDevicePolicies.size(); i++) {
             hashCode = 31 * hashCode + mDevicePolicies.keyAt(i);
@@ -505,9 +523,9 @@
                 + " mLockState=" + mLockState
                 + " mUsersWithMatchingAccounts=" + mUsersWithMatchingAccounts
                 + " mDefaultNavigationPolicy=" + mDefaultNavigationPolicy
-                + " mCrossTaskNavigationExceptions=" + mCrossTaskNavigationExceptions
+                + " mCrossTaskNavigationExemptions=" + mCrossTaskNavigationExemptions
                 + " mDefaultActivityPolicy=" + mDefaultActivityPolicy
-                + " mActivityPolicyExceptions=" + mActivityPolicyExceptions
+                + " mActivityPolicyExemptions=" + mActivityPolicyExemptions
                 + " mName=" + mName
                 + " mDevicePolicies=" + mDevicePolicies
                 + " mAudioPlaybackSessionId=" + mAudioPlaybackSessionId
@@ -524,9 +542,9 @@
         pw.println(prefix + "mLockState=" + mLockState);
         pw.println(prefix + "mUsersWithMatchingAccounts=" + mUsersWithMatchingAccounts);
         pw.println(prefix + "mDefaultNavigationPolicy=" + mDefaultNavigationPolicy);
-        pw.println(prefix + "mCrossTaskNavigationExceptions=" + mCrossTaskNavigationExceptions);
+        pw.println(prefix + "mCrossTaskNavigationExemptions=" + mCrossTaskNavigationExemptions);
         pw.println(prefix + "mDefaultActivityPolicy=" + mDefaultActivityPolicy);
-        pw.println(prefix + "mActivityPolicyExceptions=" + mActivityPolicyExceptions);
+        pw.println(prefix + "mActivityPolicyExemptions=" + mActivityPolicyExemptions);
         pw.println(prefix + "mDevicePolicies=" + mDevicePolicies);
         pw.println(prefix + "mVirtualSensorConfigs=" + mVirtualSensorConfigs);
         pw.println(prefix + "mAudioPlaybackSessionId=" + mAudioPlaybackSessionId);
@@ -552,11 +570,11 @@
 
         private @LockState int mLockState = LOCK_STATE_DEFAULT;
         @NonNull private Set<UserHandle> mUsersWithMatchingAccounts = Collections.emptySet();
-        @NonNull private Set<ComponentName> mCrossTaskNavigationExceptions = Collections.emptySet();
+        @NonNull private Set<ComponentName> mCrossTaskNavigationExemptions = Collections.emptySet();
         @NavigationPolicy
         private int mDefaultNavigationPolicy = NAVIGATION_POLICY_DEFAULT_ALLOWED;
         private boolean mDefaultNavigationPolicyConfigured = false;
-        @NonNull private Set<ComponentName> mActivityPolicyExceptions = Collections.emptySet();
+        @NonNull private Set<ComponentName> mActivityPolicyExemptions = Collections.emptySet();
         @ActivityPolicy
         private int mDefaultActivityPolicy = ACTIVITY_POLICY_DEFAULT_ALLOWED;
         private boolean mDefaultActivityPolicyConfigured = false;
@@ -700,7 +718,7 @@
             }
             mDefaultNavigationPolicy = NAVIGATION_POLICY_DEFAULT_BLOCKED;
             mDefaultNavigationPolicyConfigured = true;
-            mCrossTaskNavigationExceptions = Objects.requireNonNull(allowedCrossTaskNavigations);
+            mCrossTaskNavigationExemptions = Objects.requireNonNull(allowedCrossTaskNavigations);
             return this;
         }
 
@@ -731,7 +749,7 @@
             }
             mDefaultNavigationPolicy = NAVIGATION_POLICY_DEFAULT_ALLOWED;
             mDefaultNavigationPolicyConfigured = true;
-            mCrossTaskNavigationExceptions = Objects.requireNonNull(blockedCrossTaskNavigations);
+            mCrossTaskNavigationExemptions = Objects.requireNonNull(blockedCrossTaskNavigations);
             return this;
         }
 
@@ -757,7 +775,7 @@
             }
             mDefaultActivityPolicy = ACTIVITY_POLICY_DEFAULT_BLOCKED;
             mDefaultActivityPolicyConfigured = true;
-            mActivityPolicyExceptions = Objects.requireNonNull(allowedActivities);
+            mActivityPolicyExemptions = Objects.requireNonNull(allowedActivities);
             return this;
         }
 
@@ -783,7 +801,7 @@
             }
             mDefaultActivityPolicy = ACTIVITY_POLICY_DEFAULT_ALLOWED;
             mDefaultActivityPolicyConfigured = true;
-            mActivityPolicyExceptions = Objects.requireNonNull(blockedActivities);
+            mActivityPolicyExemptions = Objects.requireNonNull(blockedActivities);
             return this;
         }
 
@@ -956,6 +974,35 @@
                         mVirtualSensorDirectChannelCallback);
             }
 
+            if (Flags.dynamicPolicy()) {
+                switch (mDevicePolicies.get(POLICY_TYPE_ACTIVITY, -1)) {
+                    case DEVICE_POLICY_DEFAULT:
+                        if (mDefaultActivityPolicyConfigured
+                                && mDefaultActivityPolicy == ACTIVITY_POLICY_DEFAULT_BLOCKED) {
+                            throw new IllegalArgumentException(
+                                    "DEVICE_POLICY_DEFAULT is explicitly configured for "
+                                            + "POLICY_TYPE_ACTIVITY, which is exclusive with "
+                                            + "setAllowedActivities.");
+                        }
+                        break;
+                    case DEVICE_POLICY_CUSTOM:
+                        if (mDefaultActivityPolicyConfigured
+                                && mDefaultActivityPolicy == ACTIVITY_POLICY_DEFAULT_ALLOWED) {
+                            throw new IllegalArgumentException(
+                                    "DEVICE_POLICY_CUSTOM is explicitly configured for "
+                                            + "POLICY_TYPE_ACTIVITY, which is exclusive with "
+                                            + "setBlockedActivities.");
+                        }
+                        break;
+                    default:
+                        if (mDefaultActivityPolicyConfigured
+                                && mDefaultActivityPolicy == ACTIVITY_POLICY_DEFAULT_BLOCKED) {
+                            mDevicePolicies.put(POLICY_TYPE_ACTIVITY, DEVICE_POLICY_CUSTOM);
+                        }
+                        break;
+                }
+            }
+
             if ((mAudioPlaybackSessionId != AUDIO_SESSION_ID_GENERATE
                     || mAudioRecordingSessionId != AUDIO_SESSION_ID_GENERATE)
                     && mDevicePolicies.get(POLICY_TYPE_AUDIO, DEVICE_POLICY_DEFAULT)
@@ -964,7 +1011,7 @@
                         + "required for configuration of device-specific audio session ids.");
             }
 
-            SparseArray<Set<String>> sensorNameByType = new SparseArray();
+            SparseArray<Set<String>> sensorNameByType = new SparseArray<>();
             for (int i = 0; i < mVirtualSensorConfigs.size(); ++i) {
                 VirtualSensorConfig config = mVirtualSensorConfigs.get(i);
                 Set<String> sensorNames = sensorNameByType.get(config.getType(), new ArraySet<>());
@@ -979,9 +1026,9 @@
                     mLockState,
                     mUsersWithMatchingAccounts,
                     mDefaultNavigationPolicy,
-                    mCrossTaskNavigationExceptions,
+                    mCrossTaskNavigationExemptions,
                     mDefaultActivityPolicy,
-                    mActivityPolicyExceptions,
+                    mActivityPolicyExemptions,
                     mName,
                     mDevicePolicies,
                     mVirtualSensorConfigs,
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index 3a3ab24..ee36f18 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -27,4 +27,3 @@
     description: "Enable Virtual Camera"
     bug: "270352264"
 }
-
diff --git a/core/java/android/content/res/CompatibilityInfo.java b/core/java/android/content/res/CompatibilityInfo.java
index 08ba5b6..f929c1f 100644
--- a/core/java/android/content/res/CompatibilityInfo.java
+++ b/core/java/android/content/res/CompatibilityInfo.java
@@ -100,7 +100,7 @@
      * The effective screen density we have selected for this application.
      */
     public final int applicationDensity;
-    
+
     /**
      * Application's scale.
      */
@@ -112,9 +112,27 @@
      */
     public final float applicationInvertedScale;
 
+    /**
+     * Application's density scale.
+     *
+     * <p>In most cases this is equal to {@link #applicationScale}, but in some cases e.g.
+     * Automotive the requirement is to just scale the density and keep the resolution the same.
+     * This is used for artificially making apps look zoomed in to compensate for the user distance
+     * from the screen.
+     */
+    public final float applicationDensityScale;
+
+    /**
+     * Application's density inverted scale.
+     */
+    public final float applicationDensityInvertedScale;
+
     /** The process level override inverted scale. See {@link #HAS_OVERRIDE_SCALING}. */
     private static float sOverrideInvertedScale = 1f;
 
+    /** The process level override inverted density scale. See {@link #HAS_OVERRIDE_SCALING}. */
+    private static float sOverrideDensityInvertScale = 1f;
+
     @UnsupportedAppUsage
     @Deprecated
     public CompatibilityInfo(ApplicationInfo appInfo, int screenLayout, int sw,
@@ -123,17 +141,24 @@
     }
 
     public CompatibilityInfo(ApplicationInfo appInfo, int screenLayout, int sw,
-            boolean forceCompat, float overrideScale) {
+            boolean forceCompat, float scaleFactor) {
+        this(appInfo, screenLayout, sw, forceCompat, scaleFactor, scaleFactor);
+    }
+
+    public CompatibilityInfo(ApplicationInfo appInfo, int screenLayout, int sw,
+            boolean forceCompat, float scaleFactor, float densityScaleFactor) {
         int compatFlags = 0;
 
         if (appInfo.targetSdkVersion < VERSION_CODES.O) {
             compatFlags |= NEEDS_COMPAT_RES;
         }
-        if (overrideScale != 1.0f) {
-            applicationScale = overrideScale;
-            applicationInvertedScale = 1.0f / overrideScale;
+        if (scaleFactor != 1f || densityScaleFactor != 1f) {
+            applicationScale = scaleFactor;
+            applicationInvertedScale = 1f / scaleFactor;
+            applicationDensityScale = densityScaleFactor;
+            applicationDensityInvertedScale = 1f / densityScaleFactor;
             applicationDensity = (int) ((DisplayMetrics.DENSITY_DEVICE_STABLE
-                    * applicationInvertedScale) + .5f);
+                    * applicationDensityInvertedScale) + .5f);
             mCompatibilityFlags = NEVER_NEEDS_COMPAT | HAS_OVERRIDE_SCALING;
             // Override scale has the highest priority. So ignore other compatibility attributes.
             return;
@@ -181,7 +206,8 @@
             applicationDensity = DisplayMetrics.DENSITY_DEVICE;
             applicationScale = 1.0f;
             applicationInvertedScale = 1.0f;
-
+            applicationDensityScale = 1.0f;
+            applicationDensityInvertedScale = 1.0f;
         } else {
             /**
              * Has the application said that its UI is expandable?  Based on the
@@ -271,11 +297,16 @@
                 applicationDensity = DisplayMetrics.DENSITY_DEVICE;
                 applicationScale = 1.0f;
                 applicationInvertedScale = 1.0f;
+                applicationDensityScale = 1.0f;
+                applicationDensityInvertedScale = 1.0f;
             } else {
                 applicationDensity = DisplayMetrics.DENSITY_DEFAULT;
                 applicationScale = DisplayMetrics.DENSITY_DEVICE
                         / (float) DisplayMetrics.DENSITY_DEFAULT;
                 applicationInvertedScale = 1.0f / applicationScale;
+                applicationDensityScale = DisplayMetrics.DENSITY_DEVICE
+                        / (float) DisplayMetrics.DENSITY_DEFAULT;
+                applicationDensityInvertedScale = 1f / applicationDensityScale;
                 compatFlags |= SCALING_REQUIRED;
             }
         }
@@ -289,6 +320,8 @@
         applicationDensity = dens;
         applicationScale = scale;
         applicationInvertedScale = invertedScale;
+        applicationDensityScale = (float) DisplayMetrics.DENSITY_DEVICE_STABLE / dens;
+        applicationDensityInvertedScale = 1f / applicationDensityScale;
     }
 
     @UnsupportedAppUsage
@@ -528,7 +561,8 @@
     /** Applies the compatibility adjustment to the display metrics. */
     public void applyDisplayMetricsIfNeeded(DisplayMetrics inoutDm, boolean applyToSize) {
         if (hasOverrideScale()) {
-            scaleDisplayMetrics(sOverrideInvertedScale, inoutDm, applyToSize);
+            scaleDisplayMetrics(sOverrideInvertedScale, sOverrideDensityInvertScale, inoutDm,
+                    applyToSize);
             return;
         }
         if (!equals(DEFAULT_COMPATIBILITY_INFO)) {
@@ -548,15 +582,17 @@
         }
 
         if (isScalingRequired()) {
-            scaleDisplayMetrics(applicationInvertedScale, inoutDm, true /* applyToSize */);
+            scaleDisplayMetrics(applicationInvertedScale, applicationDensityInvertedScale, inoutDm,
+                    true /* applyToSize */);
         }
     }
 
     /** Scales the density of the given display metrics. */
-    private static void scaleDisplayMetrics(float invertedRatio, DisplayMetrics inoutDm,
-            boolean applyToSize) {
-        inoutDm.density = inoutDm.noncompatDensity * invertedRatio;
-        inoutDm.densityDpi = (int) ((inoutDm.noncompatDensityDpi * invertedRatio) + .5f);
+    private static void scaleDisplayMetrics(float invertScale, float densityInvertScale,
+            DisplayMetrics inoutDm, boolean applyToSize) {
+        inoutDm.density = inoutDm.noncompatDensity * densityInvertScale;
+        inoutDm.densityDpi = (int) ((inoutDm.noncompatDensityDpi
+                * densityInvertScale) + .5f);
         // Note: since this is changing the scaledDensity, you might think we also need to change
         // inoutDm.fontScaleConverter to accurately calculate non-linear font scaling. But we're not
         // going to do that, for a couple of reasons (see b/265695259 for details):
@@ -570,12 +606,12 @@
         //    b. Sometime later by WindowManager in onResume or other windowing events. In this case
         //       the DisplayMetrics object is never used by the app/resources, so it's ok if
         //       fontScaleConverter is null because it's not being used to scale fonts anyway.
-        inoutDm.scaledDensity = inoutDm.noncompatScaledDensity * invertedRatio;
-        inoutDm.xdpi = inoutDm.noncompatXdpi * invertedRatio;
-        inoutDm.ydpi = inoutDm.noncompatYdpi * invertedRatio;
+        inoutDm.scaledDensity = inoutDm.noncompatScaledDensity * densityInvertScale;
+        inoutDm.xdpi = inoutDm.noncompatXdpi * densityInvertScale;
+        inoutDm.ydpi = inoutDm.noncompatYdpi * densityInvertScale;
         if (applyToSize) {
-            inoutDm.widthPixels = (int) (inoutDm.widthPixels * invertedRatio + 0.5f);
-            inoutDm.heightPixels = (int) (inoutDm.heightPixels * invertedRatio + 0.5f);
+            inoutDm.widthPixels = (int) (inoutDm.widthPixels * invertScale + 0.5f);
+            inoutDm.heightPixels = (int) (inoutDm.heightPixels * invertScale + 0.5f);
         }
     }
 
@@ -594,38 +630,55 @@
         }
         inoutConfig.densityDpi = displayDensity;
         if (isScalingRequired()) {
-            scaleConfiguration(applicationInvertedScale, inoutConfig);
+            scaleConfiguration(applicationInvertedScale, applicationDensityInvertedScale,
+                    inoutConfig);
         }
     }
 
     /** Scales the density and bounds of the given configuration. */
-    public static void scaleConfiguration(float invertedRatio, Configuration inoutConfig) {
-        inoutConfig.densityDpi = (int) ((inoutConfig.densityDpi * invertedRatio) + .5f);
-        inoutConfig.windowConfiguration.scale(invertedRatio);
+    public static void scaleConfiguration(float invertScale, Configuration inoutConfig) {
+        scaleConfiguration(invertScale, invertScale, inoutConfig);
+    }
+
+    /** Scales the density and bounds of the given configuration. */
+    public static void scaleConfiguration(float invertScale, float densityInvertScale,
+            Configuration inoutConfig) {
+        inoutConfig.densityDpi = (int) ((inoutConfig.densityDpi
+                * densityInvertScale) + .5f);
+        inoutConfig.windowConfiguration.scale(invertScale);
     }
 
     /** @see #sOverrideInvertedScale */
     public static void applyOverrideScaleIfNeeded(Configuration config) {
         if (!hasOverrideScale()) return;
-        scaleConfiguration(sOverrideInvertedScale, config);
+        scaleConfiguration(sOverrideInvertedScale, sOverrideDensityInvertScale, config);
     }
 
     /** @see #sOverrideInvertedScale */
     public static void applyOverrideScaleIfNeeded(MergedConfiguration mergedConfig) {
         if (!hasOverrideScale()) return;
-        scaleConfiguration(sOverrideInvertedScale, mergedConfig.getGlobalConfiguration());
-        scaleConfiguration(sOverrideInvertedScale, mergedConfig.getOverrideConfiguration());
-        scaleConfiguration(sOverrideInvertedScale, mergedConfig.getMergedConfiguration());
+        scaleConfiguration(sOverrideInvertedScale, sOverrideDensityInvertScale,
+                mergedConfig.getGlobalConfiguration());
+        scaleConfiguration(sOverrideInvertedScale, sOverrideDensityInvertScale,
+                mergedConfig.getOverrideConfiguration());
+        scaleConfiguration(sOverrideInvertedScale, sOverrideDensityInvertScale,
+                mergedConfig.getMergedConfiguration());
     }
 
     /** Returns {@code true} if this process is in a environment with override scale. */
     private static boolean hasOverrideScale() {
-        return sOverrideInvertedScale != 1f;
+        return sOverrideInvertedScale != 1f || sOverrideDensityInvertScale != 1f;
     }
 
     /** @see #sOverrideInvertedScale */
-    public static void setOverrideInvertedScale(float invertedRatio) {
-        sOverrideInvertedScale = invertedRatio;
+    public static void setOverrideInvertedScale(float invertScale) {
+        setOverrideInvertedScale(invertScale, invertScale);
+    }
+
+    /** @see #sOverrideInvertedScale */
+    public static void setOverrideInvertedScale(float invertScale, float densityInvertScale) {
+        sOverrideInvertedScale = invertScale;
+        sOverrideDensityInvertScale = densityInvertScale;
     }
 
     /** @see #sOverrideInvertedScale */
@@ -633,6 +686,11 @@
         return sOverrideInvertedScale;
     }
 
+    /** @see #sOverrideDensityInvertScale */
+    public static float getOverrideDensityInvertedScale() {
+        return sOverrideDensityInvertScale;
+    }
+
     /**
      * Compute the frame Rect for applications runs under compatibility mode.
      *
@@ -693,6 +751,8 @@
             if (applicationDensity != oc.applicationDensity) return false;
             if (applicationScale != oc.applicationScale) return false;
             if (applicationInvertedScale != oc.applicationInvertedScale) return false;
+            if (applicationDensityScale != oc.applicationDensityScale) return false;
+            if (applicationDensityInvertedScale != oc.applicationDensityInvertedScale) return false;
             return true;
         } catch (ClassCastException e) {
             return false;
@@ -713,6 +773,8 @@
         if (hasOverrideScaling()) {
             sb.append(" overrideInvScale=");
             sb.append(applicationInvertedScale);
+            sb.append(" overrideDensityInvScale=");
+            sb.append(applicationDensityInvertedScale);
         }
         if (!supportsScreen()) {
             sb.append(" resizing");
@@ -734,6 +796,8 @@
         result = 31 * result + applicationDensity;
         result = 31 * result + Float.floatToIntBits(applicationScale);
         result = 31 * result + Float.floatToIntBits(applicationInvertedScale);
+        result = 31 * result + Float.floatToIntBits(applicationDensityScale);
+        result = 31 * result + Float.floatToIntBits(applicationDensityInvertedScale);
         return result;
     }
 
@@ -748,6 +812,8 @@
         dest.writeInt(applicationDensity);
         dest.writeFloat(applicationScale);
         dest.writeFloat(applicationInvertedScale);
+        dest.writeFloat(applicationDensityScale);
+        dest.writeFloat(applicationDensityInvertedScale);
     }
 
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
@@ -769,5 +835,61 @@
         applicationDensity = source.readInt();
         applicationScale = source.readFloat();
         applicationInvertedScale = source.readFloat();
+        applicationDensityScale = source.readFloat();
+        applicationDensityInvertedScale = source.readFloat();
+    }
+
+    /**
+     * A data class for holding scale factor for width, height, and density.
+     */
+    public static final class CompatScale {
+
+        public final float mScaleFactor;
+        public final float mDensityScaleFactor;
+
+        public CompatScale(float scaleFactor) {
+            this(scaleFactor, scaleFactor);
+        }
+
+        public CompatScale(float scaleFactor, float densityScaleFactor) {
+            mScaleFactor = scaleFactor;
+            mDensityScaleFactor = densityScaleFactor;
+        }
+
+        @Override
+        public boolean equals(@Nullable Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (!(o instanceof CompatScale)) {
+                return false;
+            }
+            try {
+                CompatScale oc = (CompatScale) o;
+                if (mScaleFactor != oc.mScaleFactor) return false;
+                if (mDensityScaleFactor != oc.mDensityScaleFactor) return false;
+                return true;
+            } catch (ClassCastException e) {
+                return false;
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("mScaleFactor= ");
+            sb.append(mScaleFactor);
+            sb.append(" mDensityScaleFactor= ");
+            sb.append(mDensityScaleFactor);
+            return sb.toString();
+        }
+
+        @Override
+        public int hashCode() {
+            int result = 17;
+            result = 31 * result + Float.floatToIntBits(mScaleFactor);
+            result = 31 * result + Float.floatToIntBits(mDensityScaleFactor);
+            return result;
+        }
     }
 }
diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java
index b2dfd85..4f07acf 100644
--- a/core/java/android/hardware/radio/ProgramList.java
+++ b/core/java/android/hardware/radio/ProgramList.java
@@ -23,6 +23,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 
 import com.android.internal.annotations.GuardedBy;
 
@@ -34,7 +35,6 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.Executor;
-import java.util.stream.Collectors;
 
 /**
  * @hide
@@ -45,8 +45,8 @@
     private final Object mLock = new Object();
 
     @GuardedBy("mLock")
-    private final Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> mPrograms =
-            new ArrayMap<>();
+    private final Map<ProgramSelector.Identifier, Map<UniqueProgramIdentifier,
+            RadioManager.ProgramInfo>> mPrograms = new ArrayMap<>();
 
     @GuardedBy("mLock")
     private final List<ListCallback> mListCallbacks = new ArrayList<>();
@@ -193,7 +193,7 @@
 
     void apply(Chunk chunk) {
         List<ProgramSelector.Identifier> removedList = new ArrayList<>();
-        List<ProgramSelector.Identifier> changedList = new ArrayList<>();
+        Set<ProgramSelector.Identifier> changedSet = new ArraySet<>();
         List<ProgramList.ListCallback> listCallbacksCopied;
         List<OnCompleteListener> onCompleteListenersCopied = new ArrayList<>();
         synchronized (mLock) {
@@ -203,19 +203,27 @@
             listCallbacksCopied = new ArrayList<>(mListCallbacks);
 
             if (chunk.isPurge()) {
-                Iterator<Map.Entry<ProgramSelector.Identifier, RadioManager.ProgramInfo>>
-                        programsIterator = mPrograms.entrySet().iterator();
+                Iterator<Map.Entry<ProgramSelector.Identifier, Map<UniqueProgramIdentifier,
+                        RadioManager.ProgramInfo>>> programsIterator =
+                        mPrograms.entrySet().iterator();
                 while (programsIterator.hasNext()) {
-                    RadioManager.ProgramInfo removed = programsIterator.next().getValue();
-                    if (removed != null) {
-                        removedList.add(removed.getSelector().getPrimaryId());
+                    Map.Entry<ProgramSelector.Identifier, Map<UniqueProgramIdentifier,
+                            RadioManager.ProgramInfo>> removed = programsIterator.next();
+                    if (removed.getValue() != null) {
+                        removedList.add(removed.getKey());
                     }
                     programsIterator.remove();
                 }
             }
 
-            chunk.getRemoved().stream().forEach(id -> removeLocked(id, removedList));
-            chunk.getModified().stream().forEach(info -> putLocked(info, changedList));
+            Iterator<UniqueProgramIdentifier> removedIterator = chunk.getRemoved().iterator();
+            while (removedIterator.hasNext()) {
+                removeLocked(removedIterator.next(), removedList);
+            }
+            Iterator<RadioManager.ProgramInfo> modifiedIterator = chunk.getModified().iterator();
+            while (modifiedIterator.hasNext()) {
+                putLocked(modifiedIterator.next(), changedSet);
+            }
 
             if (chunk.isComplete()) {
                 mIsComplete = true;
@@ -228,9 +236,11 @@
                 listCallbacksCopied.get(cbIndex).onItemRemoved(removedList.get(i));
             }
         }
-        for (int i = 0; i < changedList.size(); i++) {
+        Iterator<ProgramSelector.Identifier> changedIterator = changedSet.iterator();
+        while (changedIterator.hasNext()) {
+            ProgramSelector.Identifier changedId = changedIterator.next();
             for (int cbIndex = 0; cbIndex < listCallbacksCopied.size(); cbIndex++) {
-                listCallbacksCopied.get(cbIndex).onItemChanged(changedList.get(i));
+                listCallbacksCopied.get(cbIndex).onItemChanged(changedId);
             }
         }
         if (chunk.isComplete()) {
@@ -242,20 +252,31 @@
 
     @GuardedBy("mLock")
     private void putLocked(RadioManager.ProgramInfo value,
-            List<ProgramSelector.Identifier> changedIdentifierList) {
-        ProgramSelector.Identifier key = value.getSelector().getPrimaryId();
-        mPrograms.put(Objects.requireNonNull(key), value);
-        ProgramSelector.Identifier sel = value.getSelector().getPrimaryId();
-        changedIdentifierList.add(sel);
+            Set<ProgramSelector.Identifier> changedIdentifierSet) {
+        UniqueProgramIdentifier key = new UniqueProgramIdentifier(
+                value.getSelector());
+        ProgramSelector.Identifier primaryKey = Objects.requireNonNull(key.getPrimaryId());
+        if (!mPrograms.containsKey(primaryKey)) {
+            mPrograms.put(primaryKey, new ArrayMap<>());
+        }
+        mPrograms.get(primaryKey).put(key, value);
+        changedIdentifierSet.add(primaryKey);
     }
 
     @GuardedBy("mLock")
-    private void removeLocked(ProgramSelector.Identifier key,
+    private void removeLocked(UniqueProgramIdentifier key,
             List<ProgramSelector.Identifier> removedIdentifierList) {
-        RadioManager.ProgramInfo removed = mPrograms.remove(Objects.requireNonNull(key));
+        ProgramSelector.Identifier primaryKey = Objects.requireNonNull(key.getPrimaryId());
+        if (!mPrograms.containsKey(primaryKey)) {
+            return;
+        }
+        Map<UniqueProgramIdentifier, RadioManager.ProgramInfo> entries = mPrograms
+                .get(primaryKey);
+        RadioManager.ProgramInfo removed = entries.remove(Objects.requireNonNull(key));
         if (removed == null) return;
-        ProgramSelector.Identifier sel = removed.getSelector().getPrimaryId();
-        removedIdentifierList.add(sel);
+        if (entries.size() == 0) {
+            removedIdentifierList.add(primaryKey);
+        }
     }
 
     /**
@@ -264,9 +285,20 @@
      * @return the new List<> object; it won't receive any further updates
      */
     public @NonNull List<RadioManager.ProgramInfo> toList() {
+        List<RadioManager.ProgramInfo> list = new ArrayList<>();
         synchronized (mLock) {
-            return mPrograms.values().stream().collect(Collectors.toList());
+            Iterator<Map.Entry<ProgramSelector.Identifier, Map<UniqueProgramIdentifier,
+                    RadioManager.ProgramInfo>>> listIterator = mPrograms.entrySet().iterator();
+            while (listIterator.hasNext()) {
+                Iterator<Map.Entry<UniqueProgramIdentifier,
+                        RadioManager.ProgramInfo>> prorgramsIterator = listIterator.next()
+                        .getValue().entrySet().iterator();
+                while (prorgramsIterator.hasNext()) {
+                    list.add(prorgramsIterator.next().getValue());
+                }
+            }
         }
+        return list;
     }
 
     /**
@@ -276,9 +308,15 @@
      * @return the program info, or null if there is no such program on the list
      */
     public @Nullable RadioManager.ProgramInfo get(@NonNull ProgramSelector.Identifier id) {
+        Map<UniqueProgramIdentifier, RadioManager.ProgramInfo> entries;
         synchronized (mLock) {
-            return mPrograms.get(Objects.requireNonNull(id));
+            entries = mPrograms.get(Objects.requireNonNull(id,
+                    "Primary identifier can not be null"));
         }
+        if (entries == null) {
+            return null;
+        }
+        return entries.entrySet().iterator().next().getValue();
     }
 
     /**
@@ -404,7 +442,7 @@
          * Checks, if non-tunable entries that define tree structure on the
          * program list (i.e. DAB ensembles) should be included.
          *
-         * @see {@link ProgramSelector.Identifier#isCategory()}
+         * @see ProgramSelector.Identifier#isCategoryType()
          */
         public boolean areCategoriesIncluded() {
             return mIncludeCategories;
@@ -459,11 +497,11 @@
         private final boolean mPurge;
         private final boolean mComplete;
         private final @NonNull Set<RadioManager.ProgramInfo> mModified;
-        private final @NonNull Set<ProgramSelector.Identifier> mRemoved;
+        private final @NonNull Set<UniqueProgramIdentifier> mRemoved;
 
         public Chunk(boolean purge, boolean complete,
                 @Nullable Set<RadioManager.ProgramInfo> modified,
-                @Nullable Set<ProgramSelector.Identifier> removed) {
+                @Nullable Set<UniqueProgramIdentifier> removed) {
             mPurge = purge;
             mComplete = complete;
             mModified = (modified != null) ? modified : Collections.emptySet();
@@ -474,7 +512,7 @@
             mPurge = in.readByte() != 0;
             mComplete = in.readByte() != 0;
             mModified = Utils.createSet(in, RadioManager.ProgramInfo.CREATOR);
-            mRemoved = Utils.createSet(in, ProgramSelector.Identifier.CREATOR);
+            mRemoved = Utils.createSet(in, UniqueProgramIdentifier.CREATOR);
         }
 
         @Override
@@ -512,7 +550,7 @@
             return mModified;
         }
 
-        public @NonNull Set<ProgramSelector.Identifier> getRemoved() {
+        public @NonNull Set<UniqueProgramIdentifier> getRemoved() {
             return mRemoved;
         }
 
diff --git a/core/java/android/security/FileIntegrityManager.java b/core/java/android/security/FileIntegrityManager.java
index d6f3bf3..132700d 100644
--- a/core/java/android/security/FileIntegrityManager.java
+++ b/core/java/android/security/FileIntegrityManager.java
@@ -133,13 +133,11 @@
      * also use this API to download the best signature on the running device.
      *
      * @return whether the certificate is trusted in the system
-     * @deprecated The feature is no longer supported, and this API now always returns false.
      */
     @RequiresPermission(anyOf = {
             android.Manifest.permission.INSTALL_PACKAGES,
             android.Manifest.permission.REQUEST_INSTALL_PACKAGES
     })
-    @Deprecated
     public boolean isAppSourceCertificateTrusted(@NonNull X509Certificate certificate)
             throws CertificateEncodingException {
         try {
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index b27dac2..b6c2b83 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -6,3 +6,10 @@
     description: "Feature flag for fs-verity API"
     bug: "285185747"
 }
+
+flag {
+    name: "fix_unlocked_device_required_keys"
+    namespace: "hardware_backed_security"
+    description: "Fix bugs in behavior of UnlockedDeviceRequired keystore keys"
+    bug: "296464083"
+}
diff --git a/core/java/android/service/voice/HotwordTrainingAudio.java b/core/java/android/service/voice/HotwordTrainingAudio.java
index 895b0c0..91e34dc 100644
--- a/core/java/android/service/voice/HotwordTrainingAudio.java
+++ b/core/java/android/service/voice/HotwordTrainingAudio.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.media.AudioFormat;
 import android.os.Parcel;
@@ -25,6 +26,8 @@
 
 import com.android.internal.util.DataClass;
 
+import java.util.Objects;
+
 /**
  * Represents audio supporting hotword model training.
  *
@@ -43,7 +46,10 @@
     /** Represents unset value for the hotword offset. */
     public static final int HOTWORD_OFFSET_UNSET = -1;
 
-    /** Buffer of hotword audio data for training models. */
+    /**
+     * Buffer of hotword audio data for training models. The data format is expected to match
+     * {@link #getAudioFormat()}.
+     */
     @NonNull
     private final byte[] mHotwordAudio;
 
@@ -74,6 +80,24 @@
      */
     private int mHotwordOffsetMillis = HOTWORD_OFFSET_UNSET;
 
+    @DataClass.Suppress("setHotwordAudio")
+    abstract static class BaseBuilder {
+
+        /**
+         * Buffer of hotword audio data for training models. The data format is expected to match
+         * {@link #getAudioFormat()}.
+         */
+        @SuppressLint("UnflaggedApi")
+        public @NonNull HotwordTrainingAudio.Builder setHotwordAudio(@NonNull byte[] value) {
+            Objects.requireNonNull(value, "value should not be null");
+            final HotwordTrainingAudio.Builder builder = (HotwordTrainingAudio.Builder) this;
+            // If the code gen flag in build() is changed, we must update the flag e.g. 0x1 here.
+            builder.mBuilderFieldsSet |= 0x1;
+            builder.mHotwordAudio = value;
+            return builder;
+        }
+    }
+
 
 
     // Code below generated by codegen v1.0.23.
@@ -110,7 +134,8 @@
     }
 
     /**
-     * Buffer of hotword audio data for training models.
+     * Buffer of hotword audio data for training models. The data format is expected to match
+     * {@link #getAudioFormat()}.
      */
     @DataClass.Generated.Member
     public @NonNull byte[] getHotwordAudio() {
@@ -171,7 +196,7 @@
         //noinspection PointlessBooleanExpression
         return true
                 && java.util.Arrays.equals(mHotwordAudio, that.mHotwordAudio)
-                && java.util.Objects.equals(mAudioFormat, that.mAudioFormat)
+                && Objects.equals(mAudioFormat, that.mAudioFormat)
                 && mAudioType == that.mAudioType
                 && mHotwordOffsetMillis == that.mHotwordOffsetMillis;
     }
@@ -184,7 +209,7 @@
 
         int _hash = 1;
         _hash = 31 * _hash + java.util.Arrays.hashCode(mHotwordAudio);
-        _hash = 31 * _hash + java.util.Objects.hashCode(mAudioFormat);
+        _hash = 31 * _hash + Objects.hashCode(mAudioFormat);
         _hash = 31 * _hash + mAudioType;
         _hash = 31 * _hash + mHotwordOffsetMillis;
         return _hash;
@@ -251,7 +276,7 @@
      */
     @SuppressWarnings("WeakerAccess")
     @DataClass.Generated.Member
-    public static final class Builder {
+    public static final class Builder extends BaseBuilder {
 
         private @NonNull byte[] mHotwordAudio;
         private @NonNull AudioFormat mAudioFormat;
@@ -264,7 +289,8 @@
          * Creates a new Builder.
          *
          * @param hotwordAudio
-         *   Buffer of hotword audio data for training models.
+         *   Buffer of hotword audio data for training models. The data format is expected to match
+         *   {@link #getAudioFormat()}.
          * @param audioFormat
          *   The {@link AudioFormat} of the {@link HotwordTrainingAudio#mHotwordAudio}.
          */
@@ -280,17 +306,6 @@
         }
 
         /**
-         * Buffer of hotword audio data for training models.
-         */
-        @DataClass.Generated.Member
-        public @NonNull Builder setHotwordAudio(@NonNull byte... value) {
-            checkNotUsed();
-            mBuilderFieldsSet |= 0x1;
-            mHotwordAudio = value;
-            return this;
-        }
-
-        /**
          * The {@link AudioFormat} of the {@link HotwordTrainingAudio#mHotwordAudio}.
          */
         @DataClass.Generated.Member
@@ -353,10 +368,10 @@
     }
 
     @DataClass.Generated(
-            time = 1692837160437L,
+            time = 1694193905346L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/service/voice/HotwordTrainingAudio.java",
-            inputSignatures = "public static final  int HOTWORD_OFFSET_UNSET\nprivate final @android.annotation.NonNull byte[] mHotwordAudio\nprivate final @android.annotation.NonNull android.media.AudioFormat mAudioFormat\nprivate final @android.annotation.NonNull int mAudioType\nprivate  int mHotwordOffsetMillis\nprivate  java.lang.String hotwordAudioToString()\nprivate static  int defaultAudioType()\nclass HotwordTrainingAudio extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=true, genEqualsHashCode=true, genHiddenConstDefs=true, genParcelable=true, genToString=true)")
+            inputSignatures = "public static final  int HOTWORD_OFFSET_UNSET\nprivate final @android.annotation.NonNull byte[] mHotwordAudio\nprivate final @android.annotation.NonNull android.media.AudioFormat mAudioFormat\nprivate final @android.annotation.NonNull int mAudioType\nprivate  int mHotwordOffsetMillis\nprivate  java.lang.String hotwordAudioToString()\nprivate static  int defaultAudioType()\nclass HotwordTrainingAudio extends java.lang.Object implements [android.os.Parcelable]\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.service.voice.HotwordTrainingAudio.Builder setHotwordAudio(byte[])\nclass BaseBuilder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=true, genEqualsHashCode=true, genHiddenConstDefs=true, genParcelable=true, genToString=true)\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.service.voice.HotwordTrainingAudio.Builder setHotwordAudio(byte[])\nclass BaseBuilder extends java.lang.Object implements []")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index b8385c6..e64274e 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -3139,15 +3139,6 @@
         public static final int PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS = 1 << 10;
 
         /**
-         * Flag to force the status bar window to be visible all the time. If the bar is hidden when
-         * this flag is set it will be shown again.
-         * This can only be set by {@link LayoutParams#TYPE_STATUS_BAR}.
-         *
-         * {@hide}
-         */
-        public static final int PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR = 1 << 11;
-
-        /**
          * Flag to indicate that the window frame should be the requested frame adding the display
          * cutout frame. This will only be applied if a specific size smaller than the parent frame
          * is given, and the window is covering the display cutout. The extended frame will not be
@@ -3238,15 +3229,6 @@
         public static final int PRIVATE_FLAG_NOT_MAGNIFIABLE = 1 << 22;
 
         /**
-         * Flag to indicate that the status bar window is in a state such that it forces showing
-         * the navigation bar unless the navigation bar window is explicitly set to
-         * {@link View#GONE}.
-         * It only takes effects if this is set by {@link LayoutParams#TYPE_STATUS_BAR}.
-         * @hide
-         */
-        public static final int PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION = 1 << 23;
-
-        /**
          * Flag to indicate that the window is color space agnostic, and the color can be
          * interpreted to any color space.
          * @hide
@@ -3334,7 +3316,6 @@
                 PRIVATE_FLAG_SYSTEM_ERROR,
                 PRIVATE_FLAG_OPTIMIZE_MEASURE,
                 PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS,
-                PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR,
                 PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT,
                 PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY,
                 PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME,
@@ -3345,7 +3326,6 @@
                 PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY,
                 PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION,
                 PRIVATE_FLAG_NOT_MAGNIFIABLE,
-                PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION,
                 PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC,
                 PRIVATE_FLAG_USE_BLAST,
                 PRIVATE_FLAG_APPEARANCE_CONTROLLED,
@@ -3401,10 +3381,6 @@
                         equals = PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS,
                         name = "DISABLE_WALLPAPER_TOUCH_EVENTS"),
                 @ViewDebug.FlagToString(
-                        mask = PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR,
-                        equals = PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR,
-                        name = "FORCE_STATUS_BAR_VISIBLE"),
-                @ViewDebug.FlagToString(
                         mask = PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT,
                         equals = PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT,
                         name = "LAYOUT_SIZE_EXTENDED_BY_CUTOUT"),
@@ -3445,10 +3421,6 @@
                         equals = PRIVATE_FLAG_NOT_MAGNIFIABLE,
                         name = "NOT_MAGNIFIABLE"),
                 @ViewDebug.FlagToString(
-                        mask = PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION,
-                        equals = PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION,
-                        name = "STATUS_FORCE_SHOW_NAVIGATION"),
-                @ViewDebug.FlagToString(
                         mask = PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC,
                         equals = PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC,
                         name = "COLOR_SPACE_AGNOSTIC"),
@@ -4412,6 +4384,16 @@
         public InsetsFrameProvider[] providedInsets;
 
         /**
+         * Specifies which {@link InsetsType}s should be forcibly shown. The types shown by this
+         * method won't affect the app's layout. This field only takes effects if the caller has
+         * {@link android.Manifest.permission#STATUS_BAR_SERVICE} or the caller has the same uid as
+         * the recents component.
+         *
+         * @hide
+         */
+        public @InsetsType int forciblyShownTypes;
+
+        /**
          * {@link LayoutParams} to be applied to the window when layout with a assigned rotation.
          * This will make layout during rotation change smoothly.
          *
@@ -4869,6 +4851,7 @@
             out.writeInt(mBlurBehindRadius);
             out.writeBoolean(mWallpaperTouchEventsEnabled);
             out.writeTypedArray(providedInsets, 0 /* parcelableFlags */);
+            out.writeInt(forciblyShownTypes);
             checkNonRecursiveParams();
             out.writeTypedArray(paramsForRotation, 0 /* parcelableFlags */);
             out.writeInt(mDisplayFlags);
@@ -4940,6 +4923,7 @@
             mBlurBehindRadius = in.readInt();
             mWallpaperTouchEventsEnabled = in.readBoolean();
             providedInsets = in.createTypedArray(InsetsFrameProvider.CREATOR);
+            forciblyShownTypes = in.readInt();
             paramsForRotation = in.createTypedArray(LayoutParams.CREATOR);
             mDisplayFlags = in.readInt();
         }
@@ -5245,6 +5229,11 @@
                 changes |= LAYOUT_CHANGED;
             }
 
+            if (forciblyShownTypes != o.forciblyShownTypes) {
+                forciblyShownTypes = o.forciblyShownTypes;
+                changes |= PRIVATE_FLAGS_CHANGED;
+            }
+
             if (paramsForRotation != o.paramsForRotation) {
                 if ((changes & LAYOUT_CHANGED) == 0) {
                     if (paramsForRotation != null && o.paramsForRotation != null
@@ -5482,6 +5471,11 @@
                     sb.append(prefix).append("    ").append(providedInsets[i]);
                 }
             }
+            if (forciblyShownTypes != 0) {
+                sb.append(System.lineSeparator());
+                sb.append(prefix).append("  forciblyShownTypes=").append(
+                        WindowInsets.Type.toString(forciblyShownTypes));
+            }
             if (paramsForRotation != null && paramsForRotation.length != 0) {
                 sb.append(System.lineSeparator());
                 sb.append(prefix).append("  paramsForRotation:");
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 32fe4e3..3180ffb 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1466,10 +1466,9 @@
 
     <!-- Allows an application to initiate a phone call without going through
         the Dialer user interface for the user to confirm the call.
-        <p>
-        <em>Note: An app holding this permission can also call carrier MMI codes to change settings
-        such as call forwarding or call waiting preferences.
-        <p>Protection level: dangerous
+        <p class="note"><b>Note:</b> An app holding this permission can also call carrier MMI
+        codes to change settings such as call forwarding or call waiting preferences.</p>
+        <p>Protection level: dangerous</p>
     -->
     <permission android:name="android.permission.CALL_PHONE"
         android:permissionGroup="android.permission-group.UNDEFINED"
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 7d2690e..e7764d8 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5717,13 +5717,13 @@
 
     <!-- Blur radius for the Option 3 in R.integer.config_letterboxBackgroundType. Values < 0 are
         ignored and 0 is used. -->
-    <dimen name="config_letterboxBackgroundWallpaperBlurRadius">24dp</dimen>
+    <dimen name="config_letterboxBackgroundWallpaperBlurRadius">38dp</dimen>
 
     <!-- Alpha of a black translucent scrim showed over wallpaper letterbox background when
         the Option 3 is selected for R.integer.config_letterboxBackgroundType.
         Values < 0 or >= 1 are ignored and 0.0 (transparent) is used instead. -->
     <item name="config_letterboxBackgroundWallaperDarkScrimAlpha" format="float" type="dimen">
-        0.75
+        0.54
     </item>
 
     <!-- Corners appearance of the letterbox background.
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramListTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramListTest.java
index 7c3d2f2..d638fed 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramListTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/ProgramListTest.java
@@ -74,18 +74,45 @@
     private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER =
             new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
                     /* value= */ 0x1013);
+    private static final ProgramSelector.Identifier DAB_FREQUENCY_IDENTIFIER_1 =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
+                    /* value= */ 222_064);
+    private static final ProgramSelector.Identifier DAB_FREQUENCY_IDENTIFIER_2 =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
+                    /* value= */ 220_352);
+
+    private static final ProgramSelector DAB_SELECTOR_1 = new ProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_DAB, DAB_DMB_SID_EXT_IDENTIFIER,
+            new ProgramSelector.Identifier[]{DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER_1},
+            /* vendorIds= */ null);
+    private static final ProgramSelector DAB_SELECTOR_2 = new ProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_DAB, DAB_DMB_SID_EXT_IDENTIFIER,
+            new ProgramSelector.Identifier[]{DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER_2},
+            /* vendorIds= */ null);
+
+    private static final UniqueProgramIdentifier RDS_UNIQUE_IDENTIFIER =
+            new UniqueProgramIdentifier(RDS_IDENTIFIER);
+    private static final UniqueProgramIdentifier DAB_UNIQUE_IDENTIFIER_1 =
+            new UniqueProgramIdentifier(DAB_SELECTOR_1);
+    private static final UniqueProgramIdentifier DAB_UNIQUE_IDENTIFIER_2 =
+            new UniqueProgramIdentifier(DAB_SELECTOR_2);
+
     private static final RadioManager.ProgramInfo FM_PROGRAM_INFO = createFmProgramInfo(
             createProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, FM_IDENTIFIER));
-    private static final RadioManager.ProgramInfo RDS_PROGRAM_INFO = createFmProgramInfo(
-            createProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, RDS_IDENTIFIER));
+    private static final RadioManager.ProgramInfo DAB_PROGRAM_INFO_1 = createDabProgramInfo(
+            DAB_SELECTOR_1);
+    private static final RadioManager.ProgramInfo DAB_PROGRAM_INFO_2 = createDabProgramInfo(
+            DAB_SELECTOR_2);
 
     private static final Set<Integer> FILTER_IDENTIFIER_TYPES = Set.of(
-            ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, ProgramSelector.IDENTIFIER_TYPE_RDS_PI);
-    private static final Set<ProgramSelector.Identifier> FILTER_IDENTIFIERS = Set.of(FM_IDENTIFIER);
+            ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY,
+            ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT);
+    private static final Set<ProgramSelector.Identifier> FILTER_IDENTIFIERS = Set.of(
+            FM_IDENTIFIER, DAB_DMB_SID_EXT_IDENTIFIER);
 
-    private static final ProgramList.Chunk FM_RDS_ADD_CHUNK = new ProgramList.Chunk(IS_PURGE,
-            IS_COMPLETE, Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
-            Set.of(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
+    private static final ProgramList.Chunk FM_DAB_ADD_CHUNK = new ProgramList.Chunk(IS_PURGE,
+            IS_COMPLETE, Set.of(FM_PROGRAM_INFO, DAB_PROGRAM_INFO_1, DAB_PROGRAM_INFO_2),
+            Set.of(RDS_UNIQUE_IDENTIFIER));
     private static final ProgramList.Chunk FM_ADD_INCOMPLETE_CHUNK = new ProgramList.Chunk(IS_PURGE,
             /* complete= */ false, Set.of(FM_PROGRAM_INFO), new ArraySet<>());
     private static final ProgramList.Filter TEST_FILTER = new ProgramList.Filter(
@@ -213,58 +240,44 @@
 
     @Test
     public void isPurge_forChunk() {
-        ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE,
-                Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
-                Set.of(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
-
-        assertWithMessage("Puring chunk").that(chunk.isPurge()).isEqualTo(IS_PURGE);
+        assertWithMessage("Puring chunk").that(FM_DAB_ADD_CHUNK.isPurge()).isEqualTo(IS_PURGE);
     }
 
     @Test
     public void isComplete_forChunk() {
-        ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE,
-                Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
-                Set.of(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
-
-        assertWithMessage("Complete chunk").that(chunk.isComplete()).isEqualTo(IS_COMPLETE);
+        assertWithMessage("Complete chunk").that(FM_DAB_ADD_CHUNK.isComplete())
+                .isEqualTo(IS_COMPLETE);
     }
 
     @Test
     public void getModified_forChunk() {
-        ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE,
-                Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
-                Set.of(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
-
         assertWithMessage("Modified program info in chunk")
-                .that(chunk.getModified()).containsExactly(FM_PROGRAM_INFO, RDS_PROGRAM_INFO);
+                .that(FM_DAB_ADD_CHUNK.getModified())
+                .containsExactly(FM_PROGRAM_INFO, DAB_PROGRAM_INFO_1, DAB_PROGRAM_INFO_2);
     }
 
     @Test
     public void getRemoved_forChunk() {
-        ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE,
-                Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO),
-                Set.of(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER));
-
-        assertWithMessage("Removed program identifiers in chunk").that(chunk.getRemoved())
-                .containsExactly(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER);
+        assertWithMessage("Removed program identifiers in chunk")
+                .that(FM_DAB_ADD_CHUNK.getRemoved()).containsExactly(RDS_UNIQUE_IDENTIFIER);
     }
 
     @Test
     public void describeContents_forChunk() {
-        assertWithMessage("Chunk contents").that(FM_RDS_ADD_CHUNK.describeContents()).isEqualTo(0);
+        assertWithMessage("Chunk contents").that(FM_DAB_ADD_CHUNK.describeContents()).isEqualTo(0);
     }
 
     @Test
     public void writeToParcel_forChunk() {
         Parcel parcel = Parcel.obtain();
 
-        FM_RDS_ADD_CHUNK.writeToParcel(parcel, /* flags= */ 0);
+        FM_DAB_ADD_CHUNK.writeToParcel(parcel, /* flags= */ 0);
         parcel.setDataPosition(0);
 
         ProgramList.Chunk chunkFromParcel =
                 ProgramList.Chunk.CREATOR.createFromParcel(parcel);
         assertWithMessage("Chunk created from parcel")
-                .that(chunkFromParcel).isEqualTo(FM_RDS_ADD_CHUNK);
+                .that(chunkFromParcel).isEqualTo(FM_DAB_ADD_CHUNK);
     }
 
     @Test
@@ -336,37 +349,78 @@
     }
 
     @Test
-    public void onProgramListUpdated_withNewIdsAdded_invokesMockedCallbacks() throws Exception {
+    public void onProgramListUpdated_withNewIdsAdded_invokesCallbacks() throws Exception {
         createRadioTuner();
         mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
         registerListCallbacks(/* numCallbacks= */ 1);
         addOnCompleteListeners(/* numListeners= */ 1);
 
-        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);
+        mTunerCallback.onProgramListUpdated(FM_DAB_ADD_CHUNK);
 
         verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemChanged(FM_IDENTIFIER);
-        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemChanged(RDS_IDENTIFIER);
+        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemChanged(DAB_DMB_SID_EXT_IDENTIFIER);
         verify(mOnCompleteListenerMocks[0], CALLBACK_TIMEOUT).onComplete();
-        assertWithMessage("Program info in program list after adding FM and RDS info")
-                .that(mProgramList.toList()).containsExactly(FM_PROGRAM_INFO, RDS_PROGRAM_INFO);
+        assertWithMessage("Program info in program list after adding FM and DAB info")
+                .that(mProgramList.toList()).containsExactly(FM_PROGRAM_INFO, DAB_PROGRAM_INFO_1,
+                        DAB_PROGRAM_INFO_2);
     }
 
     @Test
-    public void onProgramListUpdated_withIdsRemoved_invokesMockedCallbacks() throws Exception {
+    public void onProgramListUpdated_withFmIdsRemoved_invokesCallbacks() throws Exception {
+        UniqueProgramIdentifier fmUniqueId = new UniqueProgramIdentifier(FM_IDENTIFIER);
         ProgramList.Chunk fmRemovedChunk = new ProgramList.Chunk(/* purge= */ false,
-                /* complete= */ false, new ArraySet<>(), Set.of(FM_IDENTIFIER));
+                /* complete= */ false, new ArraySet<>(), Set.of(fmUniqueId));
         createRadioTuner();
         mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
         registerListCallbacks(/* numCallbacks= */ 1);
-        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);
+        mTunerCallback.onProgramListUpdated(FM_DAB_ADD_CHUNK);
 
         mTunerCallback.onProgramListUpdated(fmRemovedChunk);
 
         verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemRemoved(FM_IDENTIFIER);
         assertWithMessage("Program info in program list after removing FM id")
-                .that(mProgramList.toList()).containsExactly(RDS_PROGRAM_INFO);
-        assertWithMessage("Program info FM identifier")
-                .that(mProgramList.get(RDS_IDENTIFIER)).isEqualTo(RDS_PROGRAM_INFO);
+                .that(mProgramList.toList()).containsExactly(DAB_PROGRAM_INFO_1,
+                        DAB_PROGRAM_INFO_2);
+    }
+
+    @Test
+    public void onProgramListUpdated_withPartOfDabIdsRemoved_doesNotInvokeCallbacks()
+            throws Exception {
+        ProgramList.Chunk dabRemovedChunk1 = new ProgramList.Chunk(/* purge= */ false,
+                /* complete= */ false, new ArraySet<>(), Set.of(DAB_UNIQUE_IDENTIFIER_1));
+        createRadioTuner();
+        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
+        registerListCallbacks(/* numCallbacks= */ 1);
+        mTunerCallback.onProgramListUpdated(FM_DAB_ADD_CHUNK);
+
+        mTunerCallback.onProgramListUpdated(dabRemovedChunk1);
+
+        verify(mListCallbackMocks[0], after(TIMEOUT_MS).never()).onItemRemoved(
+                DAB_DMB_SID_EXT_IDENTIFIER);
+        assertWithMessage("Program info in program list after removing part of DAB ids")
+                .that(mProgramList.toList()).containsExactly(FM_PROGRAM_INFO, DAB_PROGRAM_INFO_2);
+    }
+
+    @Test
+    public void onProgramListUpdated_withAllDabIdsRemoved_invokesCallbacks()
+            throws Exception {
+        ProgramList.Chunk dabRemovedChunk1 = new ProgramList.Chunk(/* purge= */ false,
+                /* complete= */ false, new ArraySet<>(), Set.of(DAB_UNIQUE_IDENTIFIER_1));
+        ProgramList.Chunk dabRemovedChunk2 = new ProgramList.Chunk(/* purge= */ false,
+                /* complete= */ false, new ArraySet<>(), Set.of(DAB_UNIQUE_IDENTIFIER_2));
+        createRadioTuner();
+        mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
+        registerListCallbacks(/* numCallbacks= */ 1);
+        mTunerCallback.onProgramListUpdated(FM_DAB_ADD_CHUNK);
+        mTunerCallback.onProgramListUpdated(dabRemovedChunk1);
+        verify(mListCallbackMocks[0], after(TIMEOUT_MS).never()).onItemRemoved(
+                DAB_DMB_SID_EXT_IDENTIFIER);
+
+        mTunerCallback.onProgramListUpdated(dabRemovedChunk2);
+
+        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemRemoved(DAB_DMB_SID_EXT_IDENTIFIER);
+        assertWithMessage("Program info in program list after removing all DAB ids")
+                .that(mProgramList.toList()).containsExactly(FM_PROGRAM_INFO);
     }
 
     @Test
@@ -388,18 +442,18 @@
         createRadioTuner();
         mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
         registerListCallbacks(/* numCallbacks= */ 1);
-        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);
+        mTunerCallback.onProgramListUpdated(FM_DAB_ADD_CHUNK);
 
         mTunerCallback.onProgramListUpdated(purgeChunk);
 
         verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemRemoved(FM_IDENTIFIER);
-        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemRemoved(RDS_IDENTIFIER);
+        verify(mListCallbackMocks[0], CALLBACK_TIMEOUT).onItemRemoved(DAB_DMB_SID_EXT_IDENTIFIER);
         assertWithMessage("Program list after purge chunk applied")
                 .that(mProgramList.toList()).isEmpty();
     }
 
     @Test
-    public void onProgramListUpdated_afterProgramListClosed_notInvokeMockedCallbacks()
+    public void onProgramListUpdated_afterProgramListClosed_notInvokeCallbacks()
             throws Exception {
         createRadioTuner();
         mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
@@ -407,7 +461,7 @@
         addOnCompleteListeners(/* numListeners= */ 1);
         mProgramList.close();
 
-        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);
+        mTunerCallback.onProgramListUpdated(FM_DAB_ADD_CHUNK);
 
         verify(mListCallbackMocks[0], after(TIMEOUT_MS).never()).onItemChanged(any());
         verify(mListCallbackMocks[0], never()).onItemChanged(any());
@@ -462,7 +516,7 @@
             throws Exception {
         createRadioTuner();
         mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
-        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);
+        mTunerCallback.onProgramListUpdated(FM_DAB_ADD_CHUNK);
 
         mTunerCallback.onBackgroundScanComplete();
 
@@ -487,7 +541,7 @@
         mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
 
         mTunerCallback.onBackgroundScanComplete();
-        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);
+        mTunerCallback.onProgramListUpdated(FM_DAB_ADD_CHUNK);
 
         verify(mTunerCallbackMock, CALLBACK_TIMEOUT).onBackgroundScanComplete();
     }
@@ -512,7 +566,7 @@
                 mock(ProgramList.OnCompleteListener.class);
 
         mProgramList.addOnCompleteListener(mExecutor, onCompleteListenerMock);
-        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);
+        mTunerCallback.onProgramListUpdated(FM_DAB_ADD_CHUNK);
 
         verify(onCompleteListenerMock, CALLBACK_TIMEOUT).onComplete();
     }
@@ -524,7 +578,7 @@
         mProgramList = mRadioTuner.getDynamicProgramList(TEST_FILTER);
         addOnCompleteListeners(numListeners);
 
-        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);
+        mTunerCallback.onProgramListUpdated(FM_DAB_ADD_CHUNK);
 
         for (int index = 0; index < numListeners; index++) {
             verify(mOnCompleteListenerMocks[index], CALLBACK_TIMEOUT).onComplete();
@@ -538,7 +592,7 @@
         addOnCompleteListeners(/* numListeners= */ 1);
 
         mProgramList.removeOnCompleteListener(mOnCompleteListenerMocks[0]);
-        mTunerCallback.onProgramListUpdated(FM_RDS_ADD_CHUNK);
+        mTunerCallback.onProgramListUpdated(FM_DAB_ADD_CHUNK);
 
         verify(mOnCompleteListenerMocks[0], after(TIMEOUT_MS).never()).onComplete();
     }
@@ -566,6 +620,13 @@
                 /* vendorInfo= */ null);
     }
 
+    private static RadioManager.ProgramInfo createDabProgramInfo(ProgramSelector selector) {
+        return new RadioManager.ProgramInfo(selector, selector.getPrimaryId(),
+                DAB_ENSEMBLE_IDENTIFIER, /* relatedContents= */ null, /* infoFlags= */ 0,
+                /* signalQuality= */ 1, new RadioMetadata.Builder().build(),
+                /* vendorInfo= */ null);
+    }
+
     private void createRadioTuner() throws Exception {
         mApplicationInfo.targetSdkVersion = TEST_TARGET_SDK_VERSION;
         when(mContextMock.getApplicationInfo()).thenReturn(mApplicationInfo);
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java
index 6c70192..4f469bb 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java
@@ -25,6 +25,7 @@
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioMetadata;
+import android.hardware.radio.UniqueProgramIdentifier;
 import android.os.RemoteException;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -149,12 +150,12 @@
 
     static ProgramList.Chunk makeChunk(boolean purge, boolean complete,
             List<RadioManager.ProgramInfo> modified,
-            List<ProgramSelector.Identifier> removed) throws RemoteException {
+            List<UniqueProgramIdentifier> removed) throws RemoteException {
         ArraySet<RadioManager.ProgramInfo> modifiedSet = new ArraySet<>();
         if (modified != null) {
             modifiedSet.addAll(modified);
         }
-        ArraySet<ProgramSelector.Identifier> removedSet = new ArraySet<>();
+        ArraySet<UniqueProgramIdentifier> removedSet = new ArraySet<>();
         if (removed != null) {
             removedSet.addAll(removed);
         }
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
index 2ef923d..89b91cf 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
@@ -25,7 +25,6 @@
 import android.hardware.broadcastradio.IdentifierType;
 import android.hardware.broadcastradio.ProgramIdentifier;
 import android.hardware.broadcastradio.ProgramInfo;
-import android.hardware.broadcastradio.ProgramListChunk;
 import android.hardware.broadcastradio.Properties;
 import android.hardware.broadcastradio.Result;
 import android.hardware.broadcastradio.VendorKeyValue;
@@ -33,6 +32,7 @@
 import android.hardware.radio.ProgramList;
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
+import android.hardware.radio.UniqueProgramIdentifier;
 import android.os.ServiceSpecificException;
 
 import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
@@ -103,12 +103,6 @@
     private static final ProgramIdentifier TEST_HAL_DAB_FREQUENCY_ID =
             AidlTestUtils.makeHalIdentifier(IdentifierType.DAB_FREQUENCY_KHZ,
                     TEST_DAB_FREQUENCY_VALUE);
-    private static final ProgramIdentifier TEST_HAL_FM_FREQUENCY_ID =
-            AidlTestUtils.makeHalIdentifier(IdentifierType.AMFM_FREQUENCY_KHZ,
-                    TEST_FM_FREQUENCY_VALUE);
-    private static final ProgramIdentifier TEST_HAL_VENDOR_ID =
-            AidlTestUtils.makeHalIdentifier(IdentifierType.VENDOR_START,
-                    TEST_VENDOR_ID_VALUE);
 
     private static final ProgramSelector TEST_DAB_SELECTOR = new ProgramSelector(
             ProgramSelector.PROGRAM_TYPE_DAB, TEST_DAB_SID_EXT_ID,
@@ -117,6 +111,12 @@
     private static final ProgramSelector TEST_FM_SELECTOR =
             AidlTestUtils.makeFmSelector(TEST_FM_FREQUENCY_VALUE);
 
+    private static final UniqueProgramIdentifier TEST_DAB_UNIQUE_ID = new UniqueProgramIdentifier(
+            TEST_DAB_SELECTOR);
+
+    private static final UniqueProgramIdentifier TEST_VENDOR_UNIQUE_ID =
+            new UniqueProgramIdentifier(TEST_VENDOR_ID);
+
     private static final int TEST_ENABLED_TYPE = Announcement.TYPE_EMERGENCY;
     private static final int TEST_ANNOUNCEMENT_FREQUENCY = FM_LOWER_LIMIT + FM_SPACING;
 
@@ -326,57 +326,6 @@
     }
 
     @Test
-    public void chunkFromHalProgramListChunk_withValidChunk() {
-        boolean purge = false;
-        boolean complete = true;
-        android.hardware.broadcastradio.ProgramSelector halDabSelector =
-                AidlTestUtils.makeHalSelector(TEST_HAL_DAB_SID_EXT_ID, new ProgramIdentifier[]{
-                        TEST_HAL_DAB_ENSEMBLE_ID, TEST_HAL_DAB_FREQUENCY_ID});
-        ProgramInfo halDabInfo = AidlTestUtils.makeHalProgramInfo(halDabSelector,
-                TEST_HAL_DAB_SID_EXT_ID, TEST_HAL_DAB_FREQUENCY_ID, TEST_SIGNAL_QUALITY);
-        RadioManager.ProgramInfo dabInfo =
-                ConversionUtils.programInfoFromHalProgramInfo(halDabInfo);
-        ProgramListChunk halChunk = AidlTestUtils.makeHalChunk(purge, complete,
-                new ProgramInfo[]{halDabInfo},
-                new ProgramIdentifier[]{TEST_HAL_VENDOR_ID, TEST_HAL_FM_FREQUENCY_ID});
-
-        ProgramList.Chunk chunk = ConversionUtils.chunkFromHalProgramListChunk(halChunk);
-
-        expect.withMessage("Purged state of the converted valid program list chunk")
-                .that(chunk.isPurge()).isEqualTo(purge);
-        expect.withMessage("Completion state of the converted valid program list chunk")
-                .that(chunk.isComplete()).isEqualTo(complete);
-        expect.withMessage("Modified program info in the converted valid program list chunk")
-                .that(chunk.getModified()).containsExactly(dabInfo);
-        expect.withMessage("Removed program ides in the converted valid program list chunk")
-                .that(chunk.getRemoved()).containsExactly(TEST_VENDOR_ID, TEST_FM_FREQUENCY_ID);
-    }
-
-    @Test
-    public void chunkFromHalProgramListChunk_withInvalidModifiedProgramInfo() {
-        boolean purge = true;
-        boolean complete = false;
-        android.hardware.broadcastradio.ProgramSelector halDabSelector =
-                AidlTestUtils.makeHalSelector(TEST_HAL_DAB_SID_EXT_ID, new ProgramIdentifier[]{
-                        TEST_HAL_DAB_ENSEMBLE_ID, TEST_HAL_DAB_FREQUENCY_ID});
-        ProgramInfo halDabInfo = AidlTestUtils.makeHalProgramInfo(halDabSelector,
-                TEST_HAL_DAB_SID_EXT_ID, TEST_HAL_DAB_ENSEMBLE_ID, TEST_SIGNAL_QUALITY);
-        ProgramListChunk halChunk = AidlTestUtils.makeHalChunk(purge, complete,
-                new ProgramInfo[]{halDabInfo}, new ProgramIdentifier[]{TEST_HAL_FM_FREQUENCY_ID});
-
-        ProgramList.Chunk chunk = ConversionUtils.chunkFromHalProgramListChunk(halChunk);
-
-        expect.withMessage("Purged state of the converted invalid program list chunk")
-                .that(chunk.isPurge()).isEqualTo(purge);
-        expect.withMessage("Completion state of the converted invalid program list chunk")
-                .that(chunk.isComplete()).isEqualTo(complete);
-        expect.withMessage("Modified program info in the converted invalid program list chunk")
-                .that(chunk.getModified()).isEmpty();
-        expect.withMessage("Removed program ids in the converted invalid program list chunk")
-                .that(chunk.getRemoved()).containsExactly(TEST_FM_FREQUENCY_ID);
-    }
-
-    @Test
     public void programSelectorMeetsSdkVersionRequirement_withLowerVersionId_returnsFalse() {
         expect.withMessage("Selector %s without required SDK version", TEST_DAB_SELECTOR)
                 .that(ConversionUtils.programSelectorMeetsSdkVersionRequirement(TEST_DAB_SELECTOR,
@@ -418,7 +367,7 @@
                 TEST_SIGNAL_QUALITY);
         ProgramList.Chunk chunk = new ProgramList.Chunk(/* purge= */ true,
                 /* complete= */ true, Set.of(dabProgramInfo, fmProgramInfo),
-                Set.of(TEST_DAB_SID_EXT_ID, TEST_DAB_ENSEMBLE_ID, TEST_VENDOR_ID));
+                Set.of(TEST_DAB_UNIQUE_ID, TEST_VENDOR_UNIQUE_ID));
 
         ProgramList.Chunk convertedChunk = ConversionUtils.convertChunkToTargetSdkVersion(chunk,
                 T_APP_UID);
@@ -434,8 +383,7 @@
                 .that(convertedChunk.getModified()).containsExactly(fmProgramInfo);
         expect.withMessage(
                 "Removed program ids in the converted program list chunk with lower SDK version")
-                .that(convertedChunk.getRemoved())
-                .containsExactly(TEST_DAB_ENSEMBLE_ID, TEST_VENDOR_ID);
+                .that(convertedChunk.getRemoved()).containsExactly(TEST_VENDOR_UNIQUE_ID);
     }
 
     @Test
@@ -446,7 +394,7 @@
                 TEST_SIGNAL_QUALITY);
         ProgramList.Chunk chunk = new ProgramList.Chunk(/* purge= */ true,
                 /* complete= */ true, Set.of(dabProgramInfo, fmProgramInfo),
-                Set.of(TEST_DAB_SID_EXT_ID, TEST_DAB_ENSEMBLE_ID, TEST_VENDOR_ID));
+                Set.of(TEST_DAB_UNIQUE_ID, TEST_VENDOR_UNIQUE_ID));
 
         ProgramList.Chunk convertedChunk = ConversionUtils.convertChunkToTargetSdkVersion(chunk,
                 U_APP_UID);
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ProgramInfoCacheTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ProgramInfoCacheTest.java
index d54397e..ce27bc1 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ProgramInfoCacheTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ProgramInfoCacheTest.java
@@ -22,6 +22,7 @@
 import android.hardware.radio.ProgramList;
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
+import android.hardware.radio.UniqueProgramIdentifier;
 import android.os.RemoteException;
 import android.util.ArraySet;
 
@@ -32,6 +33,7 @@
 import org.junit.runner.RunWith;
 import org.mockito.junit.MockitoJUnitRunner;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 
@@ -43,6 +45,9 @@
 
     private static final int TEST_SIGNAL_QUALITY = 90;
 
+    private static final int TEST_MAX_NUM_MODIFIED_PER_CHUNK = 2;
+    private static final int TEST_MAX_NUM_REMOVED_PER_CHUNK = 2;
+
     private static final ProgramSelector.Identifier TEST_FM_FREQUENCY_ID =
             new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY,
                     /* value= */ 88_500);
@@ -58,6 +63,8 @@
     private static final ProgramSelector.Identifier TEST_AM_FREQUENCY_ID =
             new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY,
                     /* value= */ 1_700);
+    private static final UniqueProgramIdentifier TEST_AM_UNIQUE_ID = new UniqueProgramIdentifier(
+            TEST_AM_FREQUENCY_ID);
     private static final RadioManager.ProgramInfo TEST_AM_INFO = AidlTestUtils.makeProgramInfo(
             AidlTestUtils.makeProgramSelector(ProgramSelector.PROGRAM_TYPE_FM,
                     TEST_AM_FREQUENCY_ID), TEST_AM_FREQUENCY_ID, TEST_AM_FREQUENCY_ID,
@@ -66,6 +73,8 @@
     private static final ProgramSelector.Identifier TEST_RDS_PI_ID =
             new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_RDS_PI,
                     /* value= */ 15_019);
+    private static final UniqueProgramIdentifier TEST_RDS_PI_UNIQUE_ID =
+            new UniqueProgramIdentifier(TEST_RDS_PI_ID);
     private static final RadioManager.ProgramInfo TEST_RDS_INFO = AidlTestUtils.makeProgramInfo(
             AidlTestUtils.makeProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, TEST_RDS_PI_ID),
             TEST_RDS_PI_ID, new ProgramSelector.Identifier(
@@ -81,11 +90,27 @@
     private static final ProgramSelector.Identifier TEST_DAB_FREQUENCY_ID =
             new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
                     /* value= */ 220_352);
-    private static final RadioManager.ProgramInfo TEST_DAB_INFO = AidlTestUtils.makeProgramInfo(
-            new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB, TEST_DAB_DMB_SID_EXT_ID,
-                    new ProgramSelector.Identifier[]{TEST_DAB_FREQUENCY_ID, TEST_DAB_ENSEMBLE_ID},
-                    /* vendorIds= */ null), TEST_DAB_DMB_SID_EXT_ID, TEST_DAB_FREQUENCY_ID,
-            TEST_SIGNAL_QUALITY);
+    private static final ProgramSelector.Identifier TEST_DAB_FREQUENCY_ID_ALTERNATIVE =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
+                    /* value= */ 220_064);
+    private static final ProgramSelector TEST_DAB_SELECTOR = new ProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_DAB, TEST_DAB_DMB_SID_EXT_ID,
+            new ProgramSelector.Identifier[]{TEST_DAB_FREQUENCY_ID, TEST_DAB_ENSEMBLE_ID},
+            /* vendorIds= */ null);
+    private static final ProgramSelector TEST_DAB_SELECTOR_ALTERNATIVE = new ProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_DAB, TEST_DAB_DMB_SID_EXT_ID,
+            new ProgramSelector.Identifier[]{TEST_DAB_FREQUENCY_ID_ALTERNATIVE,
+                    TEST_DAB_ENSEMBLE_ID}, /* vendorIds= */ null);
+    private static final UniqueProgramIdentifier TEST_DAB_UNIQUE_ID = new UniqueProgramIdentifier(
+            TEST_DAB_SELECTOR);
+    private static final UniqueProgramIdentifier TEST_DAB_UNIQUE_ID_ALTERNATIVE =
+            new UniqueProgramIdentifier(TEST_DAB_SELECTOR_ALTERNATIVE);
+    private static final RadioManager.ProgramInfo TEST_DAB_INFO =
+            AidlTestUtils.makeProgramInfo(TEST_DAB_SELECTOR, TEST_DAB_DMB_SID_EXT_ID,
+                    TEST_DAB_FREQUENCY_ID, TEST_SIGNAL_QUALITY);
+    private static final RadioManager.ProgramInfo TEST_DAB_INFO_ALTERNATIVE =
+            AidlTestUtils.makeProgramInfo(TEST_DAB_SELECTOR_ALTERNATIVE, TEST_DAB_DMB_SID_EXT_ID,
+                    TEST_DAB_FREQUENCY_ID_ALTERNATIVE, TEST_SIGNAL_QUALITY);
 
     private static final ProgramSelector.Identifier TEST_VENDOR_ID =
             new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_VENDOR_START,
@@ -95,8 +120,8 @@
                     TEST_VENDOR_ID), TEST_VENDOR_ID, TEST_VENDOR_ID, TEST_SIGNAL_QUALITY);
 
     private static final ProgramInfoCache FULL_PROGRAM_INFO_CACHE = new ProgramInfoCache(
-            /* filter= */ null, /* complete= */ true,
-            TEST_FM_INFO, TEST_AM_INFO, TEST_RDS_INFO, TEST_DAB_INFO, TEST_VENDOR_INFO);
+            /* filter= */ null, /* complete= */ true, TEST_FM_INFO, TEST_AM_INFO, TEST_RDS_INFO,
+            TEST_DAB_INFO, TEST_DAB_INFO_ALTERNATIVE, TEST_VENDOR_INFO);
 
     @Rule
     public final Expect expect = Expect.create();
@@ -163,6 +188,22 @@
     }
 
     @Test
+    public void updateFromHalProgramListChunk_withInvalidChunk() {
+        RadioManager.ProgramInfo invalidDabInfo = AidlTestUtils.makeProgramInfo(TEST_DAB_SELECTOR,
+                TEST_DAB_DMB_SID_EXT_ID, TEST_DAB_ENSEMBLE_ID, TEST_SIGNAL_QUALITY);
+        ProgramInfoCache cache = new ProgramInfoCache(/* filter= */ null,
+                /* complete= */ false);
+        ProgramListChunk chunk = AidlTestUtils.makeHalChunk(/* purge= */ false,
+                /* complete= */ true, new ProgramInfo[]{AidlTestUtils.programInfoToHalProgramInfo(
+                        invalidDabInfo)}, new ProgramIdentifier[]{});
+
+        cache.updateFromHalProgramListChunk(chunk);
+
+        expect.withMessage("Program cache updated with invalid chunk")
+                .that(cache.toProgramInfoList()).isEmpty();
+    }
+
+    @Test
     public void filterAndUpdateFromInternal_withNullFilter() {
         ProgramInfoCache cache = new ProgramInfoCache(/* filter= */ null,
                 /* complete= */ true);
@@ -172,7 +213,7 @@
         expect.withMessage("Program cache filtered by null filter")
                 .that(cache.toProgramInfoList())
                 .containsExactly(TEST_FM_INFO, TEST_AM_INFO, TEST_RDS_INFO, TEST_DAB_INFO,
-                        TEST_VENDOR_INFO);
+                        TEST_DAB_INFO_ALTERNATIVE, TEST_VENDOR_INFO);
     }
 
     @Test
@@ -186,21 +227,21 @@
         expect.withMessage("Program cache filtered by empty filter")
                 .that(cache.toProgramInfoList())
                 .containsExactly(TEST_FM_INFO, TEST_AM_INFO, TEST_RDS_INFO, TEST_DAB_INFO,
-                        TEST_VENDOR_INFO);
+                        TEST_DAB_INFO_ALTERNATIVE, TEST_VENDOR_INFO);
     }
 
     @Test
     public void filterAndUpdateFromInternal_withFilterByIdentifierType() {
         ProgramInfoCache cache = new ProgramInfoCache(
                 new ProgramList.Filter(Set.of(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY,
-                        ProgramSelector.IDENTIFIER_TYPE_RDS_PI), new ArraySet<>(),
+                        ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT), new ArraySet<>(),
                         /* includeCategories= */ true, /* excludeModifications= */ false));
 
         cache.filterAndUpdateFromInternal(FULL_PROGRAM_INFO_CACHE, /* purge= */ false);
 
         expect.withMessage("Program cache filtered by identifier type")
-                .that(cache.toProgramInfoList())
-                .containsExactly(TEST_FM_INFO, TEST_AM_INFO, TEST_RDS_INFO);
+                .that(cache.toProgramInfoList()).containsExactly(TEST_FM_INFO, TEST_AM_INFO,
+                        TEST_DAB_INFO, TEST_DAB_INFO_ALTERNATIVE);
     }
 
     @Test
@@ -208,20 +249,60 @@
         ProgramInfoCache cache = new ProgramInfoCache(new ProgramList.Filter(
                 new ArraySet<>(), Set.of(TEST_FM_FREQUENCY_ID, TEST_DAB_DMB_SID_EXT_ID),
                 /* includeCategories= */ true, /* excludeModifications= */ false));
-        int maxNumModifiedPerChunk = 2;
-        int maxNumRemovedPerChunk = 2;
 
         List<ProgramList.Chunk> programListChunks = cache.filterAndUpdateFromInternal(
-                FULL_PROGRAM_INFO_CACHE, /* purge= */ true, maxNumModifiedPerChunk,
-                maxNumRemovedPerChunk);
+                FULL_PROGRAM_INFO_CACHE, /* purge= */ false, TEST_MAX_NUM_MODIFIED_PER_CHUNK,
+                TEST_MAX_NUM_REMOVED_PER_CHUNK);
 
         expect.withMessage("Program cache filtered by identifier")
-                .that(cache.toProgramInfoList()).containsExactly(TEST_FM_INFO, TEST_DAB_INFO);
+                .that(cache.toProgramInfoList()).containsExactly(TEST_FM_INFO, TEST_DAB_INFO,
+                        TEST_DAB_INFO_ALTERNATIVE);
         verifyChunkListPurge(programListChunks, /* purge= */ true);
-        verifyChunkListComplete(programListChunks, FULL_PROGRAM_INFO_CACHE.isComplete());
+        verifyChunkListComplete(programListChunks, cache.isComplete());
+        verifyChunkListModified(programListChunks, TEST_MAX_NUM_MODIFIED_PER_CHUNK, TEST_FM_INFO,
+                TEST_DAB_INFO, TEST_DAB_INFO_ALTERNATIVE);
+        verifyChunkListRemoved(programListChunks, TEST_MAX_NUM_REMOVED_PER_CHUNK);
+    }
+
+    @Test
+    public void filterAndUpdateFromInternal_withPurging() {
+        ProgramInfoCache cache = new ProgramInfoCache(new ProgramList.Filter(new ArraySet<>(),
+                new ArraySet<>(), /* includeCategories= */ true, /* excludeModifications= */ false),
+                /* complete= */ true, TEST_RDS_INFO, TEST_DAB_INFO);
+        ProgramInfoCache otherCache = new ProgramInfoCache(/* filter= */ null, /* complete= */ true,
+                TEST_FM_INFO, TEST_RDS_INFO, TEST_DAB_INFO_ALTERNATIVE);
+
+        List<ProgramList.Chunk> programListChunks = cache.filterAndUpdateFromInternal(otherCache,
+                /* purge= */ true, TEST_MAX_NUM_MODIFIED_PER_CHUNK, TEST_MAX_NUM_REMOVED_PER_CHUNK);
+
+        expect.withMessage("Program cache filtered with purging").that(cache.toProgramInfoList())
+                .containsExactly(TEST_FM_INFO, TEST_RDS_INFO, TEST_DAB_INFO_ALTERNATIVE);
+        verifyChunkListPurge(programListChunks, /* purge= */ true);
+        verifyChunkListModified(programListChunks, TEST_MAX_NUM_MODIFIED_PER_CHUNK, TEST_FM_INFO,
+                TEST_RDS_INFO, TEST_DAB_INFO_ALTERNATIVE);
+        verifyChunkListRemoved(programListChunks, TEST_MAX_NUM_REMOVED_PER_CHUNK);
+    }
+
+    @Test
+    public void filterAndUpdateFromInternal_withoutPurging() {
+        ProgramInfoCache cache = new ProgramInfoCache(new ProgramList.Filter(new ArraySet<>(),
+                new ArraySet<>(), /* includeCategories= */ true, /* excludeModifications= */ false),
+                /* complete= */ true, TEST_RDS_INFO, TEST_DAB_INFO);
+        ProgramInfoCache otherCache = new ProgramInfoCache(/* filter= */ null, /* complete= */ true,
+                TEST_FM_INFO, TEST_RDS_INFO, TEST_DAB_INFO_ALTERNATIVE);
+        int maxNumModifiedPerChunk = 1;
+
+        List<ProgramList.Chunk> programListChunks = cache.filterAndUpdateFromInternal(otherCache,
+                /* purge= */ false, maxNumModifiedPerChunk, TEST_MAX_NUM_REMOVED_PER_CHUNK);
+
+        expect.withMessage("Program cache filtered without puring").that(cache.toProgramInfoList())
+                .containsExactly(TEST_FM_INFO, TEST_RDS_INFO, TEST_DAB_INFO_ALTERNATIVE);
+        verifyChunkListPurge(programListChunks, /* purge= */ false);
+        verifyChunkListComplete(programListChunks, cache.isComplete());
         verifyChunkListModified(programListChunks, maxNumModifiedPerChunk, TEST_FM_INFO,
-                TEST_DAB_INFO);
-        verifyChunkListRemoved(programListChunks, maxNumRemovedPerChunk);
+                TEST_DAB_INFO_ALTERNATIVE);
+        verifyChunkListRemoved(programListChunks, TEST_MAX_NUM_REMOVED_PER_CHUNK,
+                TEST_DAB_UNIQUE_ID);
     }
 
     @Test
@@ -230,20 +311,19 @@
                 new ArraySet<>(), /* includeCategories= */ false,
                 /* excludeModifications= */ false));
         int maxNumModifiedPerChunk = 3;
-        int maxNumRemovedPerChunk = 2;
 
         List<ProgramList.Chunk> programListChunks = cache.filterAndUpdateFromInternal(
                 FULL_PROGRAM_INFO_CACHE, /* purge= */ false, maxNumModifiedPerChunk,
-                maxNumRemovedPerChunk);
+                TEST_MAX_NUM_REMOVED_PER_CHUNK);
 
         expect.withMessage("Program cache filtered by excluding categories")
-                .that(cache.toProgramInfoList())
-                .containsExactly(TEST_FM_INFO, TEST_AM_INFO, TEST_RDS_INFO, TEST_DAB_INFO);
+                .that(cache.toProgramInfoList()).containsExactly(TEST_FM_INFO, TEST_AM_INFO,
+                        TEST_RDS_INFO, TEST_DAB_INFO, TEST_DAB_INFO_ALTERNATIVE);
         verifyChunkListPurge(programListChunks, /* purge= */ true);
-        verifyChunkListComplete(programListChunks, FULL_PROGRAM_INFO_CACHE.isComplete());
+        verifyChunkListComplete(programListChunks, cache.isComplete());
         verifyChunkListModified(programListChunks, maxNumModifiedPerChunk, TEST_FM_INFO,
-                TEST_AM_INFO, TEST_RDS_INFO, TEST_DAB_INFO);
-        verifyChunkListRemoved(programListChunks, maxNumRemovedPerChunk);
+                TEST_AM_INFO, TEST_RDS_INFO, TEST_DAB_INFO, TEST_DAB_INFO_ALTERNATIVE);
+        verifyChunkListRemoved(programListChunks, TEST_MAX_NUM_REMOVED_PER_CHUNK);
     }
 
     @Test
@@ -254,21 +334,21 @@
         ProgramInfoCache cache = new ProgramInfoCache(filterExcludingModifications,
                 /* complete= */ true, TEST_FM_INFO, TEST_RDS_INFO, TEST_AM_INFO, TEST_DAB_INFO);
         ProgramInfoCache halCache = new ProgramInfoCache(/* filter= */ null, /* complete= */ false,
-                TEST_FM_INFO_MODIFIED, TEST_VENDOR_INFO);
-        int maxNumModifiedPerChunk = 2;
-        int maxNumRemovedPerChunk = 2;
+                TEST_FM_INFO_MODIFIED, TEST_DAB_INFO_ALTERNATIVE, TEST_VENDOR_INFO);
 
         List<ProgramList.Chunk> programListChunks = cache.filterAndUpdateFromInternal(halCache,
-                /* purge= */ false, maxNumModifiedPerChunk, maxNumRemovedPerChunk);
+                /* purge= */ false, TEST_MAX_NUM_MODIFIED_PER_CHUNK,
+                TEST_MAX_NUM_REMOVED_PER_CHUNK);
 
         expect.withMessage("Program cache filtered by excluding modifications")
                 .that(cache.toProgramInfoList())
-                .containsExactly(TEST_FM_INFO, TEST_VENDOR_INFO);
+                .containsExactly(TEST_FM_INFO, TEST_DAB_INFO_ALTERNATIVE, TEST_VENDOR_INFO);
         verifyChunkListPurge(programListChunks, /* purge= */ false);
         verifyChunkListComplete(programListChunks, halCache.isComplete());
-        verifyChunkListModified(programListChunks, maxNumModifiedPerChunk, TEST_VENDOR_INFO);
-        verifyChunkListRemoved(programListChunks, maxNumRemovedPerChunk, TEST_RDS_PI_ID,
-                TEST_AM_FREQUENCY_ID, TEST_DAB_DMB_SID_EXT_ID);
+        verifyChunkListModified(programListChunks, TEST_MAX_NUM_MODIFIED_PER_CHUNK,
+                TEST_VENDOR_INFO, TEST_DAB_INFO_ALTERNATIVE);
+        verifyChunkListRemoved(programListChunks, TEST_MAX_NUM_REMOVED_PER_CHUNK,
+                TEST_RDS_PI_UNIQUE_ID, TEST_AM_UNIQUE_ID, TEST_DAB_UNIQUE_ID);
     }
 
     @Test
@@ -276,69 +356,88 @@
         ProgramInfoCache cache = new ProgramInfoCache(new ProgramList.Filter(new ArraySet<>(),
                 new ArraySet<>(), /* includeCategories= */ true,
                 /* excludeModifications= */ false),
-                /* complete= */ true, TEST_FM_INFO, TEST_RDS_INFO);
+                /* complete= */ true, TEST_FM_INFO, TEST_RDS_INFO, TEST_DAB_INFO);
         ProgramInfoCache halCache = new ProgramInfoCache(/* filter= */ null, /* complete= */ false,
-                TEST_FM_INFO_MODIFIED, TEST_DAB_INFO, TEST_VENDOR_INFO);
-        int maxNumModifiedPerChunk = 2;
-        int maxNumRemovedPerChunk = 2;
+                TEST_FM_INFO_MODIFIED, TEST_DAB_INFO_ALTERNATIVE, TEST_VENDOR_INFO);
 
         List<ProgramList.Chunk> programListChunks = cache.filterAndUpdateFromInternal(halCache,
-                /* purge= */ true, maxNumModifiedPerChunk, maxNumRemovedPerChunk);
+                /* purge= */ true, TEST_MAX_NUM_MODIFIED_PER_CHUNK, TEST_MAX_NUM_REMOVED_PER_CHUNK);
 
         expect.withMessage("Purged program cache").that(cache.toProgramInfoList())
-                .containsExactly(TEST_FM_INFO_MODIFIED, TEST_DAB_INFO, TEST_VENDOR_INFO);
+                .containsExactly(TEST_FM_INFO_MODIFIED, TEST_DAB_INFO_ALTERNATIVE,
+                        TEST_VENDOR_INFO);
         verifyChunkListPurge(programListChunks, /* purge= */ true);
         verifyChunkListComplete(programListChunks, halCache.isComplete());
-        verifyChunkListModified(programListChunks, maxNumModifiedPerChunk, TEST_FM_INFO_MODIFIED,
-                TEST_DAB_INFO, TEST_VENDOR_INFO);
-        verifyChunkListRemoved(programListChunks, maxNumRemovedPerChunk);
+        verifyChunkListModified(programListChunks, TEST_MAX_NUM_MODIFIED_PER_CHUNK,
+                TEST_FM_INFO_MODIFIED, TEST_DAB_INFO_ALTERNATIVE, TEST_VENDOR_INFO);
+        verifyChunkListRemoved(programListChunks, TEST_MAX_NUM_REMOVED_PER_CHUNK);
     }
 
     @Test
-    public void filterAndApplyChunkInternal_withPurgingIncompleteChunk() throws RemoteException {
+    public void filterAndApplyChunkInternal_withPurgingAndIncompleteChunk() throws RemoteException {
         ProgramInfoCache cache = new ProgramInfoCache(/* filter= */ null,
-                /* complete= */ false, TEST_FM_INFO, TEST_DAB_INFO);
-        ProgramList.Chunk chunk = AidlTestUtils.makeChunk(/* purge= */ true, /* complete= */ false,
-                List.of(TEST_FM_INFO_MODIFIED, TEST_RDS_INFO, TEST_VENDOR_INFO),
-                List.of(TEST_DAB_DMB_SID_EXT_ID));
-        int maxNumModifiedPerChunk = 2;
-        int maxNumRemovedPerChunk = 2;
+                /* complete= */ false, TEST_FM_INFO, TEST_RDS_INFO, TEST_DAB_INFO);
+        ProgramListChunk halChunk = AidlTestUtils.makeHalChunk(/* purge= */ true,
+                /* complete= */ false, List.of(TEST_FM_INFO_MODIFIED,
+                        TEST_DAB_INFO_ALTERNATIVE, TEST_VENDOR_INFO), new ArrayList<>());
 
-        List<ProgramList.Chunk> programListChunks = cache.filterAndApplyChunkInternal(chunk,
-                maxNumModifiedPerChunk, maxNumRemovedPerChunk);
+        List<ProgramList.Chunk> programListChunks = cache.filterAndApplyChunkInternal(halChunk,
+                TEST_MAX_NUM_MODIFIED_PER_CHUNK, TEST_MAX_NUM_REMOVED_PER_CHUNK);
 
-        expect.withMessage("Program cache applied with non-purging and complete chunk")
-                .that(cache.toProgramInfoList())
-                .containsExactly(TEST_FM_INFO_MODIFIED, TEST_RDS_INFO, TEST_VENDOR_INFO);
+        expect.withMessage("Program cache applied with purge-enabled and complete chunk")
+                .that(cache.toProgramInfoList()).containsExactly(TEST_FM_INFO_MODIFIED,
+                        TEST_DAB_INFO_ALTERNATIVE, TEST_VENDOR_INFO);
         verifyChunkListPurge(programListChunks, /* purge= */ true);
         verifyChunkListComplete(programListChunks, /* complete= */ false);
-        verifyChunkListModified(programListChunks, maxNumModifiedPerChunk, TEST_FM_INFO_MODIFIED,
-                TEST_RDS_INFO, TEST_VENDOR_INFO);
-        verifyChunkListRemoved(programListChunks, maxNumRemovedPerChunk);
+        verifyChunkListModified(programListChunks, TEST_MAX_NUM_MODIFIED_PER_CHUNK,
+                TEST_FM_INFO_MODIFIED, TEST_DAB_INFO_ALTERNATIVE, TEST_VENDOR_INFO);
+        verifyChunkListRemoved(programListChunks, TEST_MAX_NUM_REMOVED_PER_CHUNK);
     }
 
     @Test
-    public void filterAndApplyChunk_withNonPurgingCompleteChunk() throws RemoteException {
-        ProgramInfoCache cache = new ProgramInfoCache(/* filter= */ null,
-                /* complete= */ false, TEST_FM_INFO, TEST_RDS_INFO, TEST_AM_INFO, TEST_DAB_INFO);
-        ProgramList.Chunk chunk = AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true,
-                List.of(TEST_FM_INFO_MODIFIED, TEST_VENDOR_INFO),
+    public void filterAndApplyChunk_withNonPurgingAndIncompleteChunk() throws RemoteException {
+        ProgramInfoCache cache = new ProgramInfoCache(/* filter= */ null, /* complete= */ false,
+                TEST_FM_INFO, TEST_RDS_INFO, TEST_AM_INFO, TEST_DAB_INFO);
+        ProgramListChunk halChunk = AidlTestUtils.makeHalChunk(/* purge= */ false,
+                /* complete= */ false, List.of(TEST_FM_INFO_MODIFIED, TEST_DAB_INFO_ALTERNATIVE,
+                        TEST_VENDOR_INFO), List.of(TEST_RDS_PI_ID, TEST_AM_FREQUENCY_ID));
+
+        List<ProgramList.Chunk> programListChunks = cache.filterAndApplyChunkInternal(halChunk,
+                TEST_MAX_NUM_MODIFIED_PER_CHUNK, TEST_MAX_NUM_REMOVED_PER_CHUNK);
+
+        expect.withMessage("Program cache applied with non-purging and incomplete chunk")
+                .that(cache.toProgramInfoList()).containsExactly(TEST_DAB_INFO,
+                        TEST_DAB_INFO_ALTERNATIVE, TEST_FM_INFO_MODIFIED, TEST_VENDOR_INFO);
+        verifyChunkListPurge(programListChunks, /* purge= */ false);
+        verifyChunkListComplete(programListChunks, /* complete= */ false);
+        verifyChunkListModified(programListChunks, TEST_MAX_NUM_MODIFIED_PER_CHUNK,
+                TEST_FM_INFO_MODIFIED, TEST_DAB_INFO_ALTERNATIVE, TEST_VENDOR_INFO);
+        verifyChunkListRemoved(programListChunks, TEST_MAX_NUM_REMOVED_PER_CHUNK,
+                TEST_RDS_PI_UNIQUE_ID, TEST_AM_UNIQUE_ID);
+    }
+
+    @Test
+    public void filterAndApplyChunk_withNonPurgingAndCompleteChunk() throws RemoteException {
+        ProgramInfoCache cache = new ProgramInfoCache(/* filter= */ null, /* complete= */ false,
+                TEST_FM_INFO, TEST_RDS_INFO, TEST_AM_INFO, TEST_DAB_INFO,
+                TEST_DAB_INFO_ALTERNATIVE);
+        ProgramListChunk halChunk = AidlTestUtils.makeHalChunk(/* purge= */ false,
+                /* complete= */ true, List.of(TEST_FM_INFO_MODIFIED, TEST_VENDOR_INFO),
                 List.of(TEST_RDS_PI_ID, TEST_AM_FREQUENCY_ID, TEST_DAB_DMB_SID_EXT_ID));
-        int maxNumModifiedPerChunk = 2;
-        int maxNumRemovedPerChunk = 2;
 
-        List<ProgramList.Chunk> programListChunks = cache.filterAndApplyChunkInternal(chunk,
-                maxNumModifiedPerChunk, maxNumRemovedPerChunk);
+        List<ProgramList.Chunk> programListChunks = cache.filterAndApplyChunkInternal(halChunk,
+                TEST_MAX_NUM_MODIFIED_PER_CHUNK, TEST_MAX_NUM_REMOVED_PER_CHUNK);
 
-        expect.withMessage("Program cache applied with purge-enabled complete chunk")
+        expect.withMessage("Program cache applied with non-purging and complete chunk")
                 .that(cache.toProgramInfoList())
                 .containsExactly(TEST_FM_INFO_MODIFIED, TEST_VENDOR_INFO);
         verifyChunkListPurge(programListChunks, /* purge= */ false);
         verifyChunkListComplete(programListChunks, /* complete= */ true);
-        verifyChunkListModified(programListChunks, maxNumModifiedPerChunk, TEST_FM_INFO_MODIFIED,
-                TEST_VENDOR_INFO);
-        verifyChunkListRemoved(programListChunks, maxNumRemovedPerChunk, TEST_RDS_PI_ID,
-                TEST_AM_FREQUENCY_ID, TEST_DAB_DMB_SID_EXT_ID);
+        verifyChunkListModified(programListChunks, TEST_MAX_NUM_MODIFIED_PER_CHUNK,
+                TEST_FM_INFO_MODIFIED, TEST_VENDOR_INFO);
+        verifyChunkListRemoved(programListChunks, TEST_MAX_NUM_REMOVED_PER_CHUNK,
+                TEST_RDS_PI_UNIQUE_ID, TEST_AM_UNIQUE_ID, TEST_DAB_UNIQUE_ID,
+                TEST_DAB_UNIQUE_ID_ALTERNATIVE);
     }
 
     private void verifyChunkListPurge(List<ProgramList.Chunk> chunks, boolean purge) {
@@ -387,17 +486,17 @@
                 .that(actualSet).containsExactlyElementsIn(expectedProgramInfos);
     }
 
-    private void verifyChunkListRemoved(List<ProgramList.Chunk> chunks,
-            int maxRemovedPerChunk, ProgramSelector.Identifier... expectedIdentifiers) {
+    private void verifyChunkListRemoved(List<ProgramList.Chunk> chunks, int maxRemovedPerChunk,
+            UniqueProgramIdentifier... expectedIdentifiers) {
         if (chunks.isEmpty()) {
             expect.withMessage("Empty program info list")
                     .that(expectedIdentifiers.length).isEqualTo(0);
             return;
         }
 
-        ArraySet<ProgramSelector.Identifier> actualSet = new ArraySet<>();
+        ArraySet<UniqueProgramIdentifier> actualSet = new ArraySet<>();
         for (int i = 0; i < chunks.size(); i++) {
-            Set<ProgramSelector.Identifier> chunkRemoved = chunks.get(i).getRemoved();
+            Set<UniqueProgramIdentifier> chunkRemoved = chunks.get(i).getRemoved();
             actualSet.addAll(chunkRemoved);
 
             expect.withMessage("Chunk %s removed identifier array size ", i)
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
index 84aa864..a195228 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
@@ -18,8 +18,6 @@
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 
-import static com.google.common.truth.Truth.assertWithMessage;
-
 import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -47,6 +45,7 @@
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioTuner;
+import android.hardware.radio.UniqueProgramIdentifier;
 import android.os.Binder;
 import android.os.ParcelableException;
 import android.os.RemoteException;
@@ -59,13 +58,18 @@
 import com.android.server.broadcastradio.ExtendedRadioMockitoTestCase;
 import com.android.server.broadcastradio.RadioServiceUserController;
 
+import com.google.common.truth.Expect;
+
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.verification.VerificationWithTimeout;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -94,10 +98,6 @@
     private static final ProgramSelector.Identifier TEST_FM_FREQUENCY_ID =
             new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY,
                     /* value= */ 88_500);
-    private static final ProgramSelector.Identifier TEST_RDS_PI_ID =
-            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_RDS_PI,
-                    /* value= */ 15_019);
-
     private static final RadioManager.ProgramInfo TEST_FM_INFO = AidlTestUtils.makeProgramInfo(
             AidlTestUtils.makeProgramSelector(ProgramSelector.PROGRAM_TYPE_FM,
                     TEST_FM_FREQUENCY_ID), TEST_FM_FREQUENCY_ID, TEST_FM_FREQUENCY_ID,
@@ -106,11 +106,37 @@
             AidlTestUtils.makeProgramInfo(AidlTestUtils.makeProgramSelector(
                     ProgramSelector.PROGRAM_TYPE_FM, TEST_FM_FREQUENCY_ID), TEST_FM_FREQUENCY_ID,
                     TEST_FM_FREQUENCY_ID, /* signalQuality= */ 100);
-    private static final RadioManager.ProgramInfo TEST_RDS_INFO = AidlTestUtils.makeProgramInfo(
-            AidlTestUtils.makeProgramSelector(ProgramSelector.PROGRAM_TYPE_FM, TEST_RDS_PI_ID),
-            TEST_RDS_PI_ID, new ProgramSelector.Identifier(
-                    ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 89_500),
-            SIGNAL_QUALITY);
+
+    private static final ProgramSelector.Identifier TEST_DAB_FREQUENCY_ID =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
+                    /* value= */ 220_352);
+    private static final ProgramSelector.Identifier TEST_DAB_FREQUENCY_ID_ALT =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
+                    /* value= */ 220_064);
+    private static final ProgramSelector.Identifier TEST_DAB_SID_EXT_ID =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT,
+                    /* value= */ 0xA000000111L);
+    private static final ProgramSelector.Identifier TEST_DAB_ENSEMBLE_ID =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
+                    /* value= */ 0x1001);
+    private static final ProgramSelector TEST_DAB_SELECTOR = new ProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_DAB, TEST_DAB_SID_EXT_ID,
+            new ProgramSelector.Identifier[]{TEST_DAB_FREQUENCY_ID, TEST_DAB_ENSEMBLE_ID},
+            /* vendorIds= */ null);
+    private static final ProgramSelector TEST_DAB_SELECTOR_ALT = new ProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_DAB, TEST_DAB_SID_EXT_ID,
+            new ProgramSelector.Identifier[]{TEST_DAB_FREQUENCY_ID_ALT, TEST_DAB_ENSEMBLE_ID},
+            /* vendorIds= */ null);
+    private static final UniqueProgramIdentifier TEST_DAB_UNIQUE_ID = new UniqueProgramIdentifier(
+            TEST_DAB_SELECTOR);
+    private static final UniqueProgramIdentifier TEST_DAB_UNIQUE_ID_ALT =
+            new UniqueProgramIdentifier(TEST_DAB_SELECTOR_ALT);
+    private static final RadioManager.ProgramInfo TEST_DAB_INFO =
+            AidlTestUtils.makeProgramInfo(TEST_DAB_SELECTOR, TEST_DAB_SID_EXT_ID,
+                    TEST_DAB_FREQUENCY_ID, SIGNAL_QUALITY);
+    private static final RadioManager.ProgramInfo TEST_DAB_INFO_ALT =
+            AidlTestUtils.makeProgramInfo(TEST_DAB_SELECTOR_ALT, TEST_DAB_SID_EXT_ID,
+                    TEST_DAB_FREQUENCY_ID_ALT, SIGNAL_QUALITY);
 
     // Mocks
     @Mock
@@ -129,6 +155,9 @@
 
     private TunerSession[] mTunerSessions;
 
+    @Rule
+    public final Expect expect = Expect.create();
+
     @Override
     protected void initializeSession(StaticMockitoSessionBuilder builder) {
         builder.spyStatic(RadioServiceUserController.class).spyStatic(CompatChanges.class)
@@ -225,7 +254,7 @@
         openAidlClients(numSessions);
 
         for (int index = 0; index < numSessions; index++) {
-            assertWithMessage("Session of index %s close state", index)
+            expect.withMessage("Session of index %s close state", index)
                     .that(mTunerSessions[index].isClosed()).isFalse();
         }
     }
@@ -257,7 +286,7 @@
 
         RadioManager.BandConfig config = mTunerSessions[0].getConfiguration();
 
-        assertWithMessage("Session configuration").that(config)
+        expect.withMessage("Session configuration").that(config)
                 .isEqualTo(FM_BAND_CONFIG);
     }
 
@@ -267,7 +296,7 @@
 
         mTunerSessions[0].setMuted(/* mute= */ false);
 
-        assertWithMessage("Session mute state after setting unmuted")
+        expect.withMessage("Session mute state after setting unmuted")
                 .that(mTunerSessions[0].isMuted()).isFalse();
     }
 
@@ -277,7 +306,7 @@
 
         mTunerSessions[0].setMuted(/* mute= */ true);
 
-        assertWithMessage("Session mute state after setting muted")
+        expect.withMessage("Session mute state after setting muted")
                 .that(mTunerSessions[0].isMuted()).isTrue();
     }
 
@@ -287,7 +316,7 @@
 
         mTunerSessions[0].close();
 
-        assertWithMessage("Close state of broadcast radio service session")
+        expect.withMessage("Close state of broadcast radio service session")
                 .that(mTunerSessions[0].isClosed()).isTrue();
     }
 
@@ -301,11 +330,11 @@
 
         for (int index = 0; index < numSessions; index++) {
             if (index == closeIdx) {
-                assertWithMessage(
+                expect.withMessage(
                         "Close state of broadcast radio service session of index %s", index)
                         .that(mTunerSessions[index].isClosed()).isTrue();
             } else {
-                assertWithMessage(
+                expect.withMessage(
                         "Close state of broadcast radio service session of index %s", index)
                         .that(mTunerSessions[index].isClosed()).isFalse();
             }
@@ -320,7 +349,7 @@
         mTunerSessions[0].close(errorCode);
 
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onError(errorCode);
-        assertWithMessage("Close state of broadcast radio service session")
+        expect.withMessage("Close state of broadcast radio service session")
                 .that(mTunerSessions[0].isClosed()).isTrue();
     }
 
@@ -334,7 +363,7 @@
 
         for (int index = 0; index < numSessions; index++) {
             verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT).onError(errorCode);
-            assertWithMessage("Close state of broadcast radio service session of index %s", index)
+            expect.withMessage("Close state of broadcast radio service session of index %s", index)
                     .that(mTunerSessions[index].isClosed()).isTrue();
         }
     }
@@ -383,22 +412,12 @@
 
     @Test
     public void tune_withUnsupportedSelector_throwsException() throws Exception {
-        ProgramSelector.Identifier dabPrimaryId =
-                new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT,
-                        /* value= */ 0xA000000111L);
-        ProgramSelector.Identifier[] dabSecondaryIds =  new ProgramSelector.Identifier[]{
-                new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
-                        /* value= */ 1337),
-                new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
-                        /* value= */ 225648)};
-        ProgramSelector unsupportedSelector = new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB,
-                dabPrimaryId, dabSecondaryIds, /* vendorIds= */ null);
         openAidlClients(/* numClients= */ 1);
 
         UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
-                () -> mTunerSessions[0].tune(unsupportedSelector));
+                () -> mTunerSessions[0].tune(TEST_DAB_SELECTOR));
 
-        assertWithMessage("Exception for tuning on unsupported program selector")
+        expect.withMessage("Exception for tuning on unsupported program selector")
                 .that(thrown).hasMessageThat().contains("tune: NOT_SUPPORTED");
     }
 
@@ -413,7 +432,7 @@
         IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
                 () -> mTunerSessions[0].tune(invalidSel));
 
-        assertWithMessage("Exception for tuning on DAB selector without DAB_SID_EXT primary id")
+        expect.withMessage("Exception for tuning on DAB selector without DAB_SID_EXT primary id")
                 .that(thrown).hasMessageThat().contains("tune: INVALID_ARGUMENTS");
     }
 
@@ -457,7 +476,7 @@
             mTunerSessions[0].tune(sel);
         });
 
-        assertWithMessage("Unknown error HAL exception when tuning")
+        expect.withMessage("Unknown error HAL exception when tuning")
                 .that(thrown).hasMessageThat().contains("UNKNOWN_ERROR");
     }
 
@@ -520,7 +539,7 @@
             mTunerSessions[0].step(/* directionDown= */ true, /* skipSubChannel= */ false);
         });
 
-        assertWithMessage("Exception for stepping when HAL is in invalid state")
+        expect.withMessage("Exception for stepping when HAL is in invalid state")
                 .that(thrown).hasMessageThat().contains("INVALID_STATE");
     }
 
@@ -599,7 +618,7 @@
             mTunerSessions[0].seek(/* directionDown= */ true, /* skipSubChannel= */ false);
         });
 
-        assertWithMessage("Internal error HAL exception when seeking")
+        expect.withMessage("Internal error HAL exception when seeking")
                 .that(thrown).hasMessageThat().contains("INTERNAL_ERROR");
     }
 
@@ -636,7 +655,7 @@
             mTunerSessions[0].cancel();
         });
 
-        assertWithMessage("Exception for canceling when HAL throws remote exception")
+        expect.withMessage("Exception for canceling when HAL throws remote exception")
                 .that(thrown).hasMessageThat().contains(exceptionMessage);
     }
 
@@ -649,7 +668,7 @@
             mTunerSessions[0].getImage(imageId);
         });
 
-        assertWithMessage("Get image exception")
+        expect.withMessage("Get image exception")
                 .that(thrown).hasMessageThat().contains("Image ID is missing");
     }
 
@@ -660,7 +679,7 @@
 
         Bitmap imageTest = mTunerSessions[0].getImage(imageId);
 
-        assertWithMessage("Null image").that(imageTest).isEqualTo(null);
+        expect.withMessage("Null image").that(imageTest).isEqualTo(null);
     }
 
     @Test
@@ -674,7 +693,7 @@
             mTunerSessions[0].getImage(/* id= */ 1);
         });
 
-        assertWithMessage("Exception for getting image when HAL throws remote exception")
+        expect.withMessage("Exception for getting image when HAL throws remote exception")
                 .that(thrown).hasMessageThat().contains(exceptionMessage);
     }
 
@@ -702,18 +721,19 @@
         openAidlClients(/* numClients= */ 1);
         ProgramList.Filter filter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(),
                 /* includeCategories= */ true, /* excludeModifications= */ false);
-        ProgramFilter halFilter = ConversionUtils.filterToHalProgramFilter(filter);
-        List<RadioManager.ProgramInfo> modified = List.of(TEST_FM_INFO, TEST_RDS_INFO);
-        List<ProgramSelector.Identifier> removed = new ArrayList<>();
+        List<RadioManager.ProgramInfo> modified = List.of(TEST_FM_INFO, TEST_DAB_INFO,
+                TEST_DAB_INFO_ALT);
+        List<ProgramSelector.Identifier> halRemoved = new ArrayList<>();
+        List<UniqueProgramIdentifier> removed = new ArrayList<>();
         ProgramListChunk halProgramList = AidlTestUtils.makeHalChunk(/* purge= */ true,
-                /* complete= */ true, modified, removed);
+                /* complete= */ true, modified, halRemoved);
         ProgramList.Chunk expectedProgramList =
                 AidlTestUtils.makeChunk(/* purge= */ true, /* complete= */ true, modified, removed);
 
         mTunerSessions[0].startProgramListUpdates(filter);
         mHalTunerCallback.onProgramListUpdated(halProgramList);
 
-        verify(mBroadcastRadioMock).startProgramListUpdates(halFilter);
+        verifyHalProgramListUpdatesInvocation(filter);
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
                 .onProgramListUpdated(expectedProgramList);
     }
@@ -723,19 +743,23 @@
         openAidlClients(/* numClients= */ 1);
         ProgramList.Filter filter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(),
                 /* includeCategories= */ true, /* excludeModifications= */ false);
+        List<RadioManager.ProgramInfo> modifiedInfo = List.of(TEST_FM_INFO, TEST_DAB_INFO,
+                TEST_DAB_INFO_ALT);
         mTunerSessions[0].startProgramListUpdates(filter);
         mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ true,
-                /* complete= */ true, List.of(TEST_FM_INFO, TEST_RDS_INFO), new ArrayList<>()));
+                /* complete= */ true, modifiedInfo, new ArrayList<>()));
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated(
-                AidlTestUtils.makeChunk(/* purge= */ true, /* complete= */ true,
-                        List.of(TEST_FM_INFO, TEST_RDS_INFO), new ArrayList<>()));
+                AidlTestUtils.makeChunk(/* purge= */ true, /* complete= */ true, modifiedInfo,
+                        new ArrayList<>()));
 
         mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false,
-                /* complete= */ true, List.of(TEST_FM_INFO_MODIFIED), List.of(TEST_RDS_PI_ID)));
+                /* complete= */ true, List.of(TEST_FM_INFO_MODIFIED),
+                List.of(TEST_DAB_SID_EXT_ID)));
 
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated(
                 AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true,
-                        List.of(TEST_FM_INFO_MODIFIED), List.of(TEST_RDS_PI_ID)));
+                        List.of(TEST_FM_INFO_MODIFIED),
+                        List.of(TEST_DAB_UNIQUE_ID, TEST_DAB_UNIQUE_ID_ALT)));
     }
 
     @Test
@@ -743,17 +767,21 @@
         openAidlClients(/* numClients= */ 1);
         ProgramList.Filter filter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(),
                 /* includeCategories= */ true, /* excludeModifications= */ false);
+        List<RadioManager.ProgramInfo> modifiedInfo = List.of(TEST_FM_INFO, TEST_DAB_INFO,
+                TEST_DAB_INFO_ALT);
         mTunerSessions[0].startProgramListUpdates(filter);
         mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ true,
-                /* complete= */ true, List.of(TEST_FM_INFO, TEST_RDS_INFO), new ArrayList<>()));
+                /* complete= */ true, modifiedInfo, new ArrayList<>()));
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated(
-                AidlTestUtils.makeChunk(/* purge= */ true, /* complete= */ true,
-                        List.of(TEST_FM_INFO, TEST_RDS_INFO), new ArrayList<>()));
+                AidlTestUtils.makeChunk(/* purge= */ true, /* complete= */ true, modifiedInfo,
+                        new ArrayList<>()));
         mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false,
-                /* complete= */ true, List.of(TEST_FM_INFO_MODIFIED), List.of(TEST_RDS_PI_ID)));
+                /* complete= */ true, List.of(TEST_FM_INFO_MODIFIED),
+                List.of(TEST_DAB_SID_EXT_ID)));
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated(
                 AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true,
-                        List.of(TEST_FM_INFO_MODIFIED), List.of(TEST_RDS_PI_ID)));
+                        List.of(TEST_FM_INFO_MODIFIED),
+                        List.of(TEST_DAB_UNIQUE_ID, TEST_DAB_UNIQUE_ID_ALT)));
 
         mTunerSessions[0].startProgramListUpdates(filter);
 
@@ -766,40 +794,44 @@
     @Test
     public void startProgramListUpdates_withNullFilter() throws Exception {
         openAidlClients(/* numClients= */ 1);
+        List<RadioManager.ProgramInfo> modifiedInfo = List.of(TEST_FM_INFO, TEST_DAB_INFO,
+                TEST_DAB_INFO_ALT);
 
         mTunerSessions[0].startProgramListUpdates(/* filter= */ null);
         mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ true,
-                /* complete= */ true, List.of(TEST_FM_INFO, TEST_RDS_INFO), new ArrayList<>()));
+                /* complete= */ true, modifiedInfo, new ArrayList<>()));
 
         verify(mBroadcastRadioMock).startProgramListUpdates(any());
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated(
-                AidlTestUtils.makeChunk(/* purge= */ true, /* complete= */ true,
-                        List.of(TEST_FM_INFO, TEST_RDS_INFO), new ArrayList<>()));
+                AidlTestUtils.makeChunk(/* purge= */ true, /* complete= */ true, modifiedInfo,
+                        new ArrayList<>()));
 
         mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false,
-                /* complete= */ true, List.of(TEST_FM_INFO_MODIFIED), List.of(TEST_RDS_PI_ID)));
+                /* complete= */ true, List.of(TEST_FM_INFO_MODIFIED),
+                List.of(TEST_DAB_SID_EXT_ID)));
 
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated(
                 AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true,
-                        List.of(TEST_FM_INFO_MODIFIED), List.of(TEST_RDS_PI_ID)));
+                        List.of(TEST_FM_INFO_MODIFIED),
+                        List.of(TEST_DAB_UNIQUE_ID, TEST_DAB_UNIQUE_ID_ALT)));
     }
 
     @Test
     public void startProgramListUpdates_withIdFilter() throws Exception {
         openAidlClients(/* numClients= */ 1);
         ProgramList.Filter idFilter = new ProgramList.Filter(new ArraySet<>(),
-                Set.of(TEST_RDS_PI_ID), /* includeCategories= */ true,
+                Set.of(TEST_DAB_SID_EXT_ID), /* includeCategories= */ true,
                 /* excludeModifications= */ true);
-        ProgramFilter halFilter = ConversionUtils.filterToHalProgramFilter(idFilter);
 
         mTunerSessions[0].startProgramListUpdates(idFilter);
         mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false,
-                /* complete= */ true, List.of(TEST_RDS_INFO), new ArrayList<>()));
+                /* complete= */ true, List.of(TEST_DAB_INFO, TEST_DAB_INFO_ALT),
+                new ArrayList<>()));
 
-        verify(mBroadcastRadioMock).startProgramListUpdates(halFilter);
+        verifyHalProgramListUpdatesInvocation(idFilter);
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated(
                 AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true,
-                        List.of(TEST_RDS_INFO), new ArrayList<>()));
+                        List.of(TEST_DAB_INFO, TEST_DAB_INFO_ALT), new ArrayList<>()));
 
         mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false,
                 /* complete= */ true, List.of(TEST_FM_INFO), new ArrayList<>()));
@@ -811,50 +843,52 @@
     public void startProgramListUpdates_withFilterExcludingModifications() throws Exception {
         openAidlClients(/* numClients= */ 1);
         ProgramList.Filter filterExcludingModifications = new ProgramList.Filter(
-                Set.of(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY), new ArraySet<>(),
+                Set.of(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT,
+                        ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY), new ArraySet<>(),
                 /* includeCategories= */ true, /* excludeModifications= */ true);
-        ProgramFilter halFilter =
-                ConversionUtils.filterToHalProgramFilter(filterExcludingModifications);
 
         mTunerSessions[0].startProgramListUpdates(filterExcludingModifications);
         mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false,
-                /* complete= */ true, List.of(TEST_FM_INFO), new ArrayList<>()));
+                /* complete= */ true, List.of(TEST_FM_INFO, TEST_DAB_INFO), new ArrayList<>()));
 
-        verify(mBroadcastRadioMock).startProgramListUpdates(halFilter);
+        verifyHalProgramListUpdatesInvocation(filterExcludingModifications);
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated(
                 AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true,
-                        List.of(TEST_FM_INFO), new ArrayList<>()));
+                        List.of(TEST_FM_INFO, TEST_DAB_INFO), new ArrayList<>()));
 
         mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false,
-                /* complete= */ true, List.of(TEST_FM_INFO_MODIFIED), new ArrayList<>()));
+                /* complete= */ true, List.of(TEST_FM_INFO_MODIFIED, TEST_DAB_INFO_ALT),
+                new ArrayList<>()));
 
-        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated(any());
+        verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated(
+                AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true,
+                        List.of(TEST_DAB_INFO_ALT), new ArrayList<>()));
     }
 
     @Test
     public void startProgramListUpdates_withFilterIncludingModifications() throws Exception {
         openAidlClients(/* numClients= */ 1);
         ProgramList.Filter filterIncludingModifications = new ProgramList.Filter(
-                Set.of(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY), new ArraySet<>(),
+                Set.of(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT,
+                        ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY), new ArraySet<>(),
                 /* includeCategories= */ true, /* excludeModifications= */ false);
-        ProgramFilter halFilter =
-                ConversionUtils.filterToHalProgramFilter(filterIncludingModifications);
 
         mTunerSessions[0].startProgramListUpdates(filterIncludingModifications);
         mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false,
-                /* complete= */ true, List.of(TEST_FM_INFO), new ArrayList<>()));
+                /* complete= */ true, List.of(TEST_FM_INFO, TEST_DAB_INFO), new ArrayList<>()));
 
-        verify(mBroadcastRadioMock).startProgramListUpdates(halFilter);
+        verifyHalProgramListUpdatesInvocation(filterIncludingModifications);
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated(
                 AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true,
-                        List.of(TEST_FM_INFO), new ArrayList<>()));
+                        List.of(TEST_FM_INFO, TEST_DAB_INFO), new ArrayList<>()));
 
         mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false,
-                /* complete= */ true, List.of(TEST_FM_INFO_MODIFIED), new ArrayList<>()));
+                /* complete= */ true, List.of(TEST_FM_INFO_MODIFIED, TEST_DAB_INFO_ALT),
+                new ArrayList<>()));
 
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onProgramListUpdated(
                 AidlTestUtils.makeChunk(/* purge= */ false, /* complete= */ true,
-                        List.of(TEST_FM_INFO_MODIFIED), new ArrayList<>()));
+                        List.of(TEST_FM_INFO_MODIFIED, TEST_DAB_INFO_ALT), new ArrayList<>()));
     }
 
     @Test
@@ -910,7 +944,7 @@
         int numSessions = 3;
         openAidlClients(numSessions);
         List<ProgramList.Filter> filters = List.of(new ProgramList.Filter(
-                        Set.of(ProgramSelector.IDENTIFIER_TYPE_RDS_PI), new ArraySet<>(),
+                        Set.of(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT), new ArraySet<>(),
                         /* includeCategories= */ true, /* excludeModifications= */ false),
                 new ProgramList.Filter(new ArraySet<>(), Set.of(TEST_FM_FREQUENCY_ID),
                         /* includeCategories= */ false, /* excludeModifications= */ true),
@@ -922,18 +956,20 @@
         }
 
         mHalTunerCallback.onProgramListUpdated(AidlTestUtils.makeHalChunk(/* purge= */ false,
-                /* complete= */ true, List.of(TEST_FM_INFO, TEST_RDS_INFO), new ArrayList<>()));
+                /* complete= */ true, List.of(TEST_FM_INFO, TEST_DAB_INFO, TEST_DAB_INFO_ALT),
+                new ArrayList<>()));
 
         verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
                 .onProgramListUpdated(AidlTestUtils.makeChunk(/* purge= */ false,
-                        /* complete= */ true, List.of(TEST_RDS_INFO), new ArrayList<>()));
+                        /* complete= */ true, List.of(TEST_DAB_INFO, TEST_DAB_INFO_ALT),
+                        new ArrayList<>()));
         verify(mAidlTunerCallbackMocks[1], CALLBACK_TIMEOUT)
                 .onProgramListUpdated(AidlTestUtils.makeChunk(/* purge= */ false,
                         /* complete= */ true, List.of(TEST_FM_INFO), new ArrayList<>()));
         verify(mAidlTunerCallbackMocks[2], CALLBACK_TIMEOUT)
                 .onProgramListUpdated(AidlTestUtils.makeChunk(/* purge= */ false,
-                        /* complete= */ true, List.of(TEST_RDS_INFO, TEST_FM_INFO),
-                        new ArrayList<>()));
+                        /* complete= */ true, List.of(TEST_DAB_INFO, TEST_DAB_INFO_ALT,
+                                TEST_FM_INFO), new ArrayList<>()));
     }
 
     @Test
@@ -958,7 +994,7 @@
             mTunerSessions[0].startProgramListUpdates(/* filter= */ null);
         });
 
-        assertWithMessage("Unknown error HAL exception when updating program list")
+        expect.withMessage("Unknown error HAL exception when updating program list")
                 .that(thrown).hasMessageThat().contains("UNKNOWN_ERROR");
     }
 
@@ -995,7 +1031,7 @@
         boolean isSupported = mTunerSessions[0].isConfigFlagSupported(flag);
 
         verify(mBroadcastRadioMock).isConfigFlagSet(flag);
-        assertWithMessage("Config flag %s is supported", flag).that(isSupported).isFalse();
+        expect.withMessage("Config flag %s is supported", flag).that(isSupported).isFalse();
     }
 
     @Test
@@ -1006,7 +1042,7 @@
         boolean isSupported = mTunerSessions[0].isConfigFlagSupported(flag);
 
         verify(mBroadcastRadioMock).isConfigFlagSet(flag);
-        assertWithMessage("Config flag %s is supported", flag).that(isSupported).isTrue();
+        expect.withMessage("Config flag %s is supported", flag).that(isSupported).isTrue();
     }
 
     @Test
@@ -1018,7 +1054,7 @@
             mTunerSessions[0].setConfigFlag(flag, /* value= */ true);
         });
 
-        assertWithMessage("Exception for setting unsupported flag %s", flag)
+        expect.withMessage("Exception for setting unsupported flag %s", flag)
                 .that(thrown).hasMessageThat().contains("setConfigFlag: NOT_SUPPORTED");
     }
 
@@ -1063,7 +1099,7 @@
             mTunerSessions[0].isConfigFlagSet(flag);
         });
 
-        assertWithMessage("Exception for checking if unsupported flag %s is set", flag)
+        expect.withMessage("Exception for checking if unsupported flag %s is set", flag)
                 .that(thrown).hasMessageThat().contains("isConfigFlagSet: NOT_SUPPORTED");
     }
 
@@ -1076,7 +1112,7 @@
 
         boolean isSet = mTunerSessions[0].isConfigFlagSet(flag);
 
-        assertWithMessage("Config flag %s is set", flag)
+        expect.withMessage("Config flag %s is set", flag)
                 .that(isSet).isEqualTo(expectedConfigFlagValue);
     }
 
@@ -1090,7 +1126,7 @@
             mTunerSessions[0].isConfigFlagSet(flag);
         });
 
-        assertWithMessage("Exception for checking config flag when HAL throws remote exception")
+        expect.withMessage("Exception for checking config flag when HAL throws remote exception")
                 .that(thrown).hasMessageThat().contains("Failed to check flag");
     }
 
@@ -1131,7 +1167,7 @@
             mTunerSessions[0].setParameters(parametersSet);
         });
 
-        assertWithMessage("Exception for setting parameters when HAL throws remote exception")
+        expect.withMessage("Exception for setting parameters when HAL throws remote exception")
                 .that(thrown).hasMessageThat().contains(exceptionMessage);
     }
 
@@ -1157,7 +1193,7 @@
             mTunerSessions[0].getParameters(parameterKeys);
         });
 
-        assertWithMessage("Exception for getting parameters when HAL throws remote exception")
+        expect.withMessage("Exception for getting parameters when HAL throws remote exception")
                 .that(thrown).hasMessageThat().contains(exceptionMessage);
     }
 
@@ -1264,4 +1300,24 @@
         }
         return seekFrequency;
     }
+
+    private void verifyHalProgramListUpdatesInvocation(ProgramList.Filter filter) throws Exception {
+        ProgramFilter halFilterExpected = ConversionUtils.filterToHalProgramFilter(filter);
+        ArgumentCaptor<ProgramFilter> halFilterCaptor = ArgumentCaptor.forClass(
+                ProgramFilter.class);
+        verify(mBroadcastRadioMock).startProgramListUpdates(halFilterCaptor.capture());
+        ProgramFilter halFilterInvoked = halFilterCaptor.getValue();
+        expect.withMessage("Filtered identifier types").that(
+                halFilterInvoked.identifierTypes).asList().containsExactlyElementsIn(Arrays.stream(
+                        halFilterExpected.identifierTypes).boxed().toArray(Integer[]::new));
+        expect.withMessage("Filtered identifiers").that(
+                halFilterInvoked.identifiers).asList()
+                .containsExactlyElementsIn(halFilterExpected.identifiers);
+        expect.withMessage("Categories-included filter")
+                .that(halFilterInvoked.includeCategories)
+                .isEqualTo(halFilterExpected.includeCategories);
+        expect.withMessage("Modifications-excluded filter")
+                .that(halFilterInvoked.excludeModifications)
+                .isEqualTo(halFilterExpected.excludeModifications);
+    }
 }
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ProgramInfoCacheTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ProgramInfoCacheTest.java
index ec55ddb..fbb446b 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ProgramInfoCacheTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ProgramInfoCacheTest.java
@@ -21,12 +21,16 @@
 import android.hardware.radio.ProgramList;
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
+import android.hardware.radio.UniqueProgramIdentifier;
 import android.test.suitebuilder.annotation.MediumTest;
 
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.broadcastradio.ExtendedRadioMockitoTestCase;
 
+import com.google.common.truth.Expect;
+
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -40,188 +44,221 @@
 @RunWith(AndroidJUnit4.class)
 @MediumTest
 public class ProgramInfoCacheTest extends ExtendedRadioMockitoTestCase {
-    private static final String TAG = "BroadcastRadioTests.ProgramInfoCache";
+    private static final int TEST_QUALITY = 1;
 
-    private final ProgramSelector.Identifier mAmFmIdentifier =
-            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, 88500);
-    private final RadioManager.ProgramInfo mAmFmInfo = TestUtils.makeProgramInfo(
-            ProgramSelector.PROGRAM_TYPE_FM, mAmFmIdentifier, 0);
+    private static final ProgramSelector.Identifier TEST_AM_FM_ID = new ProgramSelector.Identifier(
+            ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, /* value= */ 88500);
+    private static final ProgramSelector TEST_AM_FM_SELECTOR = TestUtils.makeProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_FM, TEST_AM_FM_ID);
+    private static final RadioManager.ProgramInfo TEST_AM_FM_INFO = TestUtils.makeProgramInfo(
+            TEST_AM_FM_SELECTOR, TEST_QUALITY);
 
-    private final ProgramSelector.Identifier mRdsIdentifier =
-            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_RDS_PI, 15019);
-    private final RadioManager.ProgramInfo mRdsInfo = TestUtils.makeProgramInfo(
-            ProgramSelector.PROGRAM_TYPE_FM, mRdsIdentifier, 0);
+    private static final ProgramSelector.Identifier TEST_RDS_ID = new ProgramSelector.Identifier(
+            ProgramSelector.IDENTIFIER_TYPE_RDS_PI, /* value= */ 15019);
+    private static final ProgramSelector TEST_RDS_SELECTOR = TestUtils.makeProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_FM, TEST_RDS_ID);
+    private static final RadioManager.ProgramInfo TEST_RDS_INFO = TestUtils.makeProgramInfo(
+            TEST_RDS_SELECTOR, TEST_QUALITY);
 
-    private final ProgramSelector.Identifier mDabEnsembleIdentifier =
-            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, 1337);
-    private final RadioManager.ProgramInfo mDabEnsembleInfo = TestUtils.makeProgramInfo(
-            ProgramSelector.PROGRAM_TYPE_DAB, mDabEnsembleIdentifier, 0);
+    private static final ProgramSelector TEST_HD_SELECTOR = TestUtils.makeProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_FM_HD, new ProgramSelector.Identifier(
+                    ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT,
+                    /* value= */ 0x17C14100000001L));
 
-    private final ProgramSelector.Identifier mVendorCustomIdentifier =
-            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_VENDOR_START, 9001);
-    private final RadioManager.ProgramInfo mVendorCustomInfo = TestUtils.makeProgramInfo(
-            ProgramSelector.PROGRAM_TYPE_VENDOR_START, mVendorCustomIdentifier, 0);
+    private static final ProgramSelector.Identifier TEST_DAB_SID_ID =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
+                    /* value= */ 0xA000000111L);
+    private static final ProgramSelector.Identifier TEST_DAB_ENSEMBLE_ID =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
+                    /* value= */ 0x1001);
+    private static final ProgramSelector.Identifier TEST_DAB_FREQUENCY_ID =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
+                    /* value= */ 220_352);
+    private static final ProgramSelector TEST_DAB_SELECTOR = TestUtils.makeProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_DAB, TEST_DAB_SID_ID,
+            new ProgramSelector.Identifier[]{TEST_DAB_FREQUENCY_ID, TEST_DAB_ENSEMBLE_ID});
+    private static final UniqueProgramIdentifier TEST_DAB_UNIQUE_ID =
+            new UniqueProgramIdentifier(TEST_DAB_SELECTOR);
+    private static final RadioManager.ProgramInfo TEST_DAB_INFO = TestUtils.makeProgramInfo(
+            TEST_DAB_SELECTOR, TEST_QUALITY);
+    private static final ProgramSelector.Identifier TEST_VENDOR_ID = new ProgramSelector.Identifier(
+            ProgramSelector.IDENTIFIER_TYPE_VENDOR_START, /* value= */ 9001);
+    private static final ProgramSelector TEST_VENDOR_SELECTOR = TestUtils.makeProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_VENDOR_START, TEST_VENDOR_ID);
+    private static final UniqueProgramIdentifier TEST_VENDOR_UNIQUE_ID =
+            new UniqueProgramIdentifier(TEST_VENDOR_SELECTOR);
+    private static final RadioManager.ProgramInfo TEST_VENDOR_INFO = TestUtils.makeProgramInfo(
+            TEST_VENDOR_SELECTOR, TEST_QUALITY);
 
-    // HAL-side ProgramInfoCache containing all of the above ProgramInfos.
-    private final ProgramInfoCache mAllProgramInfos = new ProgramInfoCache(null, true, mAmFmInfo,
-            mRdsInfo, mDabEnsembleInfo, mVendorCustomInfo);
+    private static final ProgramInfoCache FULL_PROGRAM_INFO_CACHE = new ProgramInfoCache(
+            /* filter= */ null, /* complete= */ true, TEST_AM_FM_INFO, TEST_RDS_INFO, TEST_DAB_INFO,
+            TEST_VENDOR_INFO);
+
+    @Rule
+    public final Expect expect = Expect.create();
 
     @Test
     public void testUpdateFromHal() {
         // First test updating an incomplete cache with a purging, complete chunk.
-        ProgramInfoCache cache = new ProgramInfoCache(null, false, mAmFmInfo);
+        ProgramInfoCache cache = new ProgramInfoCache(null, false, TEST_AM_FM_INFO);
         ProgramListChunk chunk = new ProgramListChunk();
         chunk.purge = true;
         chunk.complete = true;
-        chunk.modified.add(TestUtils.programInfoToHal(mRdsInfo));
-        chunk.modified.add(TestUtils.programInfoToHal(mDabEnsembleInfo));
+        chunk.modified.add(TestUtils.programInfoToHal(TEST_RDS_INFO));
+        chunk.modified.add(TestUtils.programInfoToHal(TEST_DAB_INFO));
         cache.updateFromHalProgramListChunk(chunk);
-        assertTrue(cache.programInfosAreExactly(mRdsInfo, mDabEnsembleInfo));
+        expect.withMessage("Program info cache updated with a purging complete chunk")
+                .that(cache.toProgramInfoList()).containsExactly(TEST_RDS_INFO, TEST_DAB_INFO);
         assertTrue(cache.isComplete());
 
         // Then test a non-purging, incomplete chunk.
         chunk.purge = false;
         chunk.complete = false;
         chunk.modified.clear();
-        RadioManager.ProgramInfo updatedRdsInfo = TestUtils.makeProgramInfo(
-                ProgramSelector.PROGRAM_TYPE_FM, mRdsIdentifier, 1);
+        RadioManager.ProgramInfo updatedRdsInfo = TestUtils.makeProgramInfo(TEST_RDS_SELECTOR, 1);
         chunk.modified.add(TestUtils.programInfoToHal(updatedRdsInfo));
-        chunk.modified.add(TestUtils.programInfoToHal(mVendorCustomInfo));
-        chunk.removed.add(Convert.programIdentifierToHal(mDabEnsembleIdentifier));
+        chunk.modified.add(TestUtils.programInfoToHal(TEST_VENDOR_INFO));
+        chunk.removed.add(Convert.programIdentifierToHal(TEST_DAB_SID_ID));
         cache.updateFromHalProgramListChunk(chunk);
-        assertTrue(cache.programInfosAreExactly(updatedRdsInfo, mVendorCustomInfo));
+        expect.withMessage("Program info cache updated with non-puring incomplete chunk")
+                .that(cache.toProgramInfoList()).containsExactly(updatedRdsInfo, TEST_VENDOR_INFO);
         assertFalse(cache.isComplete());
     }
 
     @Test
     public void testNullFilter() {
         ProgramInfoCache cache = new ProgramInfoCache(null, true);
-        cache.filterAndUpdateFrom(mAllProgramInfos, false);
-        assertTrue(cache.programInfosAreExactly(mAmFmInfo, mRdsInfo, mDabEnsembleInfo,
-                  mVendorCustomInfo));
+        cache.filterAndUpdateFrom(FULL_PROGRAM_INFO_CACHE, false);
+        expect.withMessage("Program info cache with null filter")
+                .that(cache.toProgramInfoList()).containsExactly(TEST_AM_FM_INFO, TEST_RDS_INFO,
+                        TEST_DAB_INFO, TEST_VENDOR_INFO);
     }
 
     @Test
     public void testEmptyFilter() {
         ProgramInfoCache cache = new ProgramInfoCache(new ProgramList.Filter(new HashSet<Integer>(),
                   new HashSet<ProgramSelector.Identifier>(), true, false));
-        cache.filterAndUpdateFrom(mAllProgramInfos, false);
-        assertTrue(cache.programInfosAreExactly(mAmFmInfo, mRdsInfo, mDabEnsembleInfo,
-                  mVendorCustomInfo));
+        cache.filterAndUpdateFrom(FULL_PROGRAM_INFO_CACHE, false);
+        expect.withMessage("Program info cache with empty filter")
+                .that(cache.toProgramInfoList()).containsExactly(TEST_AM_FM_INFO, TEST_RDS_INFO,
+                        TEST_DAB_INFO, TEST_VENDOR_INFO);
     }
 
     @Test
     public void testFilterByType() {
         HashSet<Integer> filterTypes = new HashSet<>();
         filterTypes.add(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY);
-        filterTypes.add(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE);
+        filterTypes.add(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT);
         ProgramInfoCache cache = new ProgramInfoCache(new ProgramList.Filter(filterTypes,
                   new HashSet<ProgramSelector.Identifier>(), true, false));
-        cache.filterAndUpdateFrom(mAllProgramInfos, false);
-        assertTrue(cache.programInfosAreExactly(mAmFmInfo, mDabEnsembleInfo));
+        cache.filterAndUpdateFrom(FULL_PROGRAM_INFO_CACHE, false);
+        expect.withMessage("Program info cache with type filter")
+                .that(cache.toProgramInfoList()).containsExactly(TEST_AM_FM_INFO, TEST_DAB_INFO);
     }
 
     @Test
     public void testFilterByIdentifier() {
         HashSet<ProgramSelector.Identifier> filterIds = new HashSet<>();
-        filterIds.add(mRdsIdentifier);
-        filterIds.add(mVendorCustomIdentifier);
+        filterIds.add(TEST_RDS_ID);
+        filterIds.add(TEST_VENDOR_ID);
         ProgramInfoCache cache = new ProgramInfoCache(new ProgramList.Filter(new HashSet<Integer>(),
                   filterIds, true, false));
-        cache.filterAndUpdateFrom(mAllProgramInfos, false);
-        assertTrue(cache.programInfosAreExactly(mRdsInfo, mVendorCustomInfo));
+        cache.filterAndUpdateFrom(FULL_PROGRAM_INFO_CACHE, false);
+        expect.withMessage("Program info cache with identifier filter")
+                .that(cache.toProgramInfoList()).containsExactly(TEST_RDS_INFO, TEST_VENDOR_INFO);
     }
 
     @Test
     public void testFilterExcludeCategories() {
         ProgramInfoCache cache = new ProgramInfoCache(new ProgramList.Filter(new HashSet<Integer>(),
                   new HashSet<ProgramSelector.Identifier>(), false, false));
-        cache.filterAndUpdateFrom(mAllProgramInfos, false);
-        assertTrue(cache.programInfosAreExactly(mAmFmInfo, mRdsInfo));
+        cache.filterAndUpdateFrom(FULL_PROGRAM_INFO_CACHE, false);
+        expect.withMessage("Program info cache with filter excluding categories")
+                .that(cache.toProgramInfoList()).containsExactly(TEST_AM_FM_INFO, TEST_RDS_INFO,
+                        TEST_DAB_INFO);
     }
 
     @Test
     public void testPurgeUpdateChunks() {
-        ProgramInfoCache cache = new ProgramInfoCache(null, false, mAmFmInfo);
+        ProgramInfoCache cache = new ProgramInfoCache(null, false, TEST_AM_FM_INFO);
         List<ProgramList.Chunk> chunks =
-                cache.filterAndUpdateFromInternal(mAllProgramInfos, true, 3, 3);
+                cache.filterAndUpdateFromInternal(FULL_PROGRAM_INFO_CACHE, true, 3, 3);
         assertEquals(2, chunks.size());
         verifyChunkListFlags(chunks, true, true);
-        verifyChunkListModified(chunks, 3, mAmFmInfo, mRdsInfo, mDabEnsembleInfo,
-                mVendorCustomInfo);
+        verifyChunkListModified(chunks, 3, TEST_AM_FM_INFO, TEST_RDS_INFO, TEST_DAB_INFO,
+                TEST_VENDOR_INFO);
         verifyChunkListRemoved(chunks, 0);
     }
 
     @Test
     public void testDeltaUpdateChunksModificationsIncluded() {
         // Create a cache with a filter that allows modifications, and set its contents to
-        // mAmFmInfo, mRdsInfo, mDabEnsembleInfo, and mVendorCustomInfo.
-        ProgramInfoCache cache = new ProgramInfoCache(null, true, mAmFmInfo, mRdsInfo,
-                mDabEnsembleInfo, mVendorCustomInfo);
+        // TEST_AM_FM_INFO, TEST_RDS_INFO, TEST_DAB_INFO, and TEST_VENDOR_INFO.
+        ProgramInfoCache cache = new ProgramInfoCache(null, true, TEST_AM_FM_INFO, TEST_RDS_INFO,
+                TEST_DAB_INFO, TEST_VENDOR_INFO);
 
         // Create a HAL cache that:
         // - Is complete.
-        // - Retains mAmFmInfo.
-        // - Replaces mRdsInfo with updatedRdsInfo.
-        // - Drops mDabEnsembleInfo and mVendorCustomInfo.
-        // - Introduces a new SXM info.
-        RadioManager.ProgramInfo updatedRdsInfo = TestUtils.makeProgramInfo(
-                ProgramSelector.PROGRAM_TYPE_FM, mRdsIdentifier, 1);
-        RadioManager.ProgramInfo newSxmInfo = TestUtils.makeProgramInfo(
-                ProgramSelector.PROGRAM_TYPE_SXM,
-                new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL, 12345),
-                0);
-        ProgramInfoCache halCache = new ProgramInfoCache(null, true, mAmFmInfo, updatedRdsInfo,
-                newSxmInfo);
+        // - Retains TEST_AM_FM_INFO.
+        // - Replaces TEST_RDS_INFO with updatedRdsInfo.
+        // - Drops TEST_DAB_INFO and TEST_VENDOR_INFO.
+        // - Introduces a new HD info.
+        RadioManager.ProgramInfo updatedRdsInfo = TestUtils.makeProgramInfo(TEST_RDS_SELECTOR,
+                TEST_QUALITY + 1);
+        RadioManager.ProgramInfo newHdInfo = TestUtils.makeProgramInfo(TEST_HD_SELECTOR,
+                TEST_QUALITY);
+        ProgramInfoCache halCache = new ProgramInfoCache(null, true, TEST_AM_FM_INFO,
+                updatedRdsInfo, newHdInfo);
 
         // Update the cache and verify:
         // - The final chunk's complete flag is set.
-        // - mAmFmInfo is retained and not reported in the chunks.
-        // - updatedRdsInfo should appear as an update to mRdsInfo.
-        // - newSxmInfo should appear as a new entry.
-        // - mDabEnsembleInfo and mVendorCustomInfo should be reported as removed.
+        // - TEST_AM_FM_INFO is retained and not reported in the chunks.
+        // - updatedRdsInfo should appear as an update to TEST_RDS_INFO.
+        // - newHdInfo should appear as a new entry.
+        // - TEST_DAB_INFO and TEST_VENDOR_INFO should be reported as removed.
         List<ProgramList.Chunk> chunks = cache.filterAndUpdateFromInternal(halCache, false, 5, 1);
-        assertTrue(cache.programInfosAreExactly(mAmFmInfo, updatedRdsInfo, newSxmInfo));
+        expect.withMessage("Program info cache with modification included")
+                .that(cache.toProgramInfoList()).containsExactly(TEST_AM_FM_INFO, updatedRdsInfo,
+                        newHdInfo);
         assertEquals(2, chunks.size());
         verifyChunkListFlags(chunks, false, true);
-        verifyChunkListModified(chunks, 5, updatedRdsInfo, newSxmInfo);
-        verifyChunkListRemoved(chunks, 1, mDabEnsembleIdentifier, mVendorCustomIdentifier);
+        verifyChunkListModified(chunks, 5, updatedRdsInfo, newHdInfo);
+        verifyChunkListRemoved(chunks, 1, TEST_DAB_UNIQUE_ID, TEST_VENDOR_UNIQUE_ID);
     }
 
     @Test
     public void testDeltaUpdateChunksModificationsExcluded() {
         // Create a cache with a filter that excludes modifications, and set its contents to
-        // mAmFmInfo, mRdsInfo, mDabEnsembleInfo, and mVendorCustomInfo.
+        // TEST_AM_FM_INFO, TEST_RDS_INFO, TEST_DAB_INFO, and TEST_VENDOR_INFO.
         ProgramInfoCache cache = new ProgramInfoCache(new ProgramList.Filter(new HashSet<Integer>(),
-                new HashSet<ProgramSelector.Identifier>(), true, true), true, mAmFmInfo, mRdsInfo,
-                mDabEnsembleInfo, mVendorCustomInfo);
+                new HashSet<ProgramSelector.Identifier>(), true, true), true,
+                TEST_AM_FM_INFO, TEST_RDS_INFO, TEST_DAB_INFO, TEST_VENDOR_INFO);
 
         // Create a HAL cache that:
         // - Is incomplete.
-        // - Retains mAmFmInfo.
-        // - Replaces mRdsInfo with updatedRdsInfo.
-        // - Drops mDabEnsembleInfo and mVendorCustomInfo.
-        // - Introduces a new SXM info.
-        RadioManager.ProgramInfo updatedRdsInfo = TestUtils.makeProgramInfo(
-                ProgramSelector.PROGRAM_TYPE_FM, mRdsIdentifier, 1);
-        RadioManager.ProgramInfo newSxmInfo = TestUtils.makeProgramInfo(
-                ProgramSelector.PROGRAM_TYPE_SXM,
-                new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL, 12345),
-                0);
-        ProgramInfoCache halCache = new ProgramInfoCache(null, false, mAmFmInfo, updatedRdsInfo,
-                newSxmInfo);
+        // - Retains TEST_AM_FM_INFO.
+        // - Replaces TEST_RDS_INFO with updatedRdsInfo.
+        // - Drops TEST_DAB_INFO and TEST_VENDOR_INFO.
+        // - Introduces a new HD info.
+        RadioManager.ProgramInfo updatedRdsInfo = TestUtils.makeProgramInfo(TEST_RDS_SELECTOR, 1);
+        RadioManager.ProgramInfo newHdInfo = TestUtils.makeProgramInfo(TEST_HD_SELECTOR,
+                TEST_QUALITY);
+        ProgramInfoCache halCache = new ProgramInfoCache(null, false, TEST_AM_FM_INFO,
+                updatedRdsInfo, newHdInfo);
 
         // Update the cache and verify:
         // - All complete flags are false.
-        // - mAmFmInfo and mRdsInfo are retained and not reported in the chunks.
-        // - newSxmInfo should appear as a new entry.
-        // - mDabEnsembleInfo and mVendorCustomInfo should be reported as removed.
+        // - TEST_AM_FM_INFO and TEST_RDS_INFO are retained and not reported in the chunks.
+        // - newHdInfo should appear as a new entry.
+        // - TEST_DAB_INFO and TEST_VENDOR_INFO should be reported as removed.
         List<ProgramList.Chunk> chunks = cache.filterAndUpdateFromInternal(halCache, false, 5, 1);
-        assertTrue(cache.programInfosAreExactly(mAmFmInfo, mRdsInfo, newSxmInfo));
+        expect.withMessage("Program info cache with modification excluded")
+                .that(cache.toProgramInfoList()).containsExactly(TEST_AM_FM_INFO, TEST_RDS_INFO,
+                        newHdInfo);
         assertEquals(2, chunks.size());
         verifyChunkListFlags(chunks, false, false);
-        verifyChunkListModified(chunks, 5, newSxmInfo);
-        verifyChunkListRemoved(chunks, 1, mDabEnsembleIdentifier, mVendorCustomIdentifier);
+        verifyChunkListModified(chunks, 5, newHdInfo);
+        verifyChunkListRemoved(chunks, 1, TEST_DAB_UNIQUE_ID, TEST_VENDOR_UNIQUE_ID);
     }
 
     // Verifies that:
@@ -271,20 +308,21 @@
     // - Each chunk's removed array has a similar number of elements.
     // - Each element of expectedIdentifiers appears in a chunk.
     private static void verifyChunkListRemoved(List<ProgramList.Chunk> chunks,
-            int maxRemovedPerChunk, ProgramSelector.Identifier... expectedIdentifiers) {
+            int maxRemovedPerChunk,
+            UniqueProgramIdentifier... expectedIdentifiers) {
         if (chunks.isEmpty()) {
             assertEquals(0, expectedIdentifiers.length);
             return;
         }
-        HashSet<ProgramSelector.Identifier> expectedSet = new HashSet<>();
-        for (ProgramSelector.Identifier identifier : expectedIdentifiers) {
+        HashSet<UniqueProgramIdentifier> expectedSet = new HashSet<>();
+        for (UniqueProgramIdentifier identifier : expectedIdentifiers) {
             expectedSet.add(identifier);
         }
 
-        HashSet<ProgramSelector.Identifier> actualSet = new HashSet<>();
+        HashSet<UniqueProgramIdentifier> actualSet = new HashSet<>();
         int chunk0NumRemoved = chunks.get(0).getRemoved().size();
         for (ProgramList.Chunk chunk : chunks) {
-            Set<ProgramSelector.Identifier> chunkRemoved = chunk.getRemoved();
+            Set<UniqueProgramIdentifier> chunkRemoved = chunk.getRemoved();
             assertTrue(chunkRemoved.size() <= maxRemovedPerChunk);
             assertTrue(Math.abs(chunkRemoved.size() - chunk0NumRemoved) <= 1);
             actualSet.addAll(chunkRemoved);
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java
index 7d604d4..8c16d79 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/StartProgramListUpdatesFanoutTest.java
@@ -35,6 +35,7 @@
 import android.hardware.radio.ProgramList;
 import android.hardware.radio.ProgramSelector;
 import android.hardware.radio.RadioManager;
+import android.hardware.radio.UniqueProgramIdentifier;
 import android.os.RemoteException;
 
 import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
@@ -72,22 +73,42 @@
     private TunerSession[] mTunerSessions;
 
     // Data objects used during tests
-    private final ProgramSelector.Identifier mAmFmIdentifier =
-            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, 88500);
-    private final RadioManager.ProgramInfo mAmFmInfo = TestUtils.makeProgramInfo(
-            ProgramSelector.PROGRAM_TYPE_FM, mAmFmIdentifier, 0);
-    private final RadioManager.ProgramInfo mModifiedAmFmInfo = TestUtils.makeProgramInfo(
-            ProgramSelector.PROGRAM_TYPE_FM, mAmFmIdentifier, 1);
+    private static final int TEST_QUALITY = 0;
+    private static final ProgramSelector.Identifier TEST_AM_FM_ID =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY,
+                    /* value= */ 88_500);
+    private static final ProgramSelector TEST_AM_FM_SELECTOR = TestUtils.makeProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_FM, TEST_AM_FM_ID);
+    private static final RadioManager.ProgramInfo TEST_AM_FM_INFO = TestUtils.makeProgramInfo(
+            TEST_AM_FM_SELECTOR, TEST_QUALITY);
+    private static final RadioManager.ProgramInfo TEST_AM_FM_MODIFIED_INFO =
+            TestUtils.makeProgramInfo(TEST_AM_FM_SELECTOR, TEST_QUALITY + 1);
 
-    private final ProgramSelector.Identifier mRdsIdentifier =
-            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_RDS_PI, 15019);
-    private final RadioManager.ProgramInfo mRdsInfo = TestUtils.makeProgramInfo(
-            ProgramSelector.PROGRAM_TYPE_FM, mRdsIdentifier, 0);
+    private static final ProgramSelector.Identifier TEST_RDS_ID =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_RDS_PI,
+                    /* value= */ 15_019);
+    private static final ProgramSelector TEST_RDS_SELECTOR = TestUtils.makeProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_FM, TEST_RDS_ID);
 
-    private final ProgramSelector.Identifier mDabEnsembleIdentifier =
-            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, 1337);
-    private final RadioManager.ProgramInfo mDabEnsembleInfo = TestUtils.makeProgramInfo(
-            ProgramSelector.PROGRAM_TYPE_DAB, mDabEnsembleIdentifier, 0);
+    private static final UniqueProgramIdentifier TEST_RDS_UNIQUE_ID = new UniqueProgramIdentifier(
+            TEST_RDS_ID);
+    private static final RadioManager.ProgramInfo TEST_RDS_INFO = TestUtils.makeProgramInfo(
+            TEST_RDS_SELECTOR, TEST_QUALITY);
+
+    private static final ProgramSelector.Identifier TEST_DAB_SID_ID =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT,
+                    /* value= */ 0xA000000111L);
+    private static final ProgramSelector.Identifier TEST_DAB_ENSEMBLE_ID =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE,
+                    /* value= */ 0x1001);
+    private static final ProgramSelector.Identifier TEST_DAB_FREQUENCY_ID =
+            new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY,
+                    /* value= */ 220_352);
+    private static final ProgramSelector TEST_DAB_SELECTOR = TestUtils.makeProgramSelector(
+            ProgramSelector.PROGRAM_TYPE_DAB, TEST_DAB_SID_ID,
+            new ProgramSelector.Identifier[]{TEST_DAB_FREQUENCY_ID, TEST_DAB_ENSEMBLE_ID});
+    private static final RadioManager.ProgramInfo TEST_DAB_INFO = TestUtils.makeProgramInfo(
+            TEST_DAB_SELECTOR, TEST_QUALITY);
 
     @Override
     protected void initializeSession(StaticMockitoSessionBuilder builder) {
@@ -126,18 +147,18 @@
 
         // Initiate a program list update from the HAL side and verify both connected AIDL clients
         // receive the update.
-        updateHalProgramInfo(true, Arrays.asList(mAmFmInfo, mRdsInfo), null);
+        updateHalProgramInfo(true, Arrays.asList(TEST_AM_FM_INFO, TEST_RDS_INFO), null);
         for (int i = 0; i < 2; i++) {
             verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[i], true, Arrays.asList(
-                    mAmFmInfo, mRdsInfo), null);
+                    TEST_AM_FM_INFO, TEST_RDS_INFO), null);
         }
 
         // Repeat with a non-purging update.
-        updateHalProgramInfo(false, Arrays.asList(mModifiedAmFmInfo),
-                Arrays.asList(mRdsIdentifier));
+        updateHalProgramInfo(false, Arrays.asList(TEST_AM_FM_MODIFIED_INFO),
+                Arrays.asList(TEST_RDS_ID));
         for (int i = 0; i < 2; i++) {
             verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[i], false,
-                    Arrays.asList(mModifiedAmFmInfo), Arrays.asList(mRdsIdentifier));
+                    Arrays.asList(TEST_AM_FM_MODIFIED_INFO), Arrays.asList(TEST_RDS_UNIQUE_ID));
         }
 
         // Now start updates on the 3rd client. Verify the HAL function has not been called again
@@ -145,19 +166,19 @@
         mTunerSessions[2].startProgramListUpdates(aidlFilter);
         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(any());
         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[2], true,
-                Arrays.asList(mModifiedAmFmInfo), null);
+                Arrays.asList(TEST_AM_FM_MODIFIED_INFO), null);
     }
 
     @Test
     public void testFiltering() throws RemoteException {
         // Open 4 clients that will use the following filters:
-        // [0]: ID mRdsIdentifier, modifications excluded
+        // [0]: ID TEST_RDS_ID, modifications excluded
         // [1]: No categories, modifications excluded
         // [2]: Type IDENTIFIER_TYPE_AMFM_FREQUENCY, modifications excluded
         // [3]: Type IDENTIFIER_TYPE_AMFM_FREQUENCY, modifications included
         openAidlClients(4);
         ProgramList.Filter idFilter = new ProgramList.Filter(new HashSet<Integer>(),
-                new HashSet<ProgramSelector.Identifier>(Arrays.asList(mRdsIdentifier)), true, true);
+                new HashSet<ProgramSelector.Identifier>(Arrays.asList(TEST_RDS_ID)), true, true);
         ProgramList.Filter categoryFilter = new ProgramList.Filter(new HashSet<Integer>(),
                 new HashSet<ProgramSelector.Identifier>(), false, true);
         ProgramList.Filter typeFilterWithoutModifications = new ProgramList.Filter(
@@ -188,41 +209,40 @@
         halFilter.excludeModifications = false;
         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
 
-        // Adding mRdsInfo should update clients [0] and [1].
-        updateHalProgramInfo(false, Arrays.asList(mRdsInfo), null);
-        verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], false, Arrays.asList(mRdsInfo),
-                null);
-        verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], false, Arrays.asList(mRdsInfo),
-                null);
+        // Adding TEST_RDS_INFO should update clients [0] and [1].
+        updateHalProgramInfo(false, Arrays.asList(TEST_RDS_INFO), null);
+        verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], false,
+                Arrays.asList(TEST_RDS_INFO), null);
+        verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], false,
+                Arrays.asList(TEST_RDS_INFO), null);
 
-        // Adding mAmFmInfo should update clients [1], [2], and [3].
-        updateHalProgramInfo(false, Arrays.asList(mAmFmInfo), null);
-        verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], false, Arrays.asList(mAmFmInfo),
-                null);
-        verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[2], false, Arrays.asList(mAmFmInfo),
-                null);
-        verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[3], false, Arrays.asList(mAmFmInfo),
-                null);
-
-        // Modifying mAmFmInfo to mModifiedAmFmInfo should update only [3].
-        updateHalProgramInfo(false, Arrays.asList(mModifiedAmFmInfo), null);
+        // Adding TEST_AM_FM_INFO should update clients [1], [2], and [3].
+        updateHalProgramInfo(false, Arrays.asList(TEST_AM_FM_INFO), null);
+        verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], false,
+                Arrays.asList(TEST_AM_FM_INFO), null);
+        verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[2], false,
+                Arrays.asList(TEST_AM_FM_INFO), null);
         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[3], false,
-                Arrays.asList(mModifiedAmFmInfo), null);
+                Arrays.asList(TEST_AM_FM_INFO), null);
 
-        // Adding mDabEnsembleInfo should not update any client.
-        updateHalProgramInfo(false, Arrays.asList(mDabEnsembleInfo), null);
+        // Modifying TEST_AM_FM_INFO to TEST_AM_FM_MODIFIED_INFO should update only [3].
+        updateHalProgramInfo(false, Arrays.asList(TEST_AM_FM_MODIFIED_INFO), null);
+        verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[3], false,
+                Arrays.asList(TEST_AM_FM_MODIFIED_INFO), null);
+
+        updateHalProgramInfo(false, Arrays.asList(TEST_DAB_INFO), null);
         verify(mAidlTunerCallbackMocks[0], CB_TIMEOUT.times(1)).onProgramListUpdated(any());
-        verify(mAidlTunerCallbackMocks[1], CB_TIMEOUT.times(2)).onProgramListUpdated(any());
+        verify(mAidlTunerCallbackMocks[1], CB_TIMEOUT.times(3)).onProgramListUpdated(any());
         verify(mAidlTunerCallbackMocks[2], CB_TIMEOUT.times(2)).onProgramListUpdated(any());
         verify(mAidlTunerCallbackMocks[3], CB_TIMEOUT.times(2)).onProgramListUpdated(any());
     }
 
     @Test
     public void testClientClosing() throws RemoteException {
-        // Open 2 clients that use different filters that are both sensitive to mAmFmIdentifier.
+        // Open 2 clients that use different filters that are both sensitive to TEST_AM_FM_ID.
         openAidlClients(2);
         ProgramList.Filter idFilter = new ProgramList.Filter(new HashSet<Integer>(),
-                new HashSet<ProgramSelector.Identifier>(Arrays.asList(mAmFmIdentifier)), true,
+                new HashSet<ProgramSelector.Identifier>(Arrays.asList(TEST_AM_FM_ID)), true,
                 false);
         ProgramList.Filter typeFilter = new ProgramList.Filter(
                 new HashSet<Integer>(Arrays.asList(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY)),
@@ -237,23 +257,24 @@
         halFilter.identifiers.clear();
         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(halFilter);
 
-        // Update the HAL with mAmFmInfo, and verify both clients are updated.
-        updateHalProgramInfo(true, Arrays.asList(mAmFmInfo), null);
-        verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], true, Arrays.asList(mAmFmInfo),
-                null);
-        verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], true, Arrays.asList(mAmFmInfo),
-                null);
+        // Update the HAL with TEST_AM_FM_INFO, and verify both clients are updated.
+        updateHalProgramInfo(true, Arrays.asList(TEST_AM_FM_INFO), null);
+        verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], true,
+                Arrays.asList(TEST_AM_FM_INFO), null);
+        verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], true,
+                Arrays.asList(TEST_AM_FM_INFO), null);
 
         // Stop updates on the first client and verify the HAL filter is updated.
         mTunerSessions[0].stopProgramListUpdates();
         verify(mHalTunerSessionMock, times(1)).startProgramListUpdates(Convert.programFilterToHal(
                 typeFilter));
 
-        // Update the HAL with mModifiedAmFmInfo, and verify only the remaining client is updated.
-        updateHalProgramInfo(true, Arrays.asList(mModifiedAmFmInfo), null);
+        // Update the HAL with TEST_AM_FM_MODIFIED_INFO, and verify only the remaining client is
+        // updated.
+        updateHalProgramInfo(true, Arrays.asList(TEST_AM_FM_MODIFIED_INFO), null);
         verify(mAidlTunerCallbackMocks[0], CB_TIMEOUT.times(1)).onProgramListUpdated(any());
         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[1], true,
-                Arrays.asList(mModifiedAmFmInfo), null);
+                Arrays.asList(TEST_AM_FM_MODIFIED_INFO), null);
 
         // Close the other client without explicitly stopping updates, and verify HAL updates are
         // stopped as well.
@@ -269,15 +290,15 @@
 
         // Verify the AIDL client receives all types of updates (e.g. a new program, an update to
         // that program, and a category).
-        updateHalProgramInfo(true, Arrays.asList(mAmFmInfo, mRdsInfo), null);
+        updateHalProgramInfo(true, Arrays.asList(TEST_AM_FM_INFO, TEST_RDS_INFO), null);
         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], true, Arrays.asList(
-                mAmFmInfo, mRdsInfo), null);
-        updateHalProgramInfo(false, Arrays.asList(mModifiedAmFmInfo), null);
+                TEST_AM_FM_INFO, TEST_RDS_INFO), null);
+        updateHalProgramInfo(false, Arrays.asList(TEST_AM_FM_MODIFIED_INFO), null);
         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], false,
-                Arrays.asList(mModifiedAmFmInfo), null);
-        updateHalProgramInfo(false, Arrays.asList(mDabEnsembleInfo), null);
+                Arrays.asList(TEST_AM_FM_MODIFIED_INFO), null);
+        updateHalProgramInfo(false, Arrays.asList(TEST_DAB_INFO), null);
         verifyAidlClientReceivedChunk(mAidlTunerCallbackMocks[0], false,
-                Arrays.asList(mDabEnsembleInfo), null);
+                Arrays.asList(TEST_DAB_INFO), null);
 
         // Verify closing the AIDL session also stops HAL updates.
         mTunerSessions[0].close();
@@ -313,12 +334,12 @@
 
     private void verifyAidlClientReceivedChunk(android.hardware.radio.ITunerCallback clientMock,
             boolean purge, List<RadioManager.ProgramInfo> modified,
-            List<ProgramSelector.Identifier> removed) throws RemoteException {
+            List<UniqueProgramIdentifier> removed) throws RemoteException {
         HashSet<RadioManager.ProgramInfo> modifiedSet = new HashSet<>();
         if (modified != null) {
             modifiedSet.addAll(modified);
         }
-        HashSet<ProgramSelector.Identifier> removedSet = new HashSet<>();
+        HashSet<UniqueProgramIdentifier> removedSet = new HashSet<>();
         if (removed != null) {
             removedSet.addAll(removed);
         }
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java
index d4ca8d4..0b16141 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java
@@ -45,6 +45,23 @@
     static RadioManager.ProgramInfo makeProgramInfo(ProgramSelector selector,
             ProgramSelector.Identifier logicallyTunedTo,
             ProgramSelector.Identifier physicallyTunedTo, int signalQuality) {
+        if (logicallyTunedTo == null) {
+            logicallyTunedTo = selector.getPrimaryId();
+        }
+        if (physicallyTunedTo == null) {
+            if (selector.getPrimaryId().getType()
+                    == ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT) {
+                for (int i = 0; i < selector.getSecondaryIds().length; i++) {
+                    if (selector.getSecondaryIds()[i].getType()
+                            == ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY) {
+                        physicallyTunedTo = selector.getSecondaryIds()[i];
+                        break;
+                    }
+                }
+            } else {
+                physicallyTunedTo = selector.getPrimaryId();
+            }
+        }
         return new RadioManager.ProgramInfo(selector,
                 logicallyTunedTo, physicallyTunedTo, /* relatedContents= */ null,
                 /* infoFlags= */ 0, signalQuality,
@@ -52,14 +69,8 @@
     }
 
     static RadioManager.ProgramInfo makeProgramInfo(ProgramSelector selector, int signalQuality) {
-        return makeProgramInfo(selector, selector.getPrimaryId(), selector.getPrimaryId(),
-                signalQuality);
-    }
-
-    static RadioManager.ProgramInfo makeProgramInfo(int programType,
-            ProgramSelector.Identifier identifier, int signalQuality) {
-        return makeProgramInfo(makeProgramSelector(programType, identifier),
-                /* logicallyTunedTo= */ null, /* physicallyTunedTo= */ null, signalQuality);
+        return makeProgramInfo(selector, /* logicallyTunedTo= */ null,
+                /* physicallyTunedTo= */ null, signalQuality);
     }
 
     static ProgramSelector makeFmSelector(long freq) {
@@ -70,8 +81,12 @@
 
     static ProgramSelector makeProgramSelector(int programType,
             ProgramSelector.Identifier identifier) {
-        return new ProgramSelector(programType, identifier, /* secondaryIds= */ null,
-                /* vendorIds= */ null);
+        return makeProgramSelector(programType, identifier, /* secondaryIds= */ null);
+    }
+
+    static ProgramSelector makeProgramSelector(int programType,
+            ProgramSelector.Identifier primaryId, ProgramSelector.Identifier[] secondaryIds) {
+        return new ProgramSelector(programType, primaryId, secondaryIds, /* vendorIds= */ null);
     }
 
     static ProgramInfo programInfoToHal(RadioManager.ProgramInfo info) {
@@ -79,6 +94,21 @@
         // function only copies fields that are set by makeProgramInfo().
         ProgramInfo hwInfo = new ProgramInfo();
         hwInfo.selector = Convert.programSelectorToHal(info.getSelector());
+        hwInfo.logicallyTunedTo = hwInfo.selector.primaryId;
+        if (info.getSelector().getPrimaryId().getType()
+                == ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT) {
+            for (int i = 0; i < info.getSelector().getSecondaryIds().length; i++) {
+                if (info.getSelector().getSecondaryIds()[i].getType()
+                        == ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY) {
+                    hwInfo.physicallyTunedTo = Convert.programIdentifierToHal(info.getSelector()
+                            .getSecondaryIds()[i]);
+                    break;
+                }
+            }
+        } else {
+            hwInfo.physicallyTunedTo = Convert.programIdentifierToHal(info.getSelector()
+                    .getPrimaryId());
+        }
         hwInfo.signalQuality = info.getSignalStrength();
         return hwInfo;
     }
diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
index 0778311..819178f 100644
--- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
+++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java
@@ -99,7 +99,7 @@
 
     // The first sequence number to try with. Use a large number to avoid conflicts with the first a
     // few sequence numbers the framework used to launch the test activity.
-    private static final int BASE_SEQ = 10000;
+    private static final int BASE_SEQ = 10000000;
 
     @Rule
     public final ActivityTestRule<TestActivity> mActivityTestRule =
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 6057852..721a2db 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -3067,6 +3067,12 @@
       "group": "WM_DEBUG_REMOTE_ANIMATIONS",
       "at": "com\/android\/server\/wm\/RemoteAnimationController.java"
     },
+    "643263584": {
+      "message": "Content Recording: Apply transformations of shift %d x %d, scale %f, crop %d x %d for display %d",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_CONTENT_RECORDING",
+      "at": "com\/android\/server\/wm\/ContentRecorder.java"
+    },
     "644675193": {
       "message": "Real start recents",
       "level": "DEBUG",
diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java
index a4c655c8c..1ff5a3d 100644
--- a/graphics/java/android/graphics/Bitmap.java
+++ b/graphics/java/android/graphics/Bitmap.java
@@ -1779,7 +1779,7 @@
      * If the bitmap's internal config is in one of the public formats, return
      * that config, otherwise return null.
      */
-    @NonNull
+    @Nullable
     public final Config getConfig() {
         if (mRecycled) {
             Log.w(TAG, "Called getConfig() on a recycle()'d bitmap! This is undefined behavior!");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
index 9b80063..4640106 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -245,8 +245,8 @@
 
     private boolean shouldShowBackdrop(@NonNull TransitionInfo info,
             @NonNull TransitionInfo.Change change) {
-        final Animation a = loadAttributeAnimation(info.getType(), info, change,
-                WALLPAPER_TRANSITION_NONE, mTransitionAnimation, false);
+        final Animation a = loadAttributeAnimation(info, change, WALLPAPER_TRANSITION_NONE,
+                mTransitionAnimation, false);
         return a != null && a.getShowBackdrop();
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 093ecb1..d10de83 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -131,6 +131,7 @@
 
     private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150;
 
+    /** Should be kept in sync with value in TaskbarScrimViewController. */
     private static final float SCRIM_ALPHA = 0.32f;
 
     /** Minimum alpha value for scrim when alpha is being changed via drag */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index f90ee58..991b699 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -43,6 +43,7 @@
 import android.app.ActivityTaskManager;
 import android.app.PendingIntent;
 import android.app.TaskInfo;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ShortcutInfo;
@@ -814,21 +815,22 @@
         final String packageName1 = SplitScreenUtils.getPackageName(intent);
         final String packageName2 = getPackageName(reverseSplitPosition(position));
         final int userId2 = getUserId(reverseSplitPosition(position));
+        final ComponentName component = intent.getIntent().getComponent();
+
+        // To prevent accumulating large number of instances in the background, reuse task
+        // in the background. If we don't explicitly reuse, new may be created even if the app
+        // isn't multi-instance because WM won't automatically remove/reuse the previous instance
+        final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional
+                .map(recentTasks -> recentTasks.findTaskInBackground(component, userId1))
+                .orElse(null);
+        if (taskInfo != null) {
+            startTask(taskInfo.taskId, position, options);
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
+                    "Start task in background");
+            return;
+        }
         if (samePackage(packageName1, packageName2, userId1, userId2)) {
             if (supportMultiInstancesSplit(packageName1)) {
-                // To prevent accumulating large number of instances in the background, reuse task
-                // in the background with priority.
-                final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional
-                        .map(recentTasks -> recentTasks.findTaskInBackground(
-                                intent.getIntent().getComponent(), userId1))
-                        .orElse(null);
-                if (taskInfo != null) {
-                    startTask(taskInfo.taskId, position, options);
-                    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
-                            "Start task in background");
-                    return;
-                }
-
                 // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of
                 // the split and there is no reusable background task.
                 fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index d310ae3..7df658e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -37,12 +37,8 @@
 import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
 import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
 import static android.view.WindowManager.TRANSIT_CHANGE;
-import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
-import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_RELAUNCH;
-import static android.view.WindowManager.TRANSIT_TO_FRONT;
-import static android.window.TransitionInfo.FLAGS_IS_NON_APP_WINDOW;
 import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
 import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL;
 import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL;
@@ -338,10 +334,6 @@
         boolean isDisplayRotationAnimationStarted = false;
         final boolean isDreamTransition = isDreamTransition(info);
         final boolean isOnlyTranslucent = isOnlyTranslucent(info);
-        final boolean isActivityReplace = checkActivityReplacement(info, startTransaction);
-        // Some patterns (eg. activity "replacement") require us to re-interpret the type
-        @WindowManager.TransitionType final int transitType =
-                isActivityReplace ? TRANSIT_OPEN : info.getType();
 
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             final TransitionInfo.Change change = info.getChanges().get(i);
@@ -438,8 +430,7 @@
             // Don't animate anything that isn't independent.
             if (!TransitionInfo.isIndependent(change, info)) continue;
 
-            Animation a = loadAnimation(transitType, info, change, wallpaperTransit,
-                    isDreamTransition);
+            Animation a = loadAnimation(info, change, wallpaperTransit, isDreamTransition);
             if (a != null) {
                 if (isTask) {
                     final boolean isTranslucent = (change.getFlags() & FLAG_TRANSLUCENT) != 0;
@@ -613,53 +604,6 @@
         return (translucentOpen + translucentClose) > 0;
     }
 
-    /**
-     * Checks for an edge-case where an activity calls finish() followed immediately by
-     * startActivity() to "replace" itself. If in this case, it will swap the layer of the
-     * close/open activities and return `true`. This way, we pretend like we are just "opening"
-     * the new activity.
-     */
-    private static boolean checkActivityReplacement(@NonNull TransitionInfo info,
-            SurfaceControl.Transaction t) {
-        if (info.getType() != TRANSIT_CLOSE) {
-            return false;
-        }
-        int closing = -1;
-        int opening = -1;
-        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-            final TransitionInfo.Change change = info.getChanges().get(i);
-            if ((change.getTaskInfo() != null || change.hasFlags(FLAG_IS_DISPLAY))
-                    && !TransitionUtil.isOrderOnly(change)) {
-                // This isn't an activity-level transition.
-                return false;
-            }
-            if (change.getTaskInfo() != null
-                    && change.hasFlags(FLAG_IS_DISPLAY | FLAGS_IS_NON_APP_WINDOW)) {
-                // Ignore non-activity containers.
-                continue;
-            }
-            if (TransitionUtil.isClosingType(change.getMode())) {
-                closing = i;
-            } else if (change.getMode() == TRANSIT_OPEN) {
-                // OPEN implies that it is a new launch. If going "back" the opening app will be
-                // TO_FRONT
-                opening = i;
-            } else if (change.getMode() == TRANSIT_TO_FRONT) {
-                // Normal "going back", so not a replacement.
-                return false;
-            }
-        }
-        if (closing < 0 || opening < 0) {
-            return false;
-        }
-        // Swap the opening and closing z-orders since we're swapping the transit type.
-        final int numChanges = info.getChanges().size();
-        final int zSplitLine = numChanges + 1;
-        t.setLayer(info.getChanges().get(opening).getLeash(), zSplitLine + numChanges - opening);
-        t.setLayer(info.getChanges().get(closing).getLeash(), zSplitLine - closing);
-        return true;
-    }
-
     @Override
     public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
@@ -712,11 +656,12 @@
     }
 
     @Nullable
-    private Animation loadAnimation(int type, @NonNull TransitionInfo info,
+    private Animation loadAnimation(@NonNull TransitionInfo info,
             @NonNull TransitionInfo.Change change, int wallpaperTransit,
             boolean isDreamTransition) {
         Animation a;
 
+        final int type = info.getType();
         final int flags = info.getFlags();
         final int changeMode = change.getMode();
         final int changeFlags = change.getFlags();
@@ -771,8 +716,8 @@
             // If there's a scene-transition, then jump-cut.
             return null;
         } else {
-            a = loadAttributeAnimation(type, info, change, wallpaperTransit, mTransitionAnimation,
-                    isDreamTransition);
+            a = loadAttributeAnimation(
+                    info, change, wallpaperTransit, mTransitionAnimation, isDreamTransition);
         }
 
         if (a != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
index c99911d..d07d2b7b6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
@@ -45,7 +45,6 @@
 import android.graphics.Shader;
 import android.view.Surface;
 import android.view.SurfaceControl;
-import android.view.WindowManager;
 import android.view.animation.Animation;
 import android.view.animation.Transformation;
 import android.window.ScreenCapture;
@@ -62,10 +61,10 @@
 
     /** Loads the animation that is defined through attribute id for the given transition. */
     @Nullable
-    public static Animation loadAttributeAnimation(@WindowManager.TransitionType int type,
-            @NonNull TransitionInfo info, @NonNull TransitionInfo.Change change,
-            int wallpaperTransit, @NonNull TransitionAnimation transitionAnimation,
-            boolean isDreamTransition) {
+    public static Animation loadAttributeAnimation(@NonNull TransitionInfo info,
+            @NonNull TransitionInfo.Change change, int wallpaperTransit,
+            @NonNull TransitionAnimation transitionAnimation, boolean isDreamTransition) {
+        final int type = info.getType();
         final int changeMode = change.getMode();
         final int changeFlags = change.getFlags();
         final boolean enter = TransitionUtil.isOpeningType(changeMode);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index 568db91..99cd4f3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -36,6 +36,7 @@
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -218,8 +219,7 @@
     }
 
     @Test
-    public void startIntent_multiInstancesSupported_startTaskInBackgroundBeforeSplitActivated() {
-        doReturn(true).when(mSplitScreenController).supportMultiInstancesSplit(any());
+    public void startIntent_multiInstancesNotSupported_startTaskInBackgroundBeforeSplitActivated() {
         doNothing().when(mSplitScreenController).startTask(anyInt(), anyInt(), any());
         Intent startIntent = createStartIntent("startActivity");
         PendingIntent pendingIntent =
@@ -237,6 +237,8 @@
 
         verify(mSplitScreenController).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT),
                 isNull());
+        verify(mSplitScreenController, never()).supportMultiInstancesSplit(any());
+        verify(mStageCoordinator, never()).switchSplitPosition(any());
     }
 
     @Test
diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java
index e46db75..33907ec 100644
--- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java
+++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java
@@ -344,7 +344,9 @@
                 continue;
             }
             final ProviderInfo providerInfo = resolved.providerInfo;
-            final List<Bundle> entryData = getEntryDataFromProvider(context,
+            final List<Bundle> entryData = getEntryDataFromProvider(
+                    // Build new context so the entry data is retrieved for the queried user.
+                    context.createContextAsUser(user, 0 /* flags */),
                     providerInfo.authority);
             if (entryData == null || entryData.isEmpty()) {
                 continue;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileUtilsTest.java
index 2086466..a8063e8 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileUtilsTest.java
@@ -35,6 +35,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -52,6 +53,7 @@
 import android.content.pm.ProviderInfo;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
+import android.net.Uri;
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -69,6 +71,8 @@
 import org.robolectric.annotation.Config;
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.util.ReflectionHelpers;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -88,6 +92,10 @@
     private UserManager mUserManager;
     @Mock
     private ContentResolver mContentResolver;
+    @Mock
+    private Context mUserContext;
+    @Mock
+    private ContentResolver mUserContentResolver;
 
     private static final String URI_GET_SUMMARY = "content://authority/text/summary";
     private static final String URI_GET_ICON = "content://authority/icon/my_icon";
@@ -104,6 +112,8 @@
         mContentResolver = spy(application.getContentResolver());
         when(mContext.getContentResolver()).thenReturn(mContentResolver);
         when(mContext.getPackageName()).thenReturn("com.android.settings");
+        when(mUserContext.getContentResolver()).thenReturn(mUserContentResolver);
+        ShadowTileUtils.sCallRealEntryDataFromProvider = false;
     }
 
     @Test
@@ -375,6 +385,30 @@
     }
 
     @Test
+    public void loadTilesForAction_forUserProvider_getEntryDataFromProvider_inContextOfGivenUser() {
+        ShadowTileUtils.sCallRealEntryDataFromProvider = true;
+        UserHandle userHandle = new UserHandle(10);
+
+        doReturn(mUserContext).when(mContext).createContextAsUser(eq(userHandle), anyInt());
+
+        Map<Pair<String, String>, Tile> addedCache = new ArrayMap<>();
+        List<Tile> outTiles = new ArrayList<>();
+        List<ResolveInfo> info = new ArrayList<>();
+        ResolveInfo resolveInfo = newInfo(true, null /* category */, null, URI_GET_ICON,
+                URI_GET_SUMMARY, null, 123, PROFILE_ALL);
+        info.add(resolveInfo);
+
+        when(mPackageManager.queryIntentContentProvidersAsUser(any(Intent.class), anyInt(),
+            anyInt())).thenReturn(info);
+
+        TileUtils.loadTilesForAction(mContext, userHandle, IA_SETTINGS_ACTION,
+                addedCache, null /* defaultCategory */, outTiles, false /* requiresSettings */);
+
+        verify(mUserContentResolver, atLeastOnce())
+            .acquireUnstableProvider(any(Uri.class));
+    }
+
+    @Test
     public void loadTilesForAction_withPendingIntent_updatesPendingIntentMap() {
         Map<Pair<String, String>, Tile> addedCache = new ArrayMap<>();
         List<Tile> outTiles = new ArrayList<>();
@@ -472,8 +506,17 @@
 
         private static Bundle sMetaData;
 
+        private static boolean sCallRealEntryDataFromProvider;
+
         @Implementation
         protected static List<Bundle> getEntryDataFromProvider(Context context, String authority) {
+            if (sCallRealEntryDataFromProvider) {
+                return Shadow.directlyOn(
+                    TileUtils.class,
+                    "getEntryDataFromProvider",
+                    ReflectionHelpers.ClassParameter.from(Context.class, context),
+                    ReflectionHelpers.ClassParameter.from(String.class, authority));
+            }
             return Arrays.asList(sMetaData);
         }
 
diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS
index a892269..78da5a6 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -6,6 +6,7 @@
 
 aaliomer@google.com
 aaronjli@google.com
+achalke@google.com
 acul@google.com
 adamcohen@google.com
 aioana@google.com
@@ -72,6 +73,7 @@
 patmanning@google.com
 peanutbutter@google.com
 peskal@google.com
+petrcermak@google.com
 pinyaoting@google.com
 pixel@google.com
 pomini@google.com
@@ -82,13 +84,17 @@
 shanh@google.com
 snoeberger@google.com
 steell@google.com
+stevenckng@google.com
 stwu@google.com
 syeonlee@google.com
 sunnygoyal@google.com
 thiruram@google.com
+tkachenkoi@google.com
 tracyzhou@google.com
 tsuji@google.com
 twickham@google.com
+vadimt@google.com
+vanjan@google.com
 victortulias@google.com
 winsonc@google.com
 wleshner@google.com
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/ribbon/ui/composable/Ribbon.kt b/packages/SystemUI/compose/features/src/com/android/systemui/ribbon/ui/composable/Ribbon.kt
new file mode 100644
index 0000000..daa1592
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/ribbon/ui/composable/Ribbon.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023 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.ribbon.ui.composable
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.layout
+import com.android.compose.modifiers.thenIf
+import kotlin.math.PI
+import kotlin.math.cos
+import kotlin.math.roundToInt
+import kotlin.math.sin
+import kotlin.math.tan
+
+/**
+ * Renders a "ribbon" at the bottom right corner of its container.
+ *
+ * The [content] is rendered leaning at an angle of [degrees] degrees (between `1` and `89`,
+ * inclusive), with an alpha of [alpha] (between `0f` and `1f`, inclusive).
+ *
+ * The background color of the strip can be modified by passing a value to the [backgroundColor] or
+ * `null` to remove the strip background.
+ *
+ * Note: this function assumes that it's been placed at the bottom right of its parent by its
+ * caller. It's the caller's responsibility to meet that assumption by actually placing this
+ * composable element at the bottom right.
+ */
+@Composable
+fun BottomRightCornerRibbon(
+    content: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    degrees: Int = 45,
+    alpha: Float = 0.6f,
+    backgroundColor: Color? = Color.Red,
+) {
+    check(degrees in 1..89)
+    check(alpha in 0f..1f)
+
+    val radians = degrees * (PI / 180)
+
+    Box(
+        content = { content() },
+        modifier =
+            modifier
+                .graphicsLayer {
+                    this.alpha = alpha
+
+                    val w = size.width
+                    val h = size.height
+
+                    val sine = sin(radians).toFloat()
+                    val cosine = cos(radians).toFloat()
+
+                    translationX = (w - w * cosine + h * sine) / 2f
+                    translationY = (h - w * sine + h * cosine) / 2f
+                    rotationZ = 360f - degrees
+                }
+                .thenIf(backgroundColor != null) { Modifier.background(backgroundColor!!) }
+                .layout { measurable, constraints ->
+                    val placeable = measurable.measure(constraints)
+
+                    val tangent = tan(radians)
+                    val leftPadding = (placeable.measuredHeight / tangent).roundToInt()
+                    val rightPadding = (placeable.measuredHeight * tangent).roundToInt()
+
+                    layout(
+                        width = placeable.measuredWidth + leftPadding + rightPadding,
+                        height = placeable.measuredHeight,
+                    ) {
+                        placeable.place(leftPadding, 0)
+                    }
+                }
+    )
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index 019287d..6a5a368 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -18,14 +18,19 @@
 
 package com.android.systemui.scene.ui.composable
 
+import android.os.SystemProperties
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.input.pointer.motionEventSpy
 import androidx.compose.ui.input.pointer.pointerInput
@@ -37,6 +42,7 @@
 import com.android.compose.animation.scene.Swipe
 import com.android.compose.animation.scene.UserAction as SceneTransitionUserAction
 import com.android.compose.animation.scene.observableTransitionState
+import com.android.systemui.ribbon.ui.composable.BottomRightCornerRibbon
 import com.android.systemui.scene.shared.model.Direction
 import com.android.systemui.scene.shared.model.ObservableTransitionState
 import com.android.systemui.scene.shared.model.SceneKey
@@ -75,53 +81,70 @@
     val currentDestinations: Map<UserAction, SceneModel> by
         currentScene.destinationScenes().collectAsState()
     val state = remember { SceneTransitionLayoutState(currentSceneKey.toTransitionSceneKey()) }
+    val isRibbonEnabled = remember { SystemProperties.getBoolean("flexi.ribbon", false) }
 
     DisposableEffect(viewModel, state) {
         viewModel.setTransitionState(state.observableTransitionState().map { it.toModel() })
         onDispose { viewModel.setTransitionState(null) }
     }
 
-    SceneTransitionLayout(
-        currentScene = currentSceneKey.toTransitionSceneKey(),
-        onChangeScene = viewModel::onSceneChanged,
-        transitions = SceneContainerTransitions,
-        state = state,
-        modifier =
-            modifier
-                .fillMaxSize()
-                .motionEventSpy { event -> viewModel.onMotionEvent(event) }
-                .pointerInput(Unit) {
-                    awaitPointerEventScope {
-                        while (true) {
-                            awaitPointerEvent(PointerEventPass.Final)
-                            viewModel.onMotionEventComplete()
+    Box(
+        modifier = Modifier.fillMaxSize(),
+    ) {
+        SceneTransitionLayout(
+            currentScene = currentSceneKey.toTransitionSceneKey(),
+            onChangeScene = viewModel::onSceneChanged,
+            transitions = SceneContainerTransitions,
+            state = state,
+            modifier =
+                modifier
+                    .fillMaxSize()
+                    .motionEventSpy { event -> viewModel.onMotionEvent(event) }
+                    .pointerInput(Unit) {
+                        awaitPointerEventScope {
+                            while (true) {
+                                awaitPointerEvent(PointerEventPass.Final)
+                                viewModel.onMotionEventComplete()
+                            }
                         }
                     }
-                }
-    ) {
-        sceneByKey.forEach { (sceneKey, composableScene) ->
-            scene(
-                key = sceneKey.toTransitionSceneKey(),
-                userActions =
-                    if (sceneKey == currentSceneKey) {
-                            currentDestinations
-                        } else {
-                            composableScene.destinationScenes().value
-                        }
-                        .map { (userAction, destinationSceneModel) ->
-                            toTransitionModels(userAction, destinationSceneModel)
-                        }
-                        .toMap(),
-            ) {
-                with(composableScene) {
-                    this@scene.Content(
-                        modifier =
-                            Modifier.element(sceneKey.toTransitionSceneKey().rootElementKey)
-                                .fillMaxSize(),
-                    )
+        ) {
+            sceneByKey.forEach { (sceneKey, composableScene) ->
+                scene(
+                    key = sceneKey.toTransitionSceneKey(),
+                    userActions =
+                        if (sceneKey == currentSceneKey) {
+                                currentDestinations
+                            } else {
+                                composableScene.destinationScenes().value
+                            }
+                            .map { (userAction, destinationSceneModel) ->
+                                toTransitionModels(userAction, destinationSceneModel)
+                            }
+                            .toMap(),
+                ) {
+                    with(composableScene) {
+                        this@scene.Content(
+                            modifier =
+                                Modifier.element(sceneKey.toTransitionSceneKey().rootElementKey)
+                                    .fillMaxSize(),
+                        )
+                    }
                 }
             }
         }
+
+        if (isRibbonEnabled) {
+            BottomRightCornerRibbon(
+                content = {
+                    Text(
+                        text = "flexi\uD83E\uDD43",
+                        color = Color.White,
+                    )
+                },
+                modifier = Modifier.align(Alignment.BottomEnd),
+            )
+        }
     }
 }
 
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt
index 5d6dd3b..23d3089 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt
@@ -48,10 +48,11 @@
  */
 fun SystemUIDialogFactory.create(
     context: Context = this.applicationContext,
+    theme: Int = SystemUIDialog.DEFAULT_THEME,
     dismissOnDeviceLock: Boolean = SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK,
     content: @Composable (SystemUIDialog) -> Unit,
 ): ComponentSystemUIDialog {
-    val dialog = create(context, dismissOnDeviceLock)
+    val dialog = create(context, theme, dismissOnDeviceLock)
 
     // Create the dialog so that it is properly constructed before we set the Compose content.
     // Otherwise, the ComposeView won't render properly.
diff --git a/packages/SystemUI/res/layout/media_projection_app_selector.xml b/packages/SystemUI/res/layout/media_projection_app_selector.xml
index e474938..b77f78d 100644
--- a/packages/SystemUI/res/layout/media_projection_app_selector.xml
+++ b/packages/SystemUI/res/layout/media_projection_app_selector.xml
@@ -20,10 +20,11 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:layout_gravity="center"
-    androidprv:maxCollapsedHeight="0dp"
+    androidprv:maxCollapsedHeight="10000dp"
     androidprv:maxCollapsedHeightSmall="56dp"
     androidprv:maxWidth="@*android:dimen/chooser_width"
     android:id="@*android:id/contentPanel">
+    <!-- maxCollapsedHeight above is huge, to make sure the layout is always expanded. -->
 
     <LinearLayout
         android:id="@*android:id/chooser_header"
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
index daff5fe..aa33100 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
@@ -16,13 +16,19 @@
 
 package com.android.systemui.biometrics.data.repository
 
+import android.Manifest.permission.USE_BIOMETRIC_INTERNAL
+import android.annotation.RequiresPermission
+import android.hardware.biometrics.ComponentInfoInternal
 import android.hardware.biometrics.SensorLocationInternal
+import android.hardware.biometrics.SensorProperties
 import android.hardware.fingerprint.FingerprintManager
+import android.hardware.fingerprint.FingerprintSensorProperties
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
 import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.android.systemui.biometrics.shared.model.SensorStrength
 import com.android.systemui.biometrics.shared.model.toSensorStrength
+import com.android.systemui.biometrics.shared.model.toSensorType
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
@@ -31,11 +37,10 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
 
 /**
  * A repository for the global state of FingerprintProperty.
@@ -44,22 +49,17 @@
  */
 interface FingerprintPropertyRepository {
 
-    /**
-     * If the repository is initialized or not. Other properties are defaults until this is true.
-     */
-    val isInitialized: Flow<Boolean>
-
     /** The id of fingerprint sensor. */
-    val sensorId: StateFlow<Int>
+    val sensorId: Flow<Int>
 
     /** The security strength of sensor (convenience, weak, strong). */
-    val strength: StateFlow<SensorStrength>
+    val strength: Flow<SensorStrength>
 
     /** The types of fingerprint sensor (rear, ultrasonic, optical, etc.). */
-    val sensorType: StateFlow<FingerprintSensorType>
+    val sensorType: Flow<FingerprintSensorType>
 
     /** The sensor location relative to each physical display. */
-    val sensorLocations: StateFlow<Map<String, SensorLocationInternal>>
+    val sensorLocations: Flow<Map<String, SensorLocationInternal>>
 }
 
 @SysUISingleton
@@ -70,64 +70,64 @@
     private val fingerprintManager: FingerprintManager?,
 ) : FingerprintPropertyRepository {
 
-    override val isInitialized: Flow<Boolean> =
+    @RequiresPermission(USE_BIOMETRIC_INTERNAL)
+    private val props: StateFlow<FingerprintSensorPropertiesInternal> =
         conflatedCallbackFlow {
                 val callback =
                     object : IFingerprintAuthenticatorsRegisteredCallback.Stub() {
                         override fun onAllAuthenticatorsRegistered(
                             sensors: List<FingerprintSensorPropertiesInternal>
                         ) {
-                            if (sensors.isNotEmpty()) {
-                                setProperties(sensors[0])
-                                trySendWithFailureLogging(true, TAG, "initialize properties")
+                            if (sensors.isEmpty()) {
+                                trySendWithFailureLogging(
+                                    DEFAULT_PROPS,
+                                    TAG,
+                                    "no registered sensors, use default props"
+                                )
+                            } else {
+                                trySendWithFailureLogging(
+                                    sensors[0],
+                                    TAG,
+                                    "update properties on authenticators registered"
+                                )
                             }
                         }
                     }
                 fingerprintManager?.addAuthenticatorsRegisteredCallback(callback)
-                trySendWithFailureLogging(false, TAG, "initial value defaulting to false")
                 awaitClose {}
             }
-            .shareIn(scope = applicationScope, started = SharingStarted.Eagerly, replay = 1)
+            .stateIn(
+                applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = DEFAULT_PROPS,
+            )
 
-    private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1)
-    override val sensorId: StateFlow<Int> = _sensorId.asStateFlow()
+    override val sensorId: Flow<Int> = props.map { it.sensorId }
 
-    private val _strength: MutableStateFlow<SensorStrength> =
-        MutableStateFlow(SensorStrength.CONVENIENCE)
-    override val strength = _strength.asStateFlow()
+    override val strength: Flow<SensorStrength> = props.map { it.sensorStrength.toSensorStrength() }
 
-    private val _sensorType: MutableStateFlow<FingerprintSensorType> =
-        MutableStateFlow(FingerprintSensorType.UNKNOWN)
-    override val sensorType = _sensorType.asStateFlow()
+    override val sensorType: Flow<FingerprintSensorType> =
+        props.map { it.sensorType.toSensorType() }
 
-    private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> =
-        MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT))
-    override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> =
-        _sensorLocations.asStateFlow()
-
-    private fun setProperties(prop: FingerprintSensorPropertiesInternal) {
-        _sensorId.value = prop.sensorId
-        _strength.value = prop.sensorStrength.toSensorStrength()
-        _sensorType.value = sensorTypeIntToObject(prop.sensorType)
-        _sensorLocations.value =
-            prop.allLocations.associateBy { sensorLocationInternal ->
+    override val sensorLocations: Flow<Map<String, SensorLocationInternal>> =
+        props.map {
+            it.allLocations.associateBy { sensorLocationInternal ->
                 sensorLocationInternal.displayId
             }
-    }
+        }
 
     companion object {
         private const val TAG = "FingerprintPropertyRepositoryImpl"
-    }
-}
-
-private fun sensorTypeIntToObject(value: Int): FingerprintSensorType {
-    return when (value) {
-        0 -> FingerprintSensorType.UNKNOWN
-        1 -> FingerprintSensorType.REAR
-        2 -> FingerprintSensorType.UDFPS_ULTRASONIC
-        3 -> FingerprintSensorType.UDFPS_OPTICAL
-        4 -> FingerprintSensorType.POWER_BUTTON
-        5 -> FingerprintSensorType.HOME_BUTTON
-        else -> throw IllegalArgumentException("Invalid SensorType value: $value")
+        private val DEFAULT_PROPS =
+            FingerprintSensorPropertiesInternal(
+                -1 /* sensorId */,
+                SensorProperties.STRENGTH_CONVENIENCE,
+                0 /* maxEnrollmentsPerUser */,
+                listOf<ComponentInfoInternal>(),
+                FingerprintSensorProperties.TYPE_UNKNOWN,
+                false /* halControlsIllumination */,
+                true /* resetLockoutRequiresHardwareAuthToken */,
+                listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT)
+            )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
index 5badcaf..a6ad24e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
@@ -32,7 +32,6 @@
 import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
@@ -69,7 +68,7 @@
     val isConfirmationRequired: Flow<Boolean>
 
     /** Fingerprint sensor type */
-    val sensorType: StateFlow<FingerprintSensorType>
+    val sensorType: Flow<FingerprintSensorType>
 
     /** Use biometrics for authentication. */
     fun useBiometricsForAuthentication(
@@ -95,7 +94,7 @@
 class PromptSelectorInteractorImpl
 @Inject
 constructor(
-    private val fingerprintPropertyRepository: FingerprintPropertyRepository,
+    fingerprintPropertyRepository: FingerprintPropertyRepository,
     private val promptRepository: PromptRepository,
     lockPatternUtils: LockPatternUtils,
 ) : PromptSelectorInteractor {
@@ -147,8 +146,7 @@
             }
         }
 
-    override val sensorType: StateFlow<FingerprintSensorType> =
-        fingerprintPropertyRepository.sensorType
+    override val sensorType: Flow<FingerprintSensorType> = fingerprintPropertyRepository.sensorType
 
     override fun useBiometricsForAuthentication(
         promptInfo: PromptInfo,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
index aa85e5f3..75ae061 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
@@ -17,32 +17,43 @@
 package com.android.systemui.biometrics.domain.interactor
 
 import android.hardware.biometrics.SensorLocationInternal
-import android.util.Log
 import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
 import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
 
 /** Business logic for SideFps overlay offsets. */
 interface SideFpsOverlayInteractor {
 
-    /** Get the corresponding offsets based on different displayId. */
-    fun getOverlayOffsets(displayId: String): SensorLocationInternal
+    /** The displayId of the current display. */
+    val displayId: Flow<String>
+
+    /** Overlay offsets corresponding to given displayId. */
+    val overlayOffsets: Flow<SensorLocationInternal>
+
+    /** Called on display changes, used to keep the display state in sync */
+    fun onDisplayChanged(displayId: String)
 }
 
 @SysUISingleton
 class SideFpsOverlayInteractorImpl
 @Inject
-constructor(private val fingerprintPropertyRepository: FingerprintPropertyRepository) :
+constructor(fingerprintPropertyRepository: FingerprintPropertyRepository) :
     SideFpsOverlayInteractor {
 
-    override fun getOverlayOffsets(displayId: String): SensorLocationInternal {
-        val offsets = fingerprintPropertyRepository.sensorLocations.value
-        return if (offsets.containsKey(displayId)) {
-            offsets[displayId]!!
-        } else {
-            Log.w(TAG, "No location specified for display: $displayId")
-            offsets[""]!!
+    private val _displayId: MutableStateFlow<String> = MutableStateFlow("")
+    override val displayId: Flow<String> = _displayId.asStateFlow()
+
+    override val overlayOffsets: Flow<SensorLocationInternal> =
+        combine(displayId, fingerprintPropertyRepository.sensorLocations) { displayId, offsets ->
+            offsets[displayId] ?: SensorLocationInternal.DEFAULT
         }
+
+    override fun onDisplayChanged(displayId: String) {
+        _displayId.value = displayId
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/FingerprintSensorType.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/FingerprintSensorType.kt
index df5cefd..c6fdcb3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/FingerprintSensorType.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/FingerprintSensorType.kt
@@ -27,3 +27,15 @@
     POWER_BUTTON,
     HOME_BUTTON,
 }
+
+/** Convert [this] to corresponding [FingerprintSensorType] */
+fun Int.toSensorType(): FingerprintSensorType =
+    when (this) {
+        FingerprintSensorProperties.TYPE_UNKNOWN -> FingerprintSensorType.UNKNOWN
+        FingerprintSensorProperties.TYPE_REAR -> FingerprintSensorType.REAR
+        FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC -> FingerprintSensorType.UDFPS_ULTRASONIC
+        FingerprintSensorProperties.TYPE_UDFPS_OPTICAL -> FingerprintSensorType.UDFPS_OPTICAL
+        FingerprintSensorProperties.TYPE_POWER_BUTTON -> FingerprintSensorType.POWER_BUTTON
+        FingerprintSensorProperties.TYPE_HOME_BUTTON -> FingerprintSensorType.HOME_BUTTON
+        else -> throw IllegalArgumentException("Invalid SensorType value: $this")
+    }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt
index 30e865e..476daac 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/SensorStrength.kt
@@ -28,8 +28,8 @@
 /** Convert [this] to corresponding [SensorStrength] */
 fun Int.toSensorStrength(): SensorStrength =
     when (this) {
-        0 -> SensorStrength.CONVENIENCE
-        1 -> SensorStrength.WEAK
-        2 -> SensorStrength.STRONG
+        SensorProperties.STRENGTH_CONVENIENCE -> SensorStrength.CONVENIENCE
+        SensorProperties.STRENGTH_WEAK -> SensorStrength.WEAK
+        SensorProperties.STRENGTH_STRONG -> SensorStrength.STRONG
         else -> throw IllegalArgumentException("Invalid SensorStrength value: $this")
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModel.kt
index 9b30acb..b406ea4 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptFingerprintIconViewModel.kt
@@ -33,7 +33,7 @@
 @Inject
 constructor(
     private val displayStateInteractor: DisplayStateInteractor,
-    private val promptSelectorInteractor: PromptSelectorInteractor,
+    promptSelectorInteractor: PromptSelectorInteractor,
 ) {
     /** Current device rotation. */
     private var rotation: Int = Surface.ROTATION_0
diff --git a/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt b/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt
index ecc9d0e..f730935 100644
--- a/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/ui/view/MirroringConfirmationDialog.kt
@@ -34,7 +34,8 @@
     context: Context,
     private val onStartMirroringClickListener: View.OnClickListener,
     private val onCancelMirroring: View.OnClickListener,
-) : Dialog(context, R.style.Theme_SystemUI_Dialog) {
+    theme: Int = R.style.Theme_SystemUI_Dialog,
+) : Dialog(context, theme) {
 
     private lateinit var mirrorButton: TextView
     private lateinit var dismissButton: TextView
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 1e5fcbe..2cc07fd 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -802,4 +802,8 @@
     /** Enable haptic slider component in the brightness slider */
     @JvmField
     val HAPTIC_BRIGHTNESS_SLIDER = unreleasedFlag("haptic_brightness_slider")
+
+    // TODO(b/287205379): Tracking bug
+    @JvmField
+    val QS_CONTAINER_GRAPH_OPTIMIZER = unreleasedFlag( "qs_container_graph_optimizer")
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
index b5b56b2..6f25f7c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
@@ -44,7 +44,8 @@
     context: Context,
     initialCurrentLevel: Int,
     initialMaxLevel: Int,
-) : Dialog(context, R.style.Theme_SystemUI_Dialog) {
+    theme: Int = R.style.Theme_SystemUI_Dialog,
+) : Dialog(context, theme) {
 
     private data class RootProperties(
         val cornerRadius: Float,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
index 8954947..b82e01b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
@@ -50,7 +50,6 @@
 import com.android.systemui.log.FaceAuthenticationLogger
 import com.android.systemui.log.SessionTracker
 import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.user.data.model.SelectionStatus
 import com.android.systemui.user.data.repository.UserRepository
@@ -66,9 +65,10 @@
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.flowOf
@@ -77,6 +77,7 @@
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
@@ -153,7 +154,7 @@
     private val faceAuthLogger: FaceAuthenticationLogger,
     private val biometricSettingsRepository: BiometricSettingsRepository,
     private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
-    private val trustRepository: TrustRepository,
+    trustRepository: TrustRepository,
     private val keyguardRepository: KeyguardRepository,
     private val keyguardInteractor: KeyguardInteractor,
     private val alternateBouncerInteractor: AlternateBouncerInteractor,
@@ -202,11 +203,9 @@
     private val keyguardSessionId: InstanceId?
         get() = sessionTracker.getSessionId(StatusBarManager.SESSION_KEYGUARD)
 
-    private val _canRunFaceAuth = MutableStateFlow(false)
     override val canRunFaceAuth: StateFlow<Boolean>
-        get() = _canRunFaceAuth
 
-    private val canRunDetection = MutableStateFlow(false)
+    private val canRunDetection: StateFlow<Boolean>
 
     private val _isAuthenticated = MutableStateFlow(false)
     override val isAuthenticated: Flow<Boolean>
@@ -252,10 +251,58 @@
         dumpManager.registerCriticalDumpable("DeviceEntryFaceAuthRepositoryImpl", this)
 
         if (featureFlags.isEnabled(Flags.FACE_AUTH_REFACTOR)) {
+            canRunFaceAuth =
+                listOf(
+                        *gatingConditionsForAuthAndDetect(),
+                        Pair(isLockedOut.isFalse(), "isNotInLockOutState"),
+                        Pair(
+                            trustRepository.isCurrentUserTrusted.isFalse(),
+                            "currentUserIsNotTrusted"
+                        ),
+                        Pair(
+                            biometricSettingsRepository.isFaceAuthCurrentlyAllowed,
+                            "isFaceAuthCurrentlyAllowed"
+                        ),
+                        Pair(isAuthenticated.isFalse(), "faceNotAuthenticated"),
+                    )
+                    .andAllFlows("canFaceAuthRun", faceAuthLog)
+                    .flowOn(mainDispatcher)
+                    .stateIn(applicationScope, SharingStarted.Eagerly, false)
+
+            // Face detection can run only when lockscreen bypass is enabled
+            // & detection is supported
+            //   & biometric unlock is not allowed
+            //     or user is trusted by trust manager & we want to run face detect to dismiss
+            // keyguard
+            canRunDetection =
+                listOf(
+                        *gatingConditionsForAuthAndDetect(),
+                        Pair(isBypassEnabled, "isBypassEnabled"),
+                        Pair(
+                            biometricSettingsRepository.isFaceAuthCurrentlyAllowed
+                                .isFalse()
+                                .or(trustRepository.isCurrentUserTrusted),
+                            "faceAuthIsNotCurrentlyAllowedOrCurrentUserIsTrusted"
+                        ),
+                        // We don't want to run face detect if fingerprint can be used to unlock the
+                        // device
+                        // but it's not possible to authenticate with FP from the bouncer (UDFPS)
+                        Pair(
+                            and(isUdfps(), deviceEntryFingerprintAuthRepository.isRunning)
+                                .isFalse(),
+                            "udfpsAuthIsNotPossibleAnymore"
+                        )
+                    )
+                    .andAllFlows("canFaceDetectRun", faceDetectLog)
+                    .flowOn(mainDispatcher)
+                    .stateIn(applicationScope, SharingStarted.Eagerly, false)
             observeFaceAuthGatingChecks()
             observeFaceDetectGatingChecks()
             observeFaceAuthResettingConditions()
             listenForSchedulingWatchdog()
+        } else {
+            canRunFaceAuth = MutableStateFlow(false).asStateFlow()
+            canRunDetection = MutableStateFlow(false).asStateFlow()
         }
     }
 
@@ -298,39 +345,13 @@
     }
 
     private fun observeFaceDetectGatingChecks() {
-        // Face detection can run only when lockscreen bypass is enabled
-        // & detection is supported
-        //   & biometric unlock is not allowed
-        //     or user is trusted by trust manager & we want to run face detect to dismiss keyguard
-        listOf(
-                canFaceAuthOrDetectRun(faceDetectLog),
-                logAndObserve(isBypassEnabled, "isBypassEnabled", faceDetectLog),
-                logAndObserve(
-                    biometricSettingsRepository.isFaceAuthCurrentlyAllowed
-                        .isFalse()
-                        .or(trustRepository.isCurrentUserTrusted),
-                    "faceAuthIsNotCurrentlyAllowedOrCurrentUserIsTrusted",
-                    faceDetectLog
-                ),
-                // We don't want to run face detect if fingerprint can be used to unlock the device
-                // but it's not possible to authenticate with FP from the bouncer (UDFPS)
-                logAndObserve(
-                    and(isUdfps(), deviceEntryFingerprintAuthRepository.isRunning).isFalse(),
-                    "udfpsAuthIsNotPossibleAnymore",
-                    faceDetectLog
-                )
-            )
-            .reduce(::and)
-            .distinctUntilChanged()
+        canRunDetection
             .onEach {
-                faceAuthLogger.canRunDetectionChanged(it)
-                canRunDetection.value = it
                 if (!it) {
                     cancelDetection()
                 }
             }
             .flowOn(mainDispatcher)
-            .logDiffsForTable(faceDetectLog, "", "canFaceDetectRun", false)
             .launchIn(applicationScope)
     }
 
@@ -339,76 +360,44 @@
             it == BiometricType.UNDER_DISPLAY_FINGERPRINT
         }
 
-    private fun canFaceAuthOrDetectRun(tableLogBuffer: TableLogBuffer): Flow<Boolean> {
-        return listOf(
-                logAndObserve(
-                    biometricSettingsRepository.isFaceAuthEnrolledAndEnabled,
-                    "isFaceAuthEnrolledAndEnabled",
-                    tableLogBuffer
-                ),
-                logAndObserve(faceAuthPaused.isFalse(), "faceAuthIsNotPaused", tableLogBuffer),
-                logAndObserve(
-                    keyguardRepository.isKeyguardGoingAway.isFalse(),
-                    "keyguardNotGoingAway",
-                    tableLogBuffer
-                ),
-                logAndObserve(
-                    keyguardRepository.wakefulness.map { it.isStartingToSleep() }.isFalse(),
-                    "deviceNotStartingToSleep",
-                    tableLogBuffer
-                ),
-                logAndObserve(
-                    keyguardInteractor.isSecureCameraActive
-                        .isFalse()
-                        .or(
-                            alternateBouncerInteractor.isVisible.or(
-                                keyguardInteractor.primaryBouncerShowing
-                            )
-                        ),
-                    "secureCameraNotActiveOrAnyBouncerIsShowing",
-                    tableLogBuffer
-                ),
-                logAndObserve(
-                    biometricSettingsRepository.isFaceAuthSupportedInCurrentPosture,
-                    "isFaceAuthSupportedInCurrentPosture",
-                    tableLogBuffer
-                ),
-                logAndObserve(
-                    biometricSettingsRepository.isCurrentUserInLockdown.isFalse(),
-                    "userHasNotLockedDownDevice",
-                    tableLogBuffer
-                ),
-                logAndObserve(
-                    keyguardRepository.isKeyguardShowing,
-                    "isKeyguardShowing",
-                    tableLogBuffer
-                )
-            )
-            .reduce(::and)
+    private fun gatingConditionsForAuthAndDetect(): Array<Pair<Flow<Boolean>, String>> {
+        return arrayOf(
+            Pair(
+                biometricSettingsRepository.isFaceAuthEnrolledAndEnabled,
+                "isFaceAuthEnrolledAndEnabled"
+            ),
+            Pair(faceAuthPaused.isFalse(), "faceAuthIsNotPaused"),
+            Pair(keyguardRepository.isKeyguardGoingAway.isFalse(), "keyguardNotGoingAway"),
+            Pair(
+                keyguardRepository.wakefulness.map { it.isStartingToSleep() }.isFalse(),
+                "deviceNotStartingToSleep"
+            ),
+            Pair(
+                keyguardInteractor.isSecureCameraActive
+                    .isFalse()
+                    .or(
+                        alternateBouncerInteractor.isVisible.or(
+                            keyguardInteractor.primaryBouncerShowing
+                        )
+                    ),
+                "secureCameraNotActiveOrAnyBouncerIsShowing"
+            ),
+            Pair(
+                biometricSettingsRepository.isFaceAuthSupportedInCurrentPosture,
+                "isFaceAuthSupportedInCurrentPosture"
+            ),
+            Pair(
+                biometricSettingsRepository.isCurrentUserInLockdown.isFalse(),
+                "userHasNotLockedDownDevice"
+            ),
+            Pair(keyguardRepository.isKeyguardShowing, "isKeyguardShowing")
+        )
     }
 
     private fun observeFaceAuthGatingChecks() {
-        // Face auth can run only if all of the gating conditions are true.
-        listOf(
-                canFaceAuthOrDetectRun(faceAuthLog),
-                logAndObserve(isLockedOut.isFalse(), "isNotInLockOutState", faceAuthLog),
-                logAndObserve(
-                    trustRepository.isCurrentUserTrusted.isFalse(),
-                    "currentUserIsNotTrusted",
-                    faceAuthLog
-                ),
-                logAndObserve(
-                    biometricSettingsRepository.isFaceAuthCurrentlyAllowed,
-                    "isFaceAuthCurrentlyAllowed",
-                    faceAuthLog
-                ),
-                logAndObserve(isAuthenticated.isFalse(), "faceNotAuthenticated", faceAuthLog),
-            )
-            .reduce(::and)
-            .distinctUntilChanged()
+        canRunFaceAuth
             .onEach {
                 faceAuthLogger.canFaceAuthRunChanged(it)
-                _canRunFaceAuth.value = it
                 if (!it) {
                     // Cancel currently running auth if any of the gating checks are false.
                     faceAuthLogger.cancellingFaceAuth()
@@ -416,7 +405,6 @@
                 }
             }
             .flowOn(mainDispatcher)
-            .logDiffsForTable(faceAuthLog, "", "canFaceAuthRun", false)
             .launchIn(applicationScope)
     }
 
@@ -618,22 +606,6 @@
         _isAuthRunning.value = false
     }
 
-    private fun logAndObserve(
-        cond: Flow<Boolean>,
-        conditionName: String,
-        logBuffer: TableLogBuffer
-    ): Flow<Boolean> {
-        return cond
-            .distinctUntilChanged()
-            .logDiffsForTable(
-                logBuffer,
-                columnName = conditionName,
-                columnPrefix = "",
-                initialValue = false
-            )
-            .onEach { faceAuthLogger.observedConditionChanged(it, conditionName) }
-    }
-
     companion object {
         const val TAG = "DeviceEntryFaceAuthRepository"
 
@@ -688,3 +660,18 @@
 private fun Flow<Boolean>.isFalse(): Flow<Boolean> {
     return this.map { !it }
 }
+
+private fun List<Pair<Flow<Boolean>, String>>.andAllFlows(
+    combinedLoggingInfo: String,
+    tableLogBuffer: TableLogBuffer
+): Flow<Boolean> {
+    return combine(this.map { it.first }) {
+        val combinedValue =
+            it.reduceIndexed { index, accumulator, current ->
+                tableLogBuffer.logChange(prefix = "", columnName = this[index].second, current)
+                return@reduceIndexed accumulator && current
+            }
+        tableLogBuffer.logChange(prefix = "", combinedLoggingInfo, combinedValue)
+        return@combine combinedValue
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
index f91ae74..f5ef27d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT
 import com.android.systemui.keyguard.ui.view.layout.blueprints.KeyguardBlueprintModule
 import java.io.PrintWriter
+import java.util.TreeMap
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
@@ -52,11 +53,12 @@
     blueprints: Set<@JvmSuppressWildcards KeyguardBlueprint>,
     @Application private val applicationScope: CoroutineScope,
 ) {
-    private val blueprintIdMap: Map<String, KeyguardBlueprint> = blueprints.associateBy { it.id }
+    private val blueprintIdMap: TreeMap<String, KeyguardBlueprint> = TreeMap()
     private val _blueprint: MutableSharedFlow<KeyguardBlueprint> = MutableSharedFlow(replay = 1)
     val blueprint: Flow<KeyguardBlueprint> = _blueprint.asSharedFlow()
 
     init {
+        blueprintIdMap.putAll(blueprints.associateBy { it.id })
         applyBlueprint(blueprintIdMap[DEFAULT]!!)
         applicationScope.launch {
             configurationRepository.onAnyConfigurationChange.collect { refreshBlueprint() }
@@ -69,6 +71,20 @@
      * @param blueprintId
      * @return whether the transition has succeeded.
      */
+    fun applyBlueprint(index: Int): Boolean {
+        ArrayList(blueprintIdMap.values)[index]?.let {
+            applyBlueprint(it)
+            return true
+        }
+        return false
+    }
+
+    /**
+     * Emits the blueprint value to the collectors.
+     *
+     * @param blueprintId
+     * @return whether the transition has succeeded.
+     */
     fun applyBlueprint(blueprintId: String?): Boolean {
         val blueprint = blueprintIdMap[blueprintId] ?: return false
         applyBlueprint(blueprint)
@@ -89,6 +105,6 @@
 
     /** Prints all available blueprints to the PrintWriter. */
     fun printBlueprints(pw: PrintWriter) {
-        blueprintIdMap.forEach { entry -> pw.println("${entry.key}") }
+        blueprintIdMap.onEachIndexed { index, entry -> pw.println("$index: ${entry.key}") }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
index 390ad7e..6ce9185 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
@@ -37,6 +37,16 @@
         return keyguardBlueprintRepository.applyBlueprint(blueprintId)
     }
 
+    /**
+     * Transitions to a blueprint.
+     *
+     * @param blueprintId
+     * @return whether the transition has succeeded.
+     */
+    fun transitionToBlueprint(blueprintId: Int): Boolean {
+        return keyguardBlueprintRepository.applyBlueprint(blueprintId)
+    }
+
     /** Re-emits the blueprint value to the collectors. */
     fun refreshBlueprint() {
         keyguardBlueprintRepository.refreshBlueprint()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
index 36d21f1..ce7ec0e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.view.layout
 
+import androidx.core.text.isDigitsOnly
 import com.android.systemui.keyguard.data.repository.KeyguardBlueprintRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.statusbar.commandline.Command
@@ -45,7 +46,11 @@
                 return
             }
 
-            if (keyguardBlueprintInteractor.transitionToBlueprint(arg)) {
+            if (
+                arg.isDigitsOnly() && keyguardBlueprintInteractor.transitionToBlueprint(arg.toInt())
+            ) {
+                pw.println("Transition succeeded!")
+            } else if (keyguardBlueprintInteractor.transitionToBlueprint(arg)) {
                 pw.println("Transition succeeded!")
             } else {
                 pw.println("Invalid argument! To see available blueprint ids, run:")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt
index 79a97fb..6534dcf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt
@@ -61,6 +61,6 @@
         )
 
     companion object {
-        const val SHORTCUTS_BESIDE_UDFPS = "shortcutsBesideUdfps"
+        const val SHORTCUTS_BESIDE_UDFPS = "shortcuts-besides-udfps"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
index 79b7157..5aba229 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
@@ -18,8 +18,6 @@
 package com.android.systemui.keyguard.ui.view.layout.sections
 
 import android.content.res.Resources
-import android.view.View
-import android.widget.ImageView
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
@@ -27,13 +25,10 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.RIGHT
 import androidx.constraintlayout.widget.ConstraintSet.TOP
-import androidx.core.content.res.ResourcesCompat
 import com.android.systemui.R
-import com.android.systemui.animation.view.LaunchableImageView
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
@@ -53,10 +48,7 @@
     private val falsingManager: FalsingManager,
     private val indicationController: KeyguardIndicationController,
     private val vibratorHelper: VibratorHelper,
-) : KeyguardSection() {
-    private var leftShortcutHandle: KeyguardQuickAffordanceViewBinder.Binding? = null
-    private var rightShortcutHandle: KeyguardQuickAffordanceViewBinder.Binding? = null
-
+) : BaseShortcutSection() {
     override fun addViews(constraintLayout: ConstraintLayout) {
         if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
             addLeftShortcut(constraintLayout)
@@ -109,67 +101,4 @@
             connect(R.id.end_button, BOTTOM, R.id.lock_icon_view, BOTTOM)
         }
     }
-
-    override fun removeViews(constraintLayout: ConstraintLayout) {
-        leftShortcutHandle?.destroy()
-        rightShortcutHandle?.destroy()
-        constraintLayout.removeView(R.id.start_button)
-        constraintLayout.removeView(R.id.end_button)
-    }
-
-    private fun addLeftShortcut(constraintLayout: ConstraintLayout) {
-        val padding =
-            constraintLayout.resources.getDimensionPixelSize(
-                R.dimen.keyguard_affordance_fixed_padding
-            )
-        val view =
-            LaunchableImageView(constraintLayout.context, null).apply {
-                id = R.id.start_button
-                scaleType = ImageView.ScaleType.FIT_CENTER
-                background =
-                    ResourcesCompat.getDrawable(
-                        context.resources,
-                        R.drawable.keyguard_bottom_affordance_bg,
-                        context.theme
-                    )
-                foreground =
-                    ResourcesCompat.getDrawable(
-                        context.resources,
-                        R.drawable.keyguard_bottom_affordance_selected_border,
-                        context.theme
-                    )
-                visibility = View.INVISIBLE
-                setPadding(padding, padding, padding, padding)
-            }
-        constraintLayout.addView(view)
-    }
-
-    private fun addRightShortcut(constraintLayout: ConstraintLayout) {
-        if (constraintLayout.findViewById<View>(R.id.end_button) != null) return
-
-        val padding =
-            constraintLayout.resources.getDimensionPixelSize(
-                R.dimen.keyguard_affordance_fixed_padding
-            )
-        val view =
-            LaunchableImageView(constraintLayout.context, null).apply {
-                id = R.id.end_button
-                scaleType = ImageView.ScaleType.FIT_CENTER
-                background =
-                    ResourcesCompat.getDrawable(
-                        context.resources,
-                        R.drawable.keyguard_bottom_affordance_bg,
-                        context.theme
-                    )
-                foreground =
-                    ResourcesCompat.getDrawable(
-                        context.resources,
-                        R.drawable.keyguard_bottom_affordance_selected_border,
-                        context.theme
-                    )
-                visibility = View.INVISIBLE
-                setPadding(padding, padding, padding, padding)
-            }
-        constraintLayout.addView(view)
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/BaseShortcutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/BaseShortcutSection.kt
new file mode 100644
index 0000000..d046a19
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/BaseShortcutSection.kt
@@ -0,0 +1,99 @@
+package com.android.systemui.keyguard.ui.view.layout.sections
+
+import android.view.View
+import android.widget.ImageView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.res.ResourcesCompat
+import com.android.systemui.R
+import com.android.systemui.animation.view.LaunchableImageView
+import com.android.systemui.keyguard.shared.model.KeyguardSection
+import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
+
+abstract class BaseShortcutSection : KeyguardSection() {
+    protected var leftShortcutHandle: KeyguardQuickAffordanceViewBinder.Binding? = null
+    protected var rightShortcutHandle: KeyguardQuickAffordanceViewBinder.Binding? = null
+
+    override fun removeViews(constraintLayout: ConstraintLayout) {
+        leftShortcutHandle?.destroy()
+        rightShortcutHandle?.destroy()
+        constraintLayout.removeView(R.id.start_button)
+        constraintLayout.removeView(R.id.end_button)
+    }
+
+    protected fun addLeftShortcut(constraintLayout: ConstraintLayout) {
+        val padding =
+            constraintLayout.resources.getDimensionPixelSize(
+                R.dimen.keyguard_affordance_fixed_padding
+            )
+        val view =
+            LaunchableImageView(constraintLayout.context, null).apply {
+                id = R.id.start_button
+                scaleType = ImageView.ScaleType.FIT_CENTER
+                background =
+                    ResourcesCompat.getDrawable(
+                        context.resources,
+                        R.drawable.keyguard_bottom_affordance_bg,
+                        context.theme
+                    )
+                foreground =
+                    ResourcesCompat.getDrawable(
+                        context.resources,
+                        R.drawable.keyguard_bottom_affordance_selected_border,
+                        context.theme
+                    )
+                visibility = View.INVISIBLE
+                setPadding(padding, padding, padding, padding)
+            }
+        constraintLayout.addView(view)
+    }
+
+    protected fun addRightShortcut(constraintLayout: ConstraintLayout) {
+        if (constraintLayout.findViewById<View>(R.id.end_button) != null) return
+
+        val padding =
+            constraintLayout.resources.getDimensionPixelSize(
+                R.dimen.keyguard_affordance_fixed_padding
+            )
+        val view =
+            LaunchableImageView(constraintLayout.context, null).apply {
+                id = R.id.end_button
+                scaleType = ImageView.ScaleType.FIT_CENTER
+                background =
+                    ResourcesCompat.getDrawable(
+                        context.resources,
+                        R.drawable.keyguard_bottom_affordance_bg,
+                        context.theme
+                    )
+                foreground =
+                    ResourcesCompat.getDrawable(
+                        context.resources,
+                        R.drawable.keyguard_bottom_affordance_selected_border,
+                        context.theme
+                    )
+                visibility = View.INVISIBLE
+                setPadding(padding, padding, padding, padding)
+            }
+        constraintLayout.addView(view)
+    }
+    /**
+     * Defines equality as same class.
+     *
+     * This is to enable set operations to be done as an optimization to blueprint transitions.
+     */
+    override fun equals(other: Any?): Boolean {
+        return other is BaseShortcutSection
+    }
+
+    /**
+     * Defines hashcode as class.
+     *
+     * This is to enable set operations to be done as an optimization to blueprint transitions.
+     */
+    override fun hashCode(): Int {
+        return KEY.hashCode()
+    }
+
+    companion object {
+        private const val KEY = "shortcuts"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
index a2db1df..13ef985 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
@@ -18,21 +18,16 @@
 package com.android.systemui.keyguard.ui.view.layout.sections
 
 import android.content.res.Resources
-import android.view.View
-import android.widget.ImageView
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
 import androidx.constraintlayout.widget.ConstraintSet.LEFT
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.RIGHT
-import androidx.core.content.res.ResourcesCompat
 import com.android.systemui.R
-import com.android.systemui.animation.view.LaunchableImageView
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
@@ -52,10 +47,7 @@
     private val falsingManager: FalsingManager,
     private val indicationController: KeyguardIndicationController,
     private val vibratorHelper: VibratorHelper,
-) : KeyguardSection() {
-    private var leftShortcutHandle: KeyguardQuickAffordanceViewBinder.Binding? = null
-    private var rightShortcutHandle: KeyguardQuickAffordanceViewBinder.Binding? = null
-
+) : BaseShortcutSection() {
     override fun addViews(constraintLayout: ConstraintLayout) {
         if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) {
             addLeftShortcut(constraintLayout)
@@ -108,67 +100,4 @@
             connect(R.id.end_button, BOTTOM, PARENT_ID, BOTTOM, verticalOffsetMargin)
         }
     }
-
-    override fun removeViews(constraintLayout: ConstraintLayout) {
-        leftShortcutHandle?.destroy()
-        rightShortcutHandle?.destroy()
-        constraintLayout.removeView(R.id.start_button)
-        constraintLayout.removeView(R.id.end_button)
-    }
-
-    private fun addLeftShortcut(constraintLayout: ConstraintLayout) {
-        val padding =
-            constraintLayout.resources.getDimensionPixelSize(
-                R.dimen.keyguard_affordance_fixed_padding
-            )
-        val view =
-            LaunchableImageView(constraintLayout.context, null).apply {
-                id = R.id.start_button
-                scaleType = ImageView.ScaleType.FIT_CENTER
-                background =
-                    ResourcesCompat.getDrawable(
-                        context.resources,
-                        R.drawable.keyguard_bottom_affordance_bg,
-                        context.theme
-                    )
-                foreground =
-                    ResourcesCompat.getDrawable(
-                        context.resources,
-                        R.drawable.keyguard_bottom_affordance_selected_border,
-                        context.theme
-                    )
-                visibility = View.INVISIBLE
-                setPadding(padding, padding, padding, padding)
-            }
-        constraintLayout.addView(view)
-    }
-
-    private fun addRightShortcut(constraintLayout: ConstraintLayout) {
-        if (constraintLayout.findViewById<View>(R.id.end_button) != null) return
-
-        val padding =
-            constraintLayout.resources.getDimensionPixelSize(
-                R.dimen.keyguard_affordance_fixed_padding
-            )
-        val view =
-            LaunchableImageView(constraintLayout.context, null).apply {
-                id = R.id.end_button
-                scaleType = ImageView.ScaleType.FIT_CENTER
-                background =
-                    ResourcesCompat.getDrawable(
-                        context.resources,
-                        R.drawable.keyguard_bottom_affordance_bg,
-                        context.theme
-                    )
-                foreground =
-                    ResourcesCompat.getDrawable(
-                        context.resources,
-                        R.drawable.keyguard_bottom_affordance_selected_border,
-                        context.theme
-                    )
-                visibility = View.INVISIBLE
-                setPadding(padding, padding, padding, padding)
-            }
-        constraintLayout.addView(view)
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 4a76dd0..2dbcbc9 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -40,6 +40,7 @@
 import android.view.IWindowSession;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.WindowInsets;
 import android.view.WindowManager;
 import android.view.WindowManager.LayoutParams;
 import android.view.WindowManagerGlobal;
@@ -409,9 +410,9 @@
     private void applyForceShowNavigationFlag(NotificationShadeWindowState state) {
         if (state.panelExpanded || state.bouncerShowing
                 || ENABLE_REMOTE_INPUT && state.remoteInputActive) {
-            mLpChanged.privateFlags |= LayoutParams.PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION;
+            mLpChanged.forciblyShownTypes |= WindowInsets.Type.navigationBars();
         } else {
-            mLpChanged.privateFlags &= ~LayoutParams.PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION;
+            mLpChanged.forciblyShownTypes &= ~WindowInsets.Type.navigationBars();
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
index 3f7512a..f1e75b1 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
@@ -76,7 +76,7 @@
             scenesProvider: Provider<Set<@JvmSuppressWildcards Scene>>,
             layoutInsetController: NotificationInsetsController,
         ): WindowRootView {
-            return if (sceneContainerFlags.isEnabled()) {
+            return if (Flags.SCENE_CONTAINER_ENABLED && sceneContainerFlags.isEnabled()) {
                 val sceneWindowRootView =
                     layoutInflater.inflate(R.layout.scene_window_root, null) as SceneWindowRootView
                 sceneWindowRootView.init(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index b797c63..b45a688 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2680,6 +2680,7 @@
                                     && mStatusBarStateController.getDozeAmount() == 1f
                                     && mWakefulnessLifecycle.getLastWakeReason()
                                     == PowerManager.WAKE_REASON_POWER_BUTTON
+                                    && mFingerprintManager.get() != null
                                     && mFingerprintManager.get().isPowerbuttonFps()
                                     && mKeyguardUpdateMonitor
                                     .getCachedIsUnlockWithFingerprintPossible(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogFactory.kt
index 3b15065..d91ca92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialogFactory.kt
@@ -48,13 +48,14 @@
      */
     fun create(
         context: Context = this.applicationContext,
+        theme: Int = SystemUIDialog.DEFAULT_THEME,
         dismissOnDeviceLock: Boolean = SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK,
     ): ComponentSystemUIDialog {
         Assert.isMainThread()
 
         return ComponentSystemUIDialog(
             context,
-            SystemUIDialog.DEFAULT_THEME,
+            theme,
             dismissOnDeviceLock,
             featureFlags,
             dialogManager,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
index 24987ab..f4cc0ed 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.java
@@ -21,7 +21,6 @@
 import static android.view.WindowInsets.Type.tappableElement;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
 
 import static com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE;
 import static com.android.systemui.util.leak.RotationUtils.ROTATION_NONE;
@@ -44,6 +43,7 @@
 import android.view.Surface;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.WindowInsets;
 import android.view.WindowManager;
 
 import com.android.internal.policy.SystemBarUtils;
@@ -361,9 +361,9 @@
                 || state.mIsLaunchAnimationRunning
                 // Don't force-show the status bar if the user has already dismissed it.
                 || state.mOngoingProcessRequiresStatusBarVisible) {
-            mLpChanged.privateFlags |= PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
+            mLpChanged.forciblyShownTypes |= WindowInsets.Type.statusBars();
         } else {
-            mLpChanged.privateFlags &= ~PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
+            mLpChanged.forciblyShownTypes &= ~WindowInsets.Type.statusBars();
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
index 239e317..ed9ae5e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
@@ -29,6 +29,7 @@
 import com.android.systemui.biometrics.shared.model.SensorStrength
 import com.android.systemui.coroutines.collectLastValue
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
@@ -44,6 +45,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
 class FingerprintRepositoryImplTest : SysuiTestCase() {
@@ -73,10 +75,15 @@
     @Test
     fun initializeProperties() =
         testScope.runTest {
-            val isInitialized = collectLastValue(repository.isInitialized)
+            val sensorId by collectLastValue(repository.sensorId)
+            val strength by collectLastValue(repository.strength)
+            val sensorType by collectLastValue(repository.sensorType)
+            val sensorLocations by collectLastValue(repository.sensorLocations)
 
-            assertDefaultProperties()
-            assertThat(isInitialized()).isFalse()
+            // Assert default properties.
+            assertThat(sensorId).isEqualTo(-1)
+            assertThat(strength).isEqualTo(SensorStrength.CONVENIENCE)
+            assertThat(sensorType).isEqualTo(FingerprintSensorType.UNKNOWN)
 
             val fingerprintProps =
                 listOf(
@@ -115,31 +122,24 @@
 
             fingerprintAuthenticatorsCaptor.value.onAllAuthenticatorsRegistered(fingerprintProps)
 
-            assertThat(repository.sensorId.value).isEqualTo(1)
-            assertThat(repository.strength.value).isEqualTo(SensorStrength.STRONG)
-            assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.REAR)
+            assertThat(sensorId).isEqualTo(1)
+            assertThat(strength).isEqualTo(SensorStrength.STRONG)
+            assertThat(sensorType).isEqualTo(FingerprintSensorType.REAR)
 
-            assertThat(repository.sensorLocations.value.size).isEqualTo(2)
-            assertThat(repository.sensorLocations.value).containsKey("display_id_1")
-            with(repository.sensorLocations.value["display_id_1"]!!) {
+            assertThat(sensorLocations?.size).isEqualTo(2)
+            assertThat(sensorLocations).containsKey("display_id_1")
+            with(sensorLocations?.get("display_id_1")!!) {
                 assertThat(displayId).isEqualTo("display_id_1")
                 assertThat(sensorLocationX).isEqualTo(100)
                 assertThat(sensorLocationY).isEqualTo(300)
                 assertThat(sensorRadius).isEqualTo(20)
             }
-            assertThat(repository.sensorLocations.value).containsKey("")
-            with(repository.sensorLocations.value[""]!!) {
+            assertThat(sensorLocations).containsKey("")
+            with(sensorLocations?.get("")!!) {
                 assertThat(displayId).isEqualTo("")
                 assertThat(sensorLocationX).isEqualTo(540)
                 assertThat(sensorLocationY).isEqualTo(1636)
                 assertThat(sensorRadius).isEqualTo(130)
             }
-            assertThat(isInitialized()).isTrue()
         }
-
-    private fun assertDefaultProperties() {
-        assertThat(repository.sensorId.value).isEqualTo(-1)
-        assertThat(repository.strength.value).isEqualTo(SensorStrength.CONVENIENCE)
-        assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.UNKNOWN)
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
index fd96cf4..712eef1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.coroutines.collectLastValue
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
@@ -51,7 +52,7 @@
     }
 
     @Test
-    fun testGetOverlayOffsets() =
+    fun testOverlayOffsetUpdates() =
         testScope.runTest {
             fingerprintRepository.setProperties(
                 sensorId = 1,
@@ -76,16 +77,32 @@
                     )
             )
 
-            var offsets = interactor.getOverlayOffsets("display_id_1")
-            assertThat(offsets.displayId).isEqualTo("display_id_1")
-            assertThat(offsets.sensorLocationX).isEqualTo(100)
-            assertThat(offsets.sensorLocationY).isEqualTo(300)
-            assertThat(offsets.sensorRadius).isEqualTo(20)
+            val displayId by collectLastValue(interactor.displayId)
+            val offsets by collectLastValue(interactor.overlayOffsets)
 
-            offsets = interactor.getOverlayOffsets("invalid_display_id")
-            assertThat(offsets.displayId).isEqualTo("")
-            assertThat(offsets.sensorLocationX).isEqualTo(540)
-            assertThat(offsets.sensorLocationY).isEqualTo(1636)
-            assertThat(offsets.sensorRadius).isEqualTo(130)
+            // Assert offsets of empty displayId.
+            assertThat(displayId).isEqualTo("")
+            assertThat(offsets?.displayId).isEqualTo("")
+            assertThat(offsets?.sensorLocationX).isEqualTo(540)
+            assertThat(offsets?.sensorLocationY).isEqualTo(1636)
+            assertThat(offsets?.sensorRadius).isEqualTo(130)
+
+            // Offsets should be updated correctly.
+            interactor.onDisplayChanged("display_id_1")
+            assertThat(displayId).isEqualTo("display_id_1")
+            assertThat(offsets?.displayId).isEqualTo("display_id_1")
+            assertThat(offsets?.sensorLocationX).isEqualTo(100)
+            assertThat(offsets?.sensorLocationY).isEqualTo(300)
+            assertThat(offsets?.sensorRadius).isEqualTo(20)
+
+            // Should return default offset when the displayId is invalid.
+            interactor.onDisplayChanged("invalid_display_id")
+            assertThat(displayId).isEqualTo("invalid_display_id")
+            assertThat(offsets?.displayId).isEqualTo(SensorLocationInternal.DEFAULT.displayId)
+            assertThat(offsets?.sensorLocationX)
+                .isEqualTo(SensorLocationInternal.DEFAULT.sensorLocationX)
+            assertThat(offsets?.sensorLocationY)
+                .isEqualTo(SensorLocationInternal.DEFAULT.sensorLocationY)
+            assertThat(offsets?.sensorRadius).isEqualTo(SensorLocationInternal.DEFAULT.sensorRadius)
         }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
index bb73dc6..dbf6a29 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
@@ -81,4 +81,10 @@
         command().execute(pw, listOf("fake"))
         verify(keyguardBlueprintInteractor).transitionToBlueprint("fake")
     }
+
+    @Test
+    fun testValidArg_Int() {
+        command().execute(pw, listOf("1"))
+        verify(keyguardBlueprintInteractor).transitionToBlueprint(1)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 137566b..bd3fb9f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -399,7 +399,6 @@
 
         when(mGradientColors.supportsDarkText()).thenReturn(true);
         when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
-        ConfigurationController configurationController = new ConfigurationControllerImpl(mContext);
 
         when(mLockscreenWallpaperLazy.get()).thenReturn(mLockscreenWallpaper);
         when(mBiometricUnlockControllerLazy.get()).thenReturn(mBiometricUnlockController);
@@ -438,6 +437,11 @@
         when(mUserTracker.getUserHandle()).thenReturn(
                 UserHandle.of(ActivityManager.getCurrentUser()));
 
+        createCentralSurfaces();
+    }
+
+    private void createCentralSurfaces() {
+        ConfigurationController configurationController = new ConfigurationControllerImpl(mContext);
         mCentralSurfaces = new CentralSurfacesImpl(
                 mContext,
                 mNotificationsController,
@@ -1083,6 +1087,27 @@
         verify(mNotificationPanelViewController).setTouchAndAnimationDisabled(true);
     }
 
+    /** Regression test for b/298355063 */
+    @Test
+    public void fingerprintManagerNull_noNPE() {
+        // GIVEN null fingerprint manager
+        mFingerprintManager = null;
+        createCentralSurfaces();
+
+        // GIVEN should animate doze wakeup
+        when(mDozeServiceHost.shouldAnimateWakeup()).thenReturn(true);
+        when(mBiometricUnlockController.getMode()).thenReturn(
+                BiometricUnlockController.MODE_ONLY_WAKE);
+        when(mDozeServiceHost.isPulsing()).thenReturn(false);
+        when(mStatusBarStateController.getDozeAmount()).thenReturn(1f);
+
+        // WHEN waking up from the power button
+        mWakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
+        mCentralSurfaces.mWakefulnessObserver.onStartedWakingUp();
+
+        // THEN no NPE when fingerprintManager is null
+    }
+
     /**
      * Configures the appropriate mocks and then calls {@link CentralSurfacesImpl#updateIsKeyguard}
      * to reconfigure the keyguard to reflect the requested showing/occluded states.
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
index 2362a52..0c5e438 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
@@ -20,16 +20,12 @@
 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import com.android.systemui.biometrics.shared.model.SensorStrength
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
 class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
 
-    private val _isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
-    override val isInitialized = _isInitialized.asStateFlow()
-
     private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1)
-    override val sensorId: StateFlow<Int> = _sensorId.asStateFlow()
+    override val sensorId = _sensorId.asStateFlow()
 
     private val _strength: MutableStateFlow<SensorStrength> =
         MutableStateFlow(SensorStrength.CONVENIENCE)
@@ -37,12 +33,11 @@
 
     private val _sensorType: MutableStateFlow<FingerprintSensorType> =
         MutableStateFlow(FingerprintSensorType.UNKNOWN)
-    override val sensorType: StateFlow<FingerprintSensorType> = _sensorType.asStateFlow()
+    override val sensorType = _sensorType.asStateFlow()
 
     private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> =
         MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT))
-    override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> =
-        _sensorLocations.asStateFlow()
+    override val sensorLocations = _sensorLocations.asStateFlow()
 
     fun setProperties(
         sensorId: Int,
@@ -54,6 +49,5 @@
         _strength.value = strength
         _sensorType.value = sensorType
         _sensorLocations.value = sensorLocations
-        _isInitialized.value = true
     }
 }
diff --git a/services/accessibility/Android.bp b/services/accessibility/Android.bp
index bf8a9af..e9bb763 100644
--- a/services/accessibility/Android.bp
+++ b/services/accessibility/Android.bp
@@ -27,4 +27,20 @@
         "services.core",
         "androidx.annotation_annotation",
     ],
+    static_libs: [
+        "com_android_server_accessibility_flags_lib",
+    ],
+}
+
+aconfig_declarations {
+    name: "com_android_server_accessibility_flags",
+    package: "com.android.server.accessibility",
+    srcs: [
+        "accessibility.aconfig",
+    ],
+}
+
+java_aconfig_library {
+    name: "com_android_server_accessibility_flags_lib",
+    aconfig_declarations: "com_android_server_accessibility_flags",
 }
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
new file mode 100644
index 0000000..b5fc2b6
--- /dev/null
+++ b/services/accessibility/accessibility.aconfig
@@ -0,0 +1,7 @@
+package: "com.android.server.accessibility"
+flag {
+    name: "proxy_use_apps_on_virtual_device_listener"
+    namespace: "accessibility"
+    description: "Fixes race condition described in b/286587811"
+    bug: "286587811"
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
index 119f575..ed77476 100644
--- a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
@@ -33,6 +33,7 @@
 import android.os.IBinder;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
+import android.util.ArraySet;
 import android.util.IntArray;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -42,6 +43,7 @@
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.IAccessibilityManagerClient;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IntPair;
 import com.android.server.LocalServices;
 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
@@ -96,6 +98,9 @@
 
     private final SystemSupport mSystemSupport;
 
+    private VirtualDeviceManagerInternal.AppsOnVirtualDeviceListener
+            mAppsOnVirtualDeviceListener;
+
     /**
      * Callbacks into AccessibilityManagerService.
      */
@@ -174,6 +179,16 @@
 
         synchronized (mLock) {
             mProxyA11yServiceConnections.put(displayId, connection);
+            if (Flags.proxyUseAppsOnVirtualDeviceListener()) {
+                if (mAppsOnVirtualDeviceListener == null) {
+                    mAppsOnVirtualDeviceListener = allRunningUids ->
+                            notifyProxyOfRunningAppsChange(allRunningUids);
+                    final VirtualDeviceManagerInternal localVdm = getLocalVdm();
+                    if (localVdm != null) {
+                        localVdm.registerAppsOnVirtualDeviceListener(mAppsOnVirtualDeviceListener);
+                    }
+                }
+            }
         }
 
         // If the client dies, make sure to remove the connection.
@@ -276,11 +291,21 @@
                         }
                     }
                 });
-        // If there isn't an existing proxy for the device id, reset clients. Resetting
+        // If there isn't an existing proxy for the device id, reset app clients. Resetting
         // will usually happen, since in most cases there will only be one proxy for a
         // device.
         if (!isProxyedDeviceId(deviceId)) {
             synchronized (mLock) {
+                if (Flags.proxyUseAppsOnVirtualDeviceListener()) {
+                    if (mProxyA11yServiceConnections.size() == 0) {
+                        final VirtualDeviceManagerInternal localVdm = getLocalVdm();
+                        if (localVdm != null && mAppsOnVirtualDeviceListener != null) {
+                            localVdm.unregisterAppsOnVirtualDeviceListener(
+                                    mAppsOnVirtualDeviceListener);
+                            mAppsOnVirtualDeviceListener = null;
+                        }
+                    }
+                }
                 mSystemSupport.removeDeviceIdLocked(deviceId);
                 mLastStates.delete(deviceId);
             }
@@ -307,7 +332,7 @@
      * Returns {@code true} if {@code deviceId} is being proxy-ed.
      */
     public boolean isProxyedDeviceId(int deviceId) {
-        if (deviceId == DEVICE_ID_DEFAULT && deviceId == DEVICE_ID_INVALID) {
+        if (deviceId == DEVICE_ID_DEFAULT || deviceId == DEVICE_ID_INVALID) {
             return false;
         }
         boolean isTrackingDeviceId;
@@ -566,7 +591,7 @@
      * This is similar to onUserStateChangeLocked and onClientChangeLocked, but does not require an
      * A11yUserState and only checks proxy-relevant settings.
      */
-    public void onProxyChanged(int deviceId) {
+    private void onProxyChanged(int deviceId, boolean forceUpdate) {
         if (DEBUG) {
             Slog.v(LOG_TAG, "onProxyChanged called for deviceId: " + deviceId);
         }
@@ -584,7 +609,7 @@
             // Calls A11yManager#setRelevantEventTypes (test these)
             updateRelevantEventTypesLocked(deviceId);
             // Calls A11yManager#setState
-            scheduleUpdateProxyClientsIfNeededLocked(deviceId);
+            scheduleUpdateProxyClientsIfNeededLocked(deviceId, forceUpdate);
             //Calls A11yManager#notifyServicesStateChanged(timeout)
             scheduleNotifyProxyClientsOfServicesStateChangeLocked(deviceId);
             // Calls A11yManager#setFocusAppearance
@@ -594,16 +619,25 @@
     }
 
     /**
+     * Handles proxy changes, but does not force an update of app clients.
+     */
+    public void onProxyChanged(int deviceId) {
+        onProxyChanged(deviceId, false);
+    }
+
+    /**
      * Updates the states of the app AccessibilityManagers.
      */
-    private void scheduleUpdateProxyClientsIfNeededLocked(int deviceId) {
+    private void scheduleUpdateProxyClientsIfNeededLocked(int deviceId, boolean forceUpdate) {
         final int proxyState = getStateLocked(deviceId);
         if (DEBUG) {
             Slog.v(LOG_TAG, "State for device id " + deviceId + " is " + proxyState);
             Slog.v(LOG_TAG, "Last state for device id " + deviceId + " is "
                     + getLastSentStateLocked(deviceId));
+            Slog.v(LOG_TAG, "force update: " + forceUpdate);
         }
-        if ((getLastSentStateLocked(deviceId)) != proxyState) {
+        if ((getLastSentStateLocked(deviceId)) != proxyState
+                || (Flags.proxyUseAppsOnVirtualDeviceListener() && forceUpdate)) {
             setLastStateLocked(deviceId, proxyState);
             mMainHandler.post(() -> {
                 synchronized (mLock) {
@@ -792,7 +826,7 @@
     }
 
     /**
-     * Updates the device ids of IAccessibilityManagerClients if needed.
+     * Updates the device ids of IAccessibilityManagerClients if needed after a proxy change.
      */
     private void updateDeviceIdsIfNeededLocked(int deviceId,
             @NonNull RemoteCallbackList<IAccessibilityManagerClient> clients) {
@@ -804,13 +838,66 @@
         for (int i = 0; i < clients.getRegisteredCallbackCount(); i++) {
             final AccessibilityManagerService.Client client =
                     ((AccessibilityManagerService.Client) clients.getRegisteredCallbackCookie(i));
-            if (deviceId != DEVICE_ID_DEFAULT && deviceId != DEVICE_ID_INVALID
-                    && localVdm.getDeviceIdsForUid(client.mUid).contains(deviceId)) {
-                if (DEBUG) {
-                    Slog.v(LOG_TAG, "Packages moved to device id " + deviceId + " are "
-                            + Arrays.toString(client.mPackageNames));
+            if (Flags.proxyUseAppsOnVirtualDeviceListener()) {
+                if (deviceId == DEVICE_ID_DEFAULT || deviceId == DEVICE_ID_INVALID) {
+                    continue;
                 }
-                client.mDeviceId = deviceId;
+                boolean uidBelongsToDevice =
+                        localVdm.getDeviceIdsForUid(client.mUid).contains(deviceId);
+                if (client.mDeviceId != deviceId && uidBelongsToDevice) {
+                    if (DEBUG) {
+                        Slog.v(LOG_TAG, "Packages moved to device id " + deviceId + " are "
+                                + Arrays.toString(client.mPackageNames));
+                    }
+                    client.mDeviceId = deviceId;
+                } else if (client.mDeviceId == deviceId && !uidBelongsToDevice) {
+                    client.mDeviceId = DEVICE_ID_DEFAULT;
+                    if (DEBUG) {
+                        Slog.v(LOG_TAG, "Packages moved to the default device from device id "
+                                + deviceId + " are " + Arrays.toString(client.mPackageNames));
+                    }
+                }
+            } else {
+                if (deviceId != DEVICE_ID_DEFAULT && deviceId != DEVICE_ID_INVALID
+                    && localVdm.getDeviceIdsForUid(client.mUid).contains(deviceId)) {
+                    if (DEBUG) {
+                        Slog.v(LOG_TAG, "Packages moved to device id " + deviceId + " are "
+                                + Arrays.toString(client.mPackageNames));
+                    }
+                    client.mDeviceId = deviceId;
+                }
+            }
+        }
+    }
+
+    @VisibleForTesting
+    void notifyProxyOfRunningAppsChange(Set<Integer> allRunningUids) {
+        if (DEBUG) {
+            Slog.v(LOG_TAG, "notifyProxyOfRunningAppsChange: " + allRunningUids);
+        }
+        synchronized (mLock) {
+            if (mProxyA11yServiceConnections.size() == 0) {
+                return;
+            }
+            final VirtualDeviceManagerInternal localVdm = getLocalVdm();
+            if  (localVdm == null) {
+                return;
+            }
+            final ArraySet<Integer> deviceIdsToUpdate = new ArraySet<>();
+            for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) {
+                final ProxyAccessibilityServiceConnection proxy =
+                        mProxyA11yServiceConnections.valueAt(i);
+                if (proxy != null) {
+                    final int proxyDeviceId = proxy.getDeviceId();
+                    for (Integer uid : allRunningUids) {
+                        if (localVdm.getDeviceIdsForUid(uid).contains(proxyDeviceId)) {
+                            deviceIdsToUpdate.add(proxyDeviceId);
+                        }
+                    }
+                }
+            }
+            for (Integer proxyDeviceId : deviceIdsToUpdate) {
+                onProxyChanged(proxyDeviceId, true);
             }
         }
     }
@@ -843,6 +930,11 @@
         return mLocalVdm;
     }
 
+    @VisibleForTesting
+    void setLocalVirtualDeviceManager(VirtualDeviceManagerInternal localVdm) {
+        mLocalVdm = localVdm;
+    }
+
     /**
      * Prints information belonging to each display that is controlled by an
      * AccessibilityDisplayProxy.
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java
index 39756df..cae047f 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceShellCommand.java
@@ -18,7 +18,6 @@
 
 import static android.service.autofill.AutofillFieldClassificationService.EXTRA_SCORES;
 import static android.service.autofill.AutofillService.EXTRA_RESULT;
-
 import static com.android.server.autofill.AutofillManagerService.RECEIVER_BUNDLE_EXTRA_SESSIONS;
 
 import android.os.Bundle;
@@ -155,20 +154,31 @@
             pw.println("");
         }
 
+        Method[] flagMethods = {};
+
         try {
-            Method[] flagMethods = Flags.class.getMethods();
-            // For some reason, unreferenced flags do not show up here
-            // Maybe compiler optomized them out of bytecode?
-            for (Method method : flagMethods) {
-                if (Modifier.isPublic(method.getModifiers())) {
-                    pw.println(method.getName() + ": " + method.invoke(null));
-                }
-            }
-        } catch (Exception ex) {
-            pw.println(ex);
+            flagMethods = Flags.class.getDeclaredMethods();
+        } catch (SecurityException ex) {
+            ex.printStackTrace(pw);
             return -1;
         }
 
+        // For some reason, unreferenced flags do not show up here
+        // Maybe compiler optomized them out of bytecode?
+        for (Method method : flagMethods) {
+            if (!Modifier.isPublic(method.getModifiers())) {
+                continue;
+            }
+            try {
+                pw.print(method.getName() + ": ");
+                pw.print(method.invoke(null));
+            } catch (Exception ex) {
+                ex.printStackTrace(pw);
+            } finally {
+                pw.println("");
+            }
+        }
+
         return 0;
     }
 
diff --git a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
index b07a0bb..102c262 100644
--- a/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
+++ b/services/companion/java/com/android/server/companion/virtual/GenericWindowPolicyController.java
@@ -47,7 +47,6 @@
 
 import java.util.Set;
 
-
 /**
  * A controller to control the policies of the windows that can be displayed on the virtual display.
  */
@@ -106,12 +105,14 @@
     public static final long ALLOW_SECURE_ACTIVITY_DISPLAY_ON_REMOTE_DEVICE = 201712607L;
     @NonNull
     private final ArraySet<UserHandle> mAllowedUsers;
-    private final boolean mActivityLaunchAllowedByDefault;
+    @GuardedBy("mGenericWindowPolicyControllerLock")
+    private boolean mActivityLaunchAllowedByDefault;
     @NonNull
-    private final ArraySet<ComponentName> mActivityPolicyExceptions;
+    @GuardedBy("mGenericWindowPolicyControllerLock")
+    private final Set<ComponentName> mActivityPolicyExemptions;
     private final boolean mCrossTaskNavigationAllowedByDefault;
     @NonNull
-    private final ArraySet<ComponentName> mCrossTaskNavigationExceptions;
+    private final ArraySet<ComponentName> mCrossTaskNavigationExemptions;
     private final Object mGenericWindowPolicyControllerLock = new Object();
     @Nullable private final ActivityBlockedCallback mActivityBlockedCallback;
     private int mDisplayId = Display.INVALID_DISPLAY;
@@ -142,11 +143,11 @@
      * @param allowedUsers The set of users that are allowed to stream in this display.
      * @param activityLaunchAllowedByDefault Whether activities are default allowed to be launched
      *   or blocked.
-     * @param activityPolicyExceptions The set of activities explicitly exempt from the default
+     * @param activityPolicyExemptions The set of activities explicitly exempt from the default
      *   activity policy.
      * @param crossTaskNavigationAllowedByDefault Whether cross task navigations are allowed by
      *   default or not.
-     * @param crossTaskNavigationExceptions The set of components explicitly exempt from the default
+     * @param crossTaskNavigationExemptions The set of components explicitly exempt from the default
      *   navigation policy.
      * @param activityListener Activity listener to listen for activity changes.
      * @param activityBlockedCallback Callback that is called when an activity is blocked from
@@ -157,12 +158,14 @@
      *   passed in filters.
      * @param showTasksInHostDeviceRecents whether to show activities in recents on the host device.
      */
-    public GenericWindowPolicyController(int windowFlags, int systemWindowFlags,
+    public GenericWindowPolicyController(
+            int windowFlags,
+            int systemWindowFlags,
             @NonNull ArraySet<UserHandle> allowedUsers,
             boolean activityLaunchAllowedByDefault,
-            @NonNull Set<ComponentName> activityPolicyExceptions,
+            @NonNull Set<ComponentName> activityPolicyExemptions,
             boolean crossTaskNavigationAllowedByDefault,
-            @NonNull Set<ComponentName> crossTaskNavigationExceptions,
+            @NonNull Set<ComponentName> crossTaskNavigationExemptions,
             @Nullable ActivityListener activityListener,
             @Nullable PipBlockedCallback pipBlockedCallback,
             @Nullable ActivityBlockedCallback activityBlockedCallback,
@@ -173,9 +176,9 @@
         super();
         mAllowedUsers = allowedUsers;
         mActivityLaunchAllowedByDefault = activityLaunchAllowedByDefault;
-        mActivityPolicyExceptions = new ArraySet<>(activityPolicyExceptions);
+        mActivityPolicyExemptions = activityPolicyExemptions;
         mCrossTaskNavigationAllowedByDefault = crossTaskNavigationAllowedByDefault;
-        mCrossTaskNavigationExceptions = new ArraySet<>(crossTaskNavigationExceptions);
+        mCrossTaskNavigationExemptions = new ArraySet<>(crossTaskNavigationExemptions);
         mActivityBlockedCallback = activityBlockedCallback;
         setInterestedWindowFlags(windowFlags, systemWindowFlags);
         mActivityListener = activityListener;
@@ -202,6 +205,24 @@
         }
     }
 
+    void setActivityLaunchDefaultAllowed(boolean activityLaunchDefaultAllowed) {
+        synchronized (mGenericWindowPolicyControllerLock) {
+            mActivityLaunchAllowedByDefault = activityLaunchDefaultAllowed;
+        }
+    }
+
+    void addActivityPolicyExemption(@NonNull ComponentName componentName) {
+        synchronized (mGenericWindowPolicyControllerLock) {
+            mActivityPolicyExemptions.add(componentName);
+        }
+    }
+
+    void removeActivityPolicyExemption(@NonNull ComponentName componentName) {
+        synchronized (mGenericWindowPolicyControllerLock) {
+            mActivityPolicyExemptions.remove(componentName);
+        }
+    }
+
     /** Register a listener for running applications changes. */
     public void registerRunningAppsChangedListener(@NonNull RunningAppsChangedListener listener) {
         synchronized (mGenericWindowPolicyControllerLock) {
@@ -265,14 +286,17 @@
                     + mDisplayCategories);
             return false;
         }
-        if (!isAllowedByPolicy(mActivityLaunchAllowedByDefault, mActivityPolicyExceptions,
-                activityComponent)) {
-            Slog.d(TAG, "Virtual device launch disallowed by policy: " + activityComponent);
-            return false;
+        synchronized (mGenericWindowPolicyControllerLock) {
+            if (!isAllowedByPolicy(mActivityLaunchAllowedByDefault, mActivityPolicyExemptions,
+                    activityComponent)) {
+                Slog.d(TAG, "Virtual device launch disallowed by policy: "
+                        + activityComponent);
+                return false;
+            }
         }
         if (isNewTask && launchingFromDisplayId != DEFAULT_DISPLAY
                 && !isAllowedByPolicy(mCrossTaskNavigationAllowedByDefault,
-                        mCrossTaskNavigationExceptions, activityComponent)) {
+                        mCrossTaskNavigationExemptions, activityComponent)) {
             Slog.d(TAG, "Virtual device cross task navigation disallowed by policy: "
                     + activityComponent);
             return false;
@@ -378,11 +402,11 @@
                     && mDisplayCategories.contains(activityInfo.requiredDisplayCategory);
     }
 
-    private boolean isAllowedByPolicy(boolean allowedByDefault, ArraySet<ComponentName> exceptions,
-            ComponentName component) {
-        // Either allowed and the exceptions do not contain the component,
-        // or disallowed and the exceptions contain the component.
-        return allowedByDefault != exceptions.contains(component);
+    private static boolean isAllowedByPolicy(boolean allowedByDefault,
+            Set<ComponentName> exemptions, ComponentName component) {
+        // Either allowed and the exemptions do not contain the component,
+        // or disallowed and the exemptions contain the component.
+        return allowedByDefault != exemptions.contains(component);
     }
 
     @VisibleForTesting
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 8f765e4..3b13410 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -22,6 +22,7 @@
 import static android.companion.virtual.VirtualDeviceParams.ACTIVITY_POLICY_DEFAULT_ALLOWED;
 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
 import static android.companion.virtual.VirtualDeviceParams.NAVIGATION_POLICY_DEFAULT_ALLOWED;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
 import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
@@ -93,7 +94,6 @@
 import android.view.WindowManager;
 import android.widget.Toast;
 
-
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.BlockedAppStreamingActivity;
@@ -174,11 +174,15 @@
     @NonNull
     private final VirtualDevice mPublicVirtualDeviceObject;
 
+    @GuardedBy("mVirtualDeviceLock")
+    @NonNull
+    private final Set<ComponentName> mActivityPolicyExemptions;
+
     private ActivityListener createListenerAdapter() {
         return new ActivityListener() {
 
             @Override
-            public void onTopActivityChanged(int displayId, ComponentName topActivity) {
+            public void onTopActivityChanged(int displayId, @NonNull ComponentName topActivity) {
                 try {
                     mActivityListener.onTopActivityChanged(displayId, topActivity,
                             UserHandle.USER_NULL);
@@ -188,7 +192,7 @@
             }
 
             @Override
-            public void onTopActivityChanged(int displayId, ComponentName topActivity,
+            public void onTopActivityChanged(int displayId, @NonNull ComponentName topActivity,
                     @UserIdInt int userId) {
                 try {
                     mActivityListener.onTopActivityChanged(displayId, topActivity, userId);
@@ -295,6 +299,18 @@
 
         mPublicVirtualDeviceObject = new VirtualDevice(
                 this, getDeviceId(), getPersistentDeviceId(), mParams.getName());
+
+        if (Flags.dynamicPolicy()) {
+            mActivityPolicyExemptions = new ArraySet<>(
+                    mParams.getDevicePolicy(POLICY_TYPE_ACTIVITY) == DEVICE_POLICY_DEFAULT
+                            ? mParams.getBlockedActivities()
+                            : mParams.getAllowedActivities());
+        } else {
+            mActivityPolicyExemptions =
+                    mParams.getDefaultActivityPolicy() == ACTIVITY_POLICY_DEFAULT_ALLOWED
+                            ? mParams.getBlockedActivities()
+                            : mParams.getAllowedActivities();
+        }
     }
 
     @VisibleForTesting
@@ -414,6 +430,34 @@
         }
     }
 
+    @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void addActivityPolicyExemption(@NonNull ComponentName componentName) {
+        super.addActivityPolicyExemption_enforcePermission();
+        synchronized (mVirtualDeviceLock) {
+            if (mActivityPolicyExemptions.add(componentName)) {
+                for (int i = 0; i < mVirtualDisplays.size(); i++) {
+                    mVirtualDisplays.valueAt(i).getWindowPolicyController()
+                            .addActivityPolicyExemption(componentName);
+                }
+            }
+        }
+    }
+
+    @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void removeActivityPolicyExemption(@NonNull ComponentName componentName) {
+        super.removeActivityPolicyExemption_enforcePermission();
+        synchronized (mVirtualDeviceLock) {
+            if (mActivityPolicyExemptions.remove(componentName)) {
+                for (int i = 0; i < mVirtualDisplays.size(); i++) {
+                    mVirtualDisplays.valueAt(i).getWindowPolicyController()
+                            .removeActivityPolicyExemption(componentName);
+                }
+            }
+        }
+    }
+
     private void sendPendingIntent(int displayId, PendingIntent pendingIntent)
             throws PendingIntent.CanceledException {
         final ActivityOptions options = ActivityOptions.makeBasic().setLaunchDisplayId(displayId);
@@ -543,6 +587,16 @@
                     }
                 }
                 break;
+            case POLICY_TYPE_ACTIVITY:
+                synchronized (mVirtualDeviceLock) {
+                    mDevicePolicies.put(policyType, devicePolicy);
+                    for (int i = 0; i < mVirtualDisplays.size(); i++) {
+                        mVirtualDisplays.valueAt(i).getWindowPolicyController()
+                                .setActivityLaunchDefaultAllowed(
+                                        devicePolicy == DEVICE_POLICY_DEFAULT);
+                    }
+                }
+                break;
             default:
                 throw new IllegalArgumentException("Device policy " + policyType
                         + " cannot be changed at runtime. ");
@@ -840,24 +894,26 @@
         mSensorController.dump(fout);
     }
 
-    private GenericWindowPolicyController createWindowPolicyController(
+    @GuardedBy("mVirtualDeviceLock")
+    private GenericWindowPolicyController createWindowPolicyControllerLocked(
             @NonNull Set<String> displayCategories) {
         final boolean activityLaunchAllowedByDefault =
-                mParams.getDefaultActivityPolicy() == ACTIVITY_POLICY_DEFAULT_ALLOWED;
+                Flags.dynamicPolicy()
+                        ? getDevicePolicy(POLICY_TYPE_ACTIVITY) == DEVICE_POLICY_DEFAULT
+                        : mParams.getDefaultActivityPolicy() == ACTIVITY_POLICY_DEFAULT_ALLOWED;
         final boolean crossTaskNavigationAllowedByDefault =
                 mParams.getDefaultNavigationPolicy() == NAVIGATION_POLICY_DEFAULT_ALLOWED;
         final boolean showTasksInHostDeviceRecents =
-                mParams.getDevicePolicy(POLICY_TYPE_RECENTS) == DEVICE_POLICY_DEFAULT;
+                getDevicePolicy(POLICY_TYPE_RECENTS) == DEVICE_POLICY_DEFAULT;
 
         final GenericWindowPolicyController gwpc = new GenericWindowPolicyController(
                 FLAG_SECURE,
                 SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
                 getAllowedUserHandles(),
                 activityLaunchAllowedByDefault,
-                /*activityPolicyExceptions=*/activityLaunchAllowedByDefault
-                        ? mParams.getBlockedActivities() : mParams.getAllowedActivities(),
+                mActivityPolicyExemptions,
                 crossTaskNavigationAllowedByDefault,
-                /*crossTaskNavigationExceptions=*/crossTaskNavigationAllowedByDefault
+                /*crossTaskNavigationExemptions=*/crossTaskNavigationAllowedByDefault
                         ? mParams.getBlockedCrossTaskNavigations()
                         : mParams.getAllowedCrossTaskNavigations(),
                 createListenerAdapter(),
@@ -873,8 +929,10 @@
 
     int createVirtualDisplay(@NonNull VirtualDisplayConfig virtualDisplayConfig,
             @NonNull IVirtualDisplayCallback callback, String packageName) {
-        GenericWindowPolicyController gwpc = createWindowPolicyController(
-                virtualDisplayConfig.getDisplayCategories());
+        GenericWindowPolicyController gwpc;
+        synchronized (mVirtualDeviceLock) {
+            gwpc = createWindowPolicyControllerLocked(virtualDisplayConfig.getDisplayCategories());
+        }
         DisplayManagerInternal displayManager = LocalServices.getService(
                 DisplayManagerInternal.class);
         int displayId;
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 7329f1a..b941aaf 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -180,7 +180,6 @@
         "android.hardware.rebootescrow-V1-java",
         "android.hardware.power.stats-V2-java",
         "android.hidl.manager-V1.2-java",
-        "com.android.server.security.flags-aconfig-java",
         "cbor-java",
         "display_flags_lib",
         "icu4j_calendar_astronomer",
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 0d265a0..b5911f6 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -9543,6 +9543,14 @@
             }
         } else {
             worker.start();
+            if (process != null && process.mPid == MY_PID && "crash".equals(eventType)) {
+                // We're actually crashing, let's wait for up to 2 seconds before killing ourselves,
+                // so the data could be persisted into the dropbox.
+                try {
+                    worker.join(2000);
+                } catch (InterruptedException ignored) {
+                }
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index d3176ee4..87077a6 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -2823,9 +2823,7 @@
                 }
             }
 
-            if (schedGroup < SCHED_GROUP_TOP_APP
-                    && cr.hasFlag(Context.BIND_SCHEDULE_LIKE_TOP_APP)
-                    && clientIsSystem) {
+            if (cr.hasFlag(Context.BIND_SCHEDULE_LIKE_TOP_APP) && clientIsSystem) {
                 schedGroup = SCHED_GROUP_TOP_APP;
                 state.setScheduleLikeTopApp(true);
             }
diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
index fdf607d..64691e0 100644
--- a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
+++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
@@ -53,6 +53,9 @@
     static final int MAXIMUM_ENROLLMENT_NOTIFICATIONS = 1;
 
     @NonNull private final Context mContext;
+    @NonNull private final PackageManager mPackageManager;
+    @NonNull private final FaceManager mFaceManager;
+    @NonNull private final FingerprintManager mFingerprintManager;
 
     private final float mThreshold;
     private final int mModality;
@@ -86,6 +89,10 @@
         mModality = modality;
         mBiometricNotification = biometricNotification;
 
+        mPackageManager = context.getPackageManager();
+        mFaceManager = mContext.getSystemService(FaceManager.class);
+        mFingerprintManager = mContext.getSystemService(FingerprintManager.class);
+
         IntentFilter intentFilter = new IntentFilter();
         intentFilter.addAction(Intent.ACTION_USER_REMOVED);
         context.registerReceiver(mBroadcastReceiver, intentFilter);
@@ -108,6 +115,13 @@
 
     /** Update total authentication and rejected attempts. */
     public void authenticate(int userId, boolean authenticated) {
+
+        // Don't collect data for single-modality devices or user has both biometrics enrolled.
+        if (isSingleModalityDevice()
+                || (hasEnrolledFace(userId) && hasEnrolledFingerprint(userId))) {
+            return;
+        }
+
         // SharedPreference is not ready when starting system server, initialize
         // mUserAuthenticationStatsMap in authentication to ensure SharedPreference
         // is ready for application use.
@@ -150,25 +164,9 @@
 
         authenticationStats.resetData();
 
-        final PackageManager packageManager = mContext.getPackageManager();
+        final boolean hasEnrolledFace = hasEnrolledFace(userId);
+        final boolean hasEnrolledFingerprint = hasEnrolledFingerprint(userId);
 
-        // Don't send notification to single-modality devices.
-        if (!packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)
-                || !packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) {
-            return;
-        }
-
-        final FaceManager faceManager = mContext.getSystemService(FaceManager.class);
-        final boolean hasEnrolledFace = faceManager.hasEnrolledTemplates(userId);
-
-        final FingerprintManager fingerprintManager = mContext
-                .getSystemService(FingerprintManager.class);
-        final boolean hasEnrolledFingerprint = fingerprintManager.hasEnrolledTemplates(userId);
-
-        // Don't send notification when both face and fingerprint are enrolled.
-        if (hasEnrolledFace && hasEnrolledFingerprint) {
-            return;
-        }
         if (hasEnrolledFace && !hasEnrolledFingerprint) {
             mBiometricNotification.sendFpEnrollNotification(mContext);
             authenticationStats.updateNotificationCounter();
@@ -199,6 +197,19 @@
         }
     }
 
+    private boolean isSingleModalityDevice() {
+        return !mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)
+                || !mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE);
+    }
+
+    private boolean hasEnrolledFace(int userId) {
+        return mFaceManager.hasEnrolledTemplates(userId);
+    }
+
+    private boolean hasEnrolledFingerprint(int userId) {
+        return mFingerprintManager.hasEnrolledTemplates(userId);
+    }
+
     /**
      * Only being used in tests. Callers should not make any changes to the returned
      * authentication stats.
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
index f1c74f0..2aec9ae 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
@@ -176,6 +176,7 @@
                 .setSmallIcon(R.drawable.ic_lock)
                 .setContentTitle(title)
                 .setContentText(content)
+                .setStyle(new Notification.BigTextStyle().bigText(content))
                 .setSubText(name)
                 .setOnlyAlertOnce(true)
                 .setLocalOnly(true)
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
index adea13f..d4232ab 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
@@ -29,7 +29,6 @@
 import android.hardware.broadcastradio.ProgramFilter;
 import android.hardware.broadcastradio.ProgramIdentifier;
 import android.hardware.broadcastradio.ProgramInfo;
-import android.hardware.broadcastradio.ProgramListChunk;
 import android.hardware.broadcastradio.Properties;
 import android.hardware.broadcastradio.Result;
 import android.hardware.broadcastradio.VendorKeyValue;
@@ -38,6 +37,7 @@
 import android.hardware.radio.RadioManager;
 import android.hardware.radio.RadioMetadata;
 import android.hardware.radio.RadioTuner;
+import android.hardware.radio.UniqueProgramIdentifier;
 import android.os.Build;
 import android.os.ParcelableException;
 import android.os.ServiceSpecificException;
@@ -553,31 +553,6 @@
         return hwFilter;
     }
 
-    static ProgramList.Chunk chunkFromHalProgramListChunk(ProgramListChunk chunk) {
-        Set<RadioManager.ProgramInfo> modified = new ArraySet<>(chunk.modified.length);
-        for (int i = 0; i < chunk.modified.length; i++) {
-            RadioManager.ProgramInfo modifiedInfo =
-                    programInfoFromHalProgramInfo(chunk.modified[i]);
-            if (modifiedInfo == null) {
-                Slogf.w(TAG, "Program info %s in program list chunk is not valid",
-                        chunk.modified[i]);
-                continue;
-            }
-            modified.add(modifiedInfo);
-        }
-        Set<ProgramSelector.Identifier> removed = new ArraySet<>();
-        if (chunk.removed != null) {
-            for (int i = 0; i < chunk.removed.length; i++) {
-                ProgramSelector.Identifier removedId =
-                        identifierFromHalProgramIdentifier(chunk.removed[i]);
-                if (removedId != null) {
-                    removed.add(removedId);
-                }
-            }
-        }
-        return new ProgramList.Chunk(chunk.purge, chunk.complete, modified, removed);
-    }
-
     private static boolean isNewIdentifierInU(ProgramSelector.Identifier id) {
         return id.getType() == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT;
     }
@@ -630,11 +605,11 @@
                 modified.add(info);
             }
         }
-        Set<ProgramSelector.Identifier> removed = new ArraySet<>();
-        Iterator<ProgramSelector.Identifier> removedIterator = chunk.getRemoved().iterator();
+        Set<UniqueProgramIdentifier> removed = new ArraySet<>();
+        Iterator<UniqueProgramIdentifier> removedIterator = chunk.getRemoved().iterator();
         while (removedIterator.hasNext()) {
-            ProgramSelector.Identifier id = removedIterator.next();
-            if (!isNewIdentifierInU(id)) {
+            UniqueProgramIdentifier id = removedIterator.next();
+            if (!isNewIdentifierInU(id.getPrimaryId())) {
                 removed.add(id);
             }
         }
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java b/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java
index c9ae735..756dbbb 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java
@@ -17,9 +17,11 @@
 package com.android.server.broadcastradio.aidl;
 
 import android.annotation.Nullable;
+import android.hardware.broadcastradio.ProgramListChunk;
 import android.hardware.radio.ProgramList;
-import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.ProgramSelector.Identifier;
 import android.hardware.radio.RadioManager;
+import android.hardware.radio.UniqueProgramIdentifier;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
@@ -30,7 +32,6 @@
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 /**
@@ -40,24 +41,25 @@
 
     private static final String TAG = "BcRadioAidlSrv.cache";
     /**
-     * Maximum number of {@link RadioManager#ProgramInfo} elements that will be put into a
+     * Maximum number of {@link RadioManager.ProgramInfo} elements that will be put into a
      * ProgramList.Chunk.mModified array. Used to try to ensure a single ProgramList.Chunk
      * stays within the AIDL data size limit.
      */
     private static final int MAX_NUM_MODIFIED_PER_CHUNK = 100;
 
     /**
-     * Maximum number of {@link ProgramSelector#Identifier} elements that will be put
-     * into the removed array of {@link ProgramList#Chunk}. Used to try to ensure a single
-     * {@link ProgramList#Chunk} stays within the AIDL data size limit.
+     * Maximum number of {@link Identifier} elements that will be put into the removed array
+     * of {@link ProgramList.Chunk}. Use to attempt and keep the single {@link ProgramList.Chunk}
+     * within the AIDL data size limit.
      */
     private static final int MAX_NUM_REMOVED_PER_CHUNK = 500;
 
     /**
-     * Map from primary identifier to corresponding {@link RadioManager#ProgramInfo}.
+     * Map from primary identifier to {@link UniqueProgramIdentifier} and then to corresponding
+     * {@link RadioManager.ProgramInfo}.
      */
-    private final Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> mProgramInfoMap =
-            new ArrayMap<>();
+    private final ArrayMap<Identifier, ArrayMap<UniqueProgramIdentifier, RadioManager.ProgramInfo>>
+            mProgramInfoMap = new ArrayMap<>();
 
     /**
      * Flag indicating whether mProgramInfoMap is considered complete based upon the received
@@ -81,13 +83,17 @@
         mFilter = filter;
         mComplete = complete;
         for (int i = 0; i < programInfos.length; i++) {
-            mProgramInfoMap.put(programInfos[i].getSelector().getPrimaryId(), programInfos[i]);
+            putInfo(programInfos[i]);
         }
     }
 
     @VisibleForTesting
     List<RadioManager.ProgramInfo> toProgramInfoList() {
-        return new ArrayList<>(mProgramInfoMap.values());
+        List<RadioManager.ProgramInfo> programInfoList = new ArrayList<>();
+        for (int index = 0; index < mProgramInfoMap.size(); index++) {
+            programInfoList.addAll(mProgramInfoMap.valueAt(index).values());
+        }
+        return programInfoList;
     }
 
     @Override
@@ -97,10 +103,14 @@
         sb.append(", mFilter = ");
         sb.append(mFilter);
         sb.append(", mProgramInfoMap = [");
-        mProgramInfoMap.forEach((id, programInfo) -> {
-            sb.append(", ");
-            sb.append(programInfo);
-        });
+        for (int index = 0; index < mProgramInfoMap.size(); index++) {
+            ArrayMap<UniqueProgramIdentifier, RadioManager.ProgramInfo> entries =
+                    mProgramInfoMap.valueAt(index);
+            for (int entryIndex = 0; entryIndex < entries.size(); entryIndex++) {
+                sb.append(", ");
+                sb.append(entries.valueAt(entryIndex));
+            }
+        }
         return sb.append("])").toString();
     }
 
@@ -114,8 +124,7 @@
     }
 
     @VisibleForTesting
-    void updateFromHalProgramListChunk(
-            android.hardware.broadcastradio.ProgramListChunk chunk) {
+    void updateFromHalProgramListChunk(ProgramListChunk chunk) {
         if (chunk.purge) {
             mProgramInfoMap.clear();
         }
@@ -125,8 +134,9 @@
             if (programInfo == null) {
                 Slogf.e(TAG, "Program info in program info %s in chunk is not valid",
                         chunk.modified[i]);
+                continue;
             }
-            mProgramInfoMap.put(programInfo.getSelector().getPrimaryId(), programInfo);
+            putInfo(programInfo);
         }
         if (chunk.removed != null) {
             for (int i = 0; i < chunk.removed.length; i++) {
@@ -155,25 +165,31 @@
             purge = true;
         }
 
-        Set<RadioManager.ProgramInfo> modified = new ArraySet<>();
-        Set<ProgramSelector.Identifier> removed = new ArraySet<>(mProgramInfoMap.keySet());
-        for (Map.Entry<ProgramSelector.Identifier, RadioManager.ProgramInfo> entry
-                : other.mProgramInfoMap.entrySet()) {
-            ProgramSelector.Identifier id = entry.getKey();
+        ArraySet<RadioManager.ProgramInfo> modified = new ArraySet<>();
+        ArraySet<UniqueProgramIdentifier> removed = new ArraySet<>();
+        for (int index = 0; index < mProgramInfoMap.size(); index++) {
+            removed.addAll(mProgramInfoMap.valueAt(index).keySet());
+        }
+        for (int index = 0; index < other.mProgramInfoMap.size(); index++) {
+            Identifier id = other.mProgramInfoMap.keyAt(index);
             if (!passesFilter(id)) {
                 continue;
             }
-            removed.remove(id);
+            ArrayMap<UniqueProgramIdentifier, RadioManager.ProgramInfo> entries =
+                    other.mProgramInfoMap.valueAt(index);
+            for (int entryIndex = 0; entryIndex < entries.size(); entryIndex++) {
+                removed.remove(entries.keyAt(entryIndex));
 
-            RadioManager.ProgramInfo newInfo = entry.getValue();
-            if (!shouldIncludeInModified(newInfo)) {
-                continue;
+                RadioManager.ProgramInfo newInfo = entries.valueAt(entryIndex);
+                if (!shouldIncludeInModified(newInfo)) {
+                    continue;
+                }
+                putInfo(newInfo);
+                modified.add(newInfo);
             }
-            mProgramInfoMap.put(id, newInfo);
-            modified.add(newInfo);
         }
-        for (ProgramSelector.Identifier rem : removed) {
-            mProgramInfoMap.remove(rem);
+        for (int removedIndex = 0; removedIndex < removed.size(); removedIndex++) {
+            removeUniqueId(removed.valueAt(removedIndex));
         }
         mComplete = other.mComplete;
         return buildChunks(purge, mComplete, modified, maxNumModifiedPerChunk, removed,
@@ -181,45 +197,61 @@
     }
 
     @Nullable
-    List<ProgramList.Chunk> filterAndApplyChunk(ProgramList.Chunk chunk) {
+    List<ProgramList.Chunk> filterAndApplyChunk(ProgramListChunk chunk) {
         return filterAndApplyChunkInternal(chunk, MAX_NUM_MODIFIED_PER_CHUNK,
                 MAX_NUM_REMOVED_PER_CHUNK);
     }
 
     @VisibleForTesting
     @Nullable
-    List<ProgramList.Chunk> filterAndApplyChunkInternal(ProgramList.Chunk chunk,
+    List<ProgramList.Chunk> filterAndApplyChunkInternal(ProgramListChunk chunk,
             int maxNumModifiedPerChunk, int maxNumRemovedPerChunk) {
-        if (chunk.isPurge()) {
+        if (chunk.purge) {
             mProgramInfoMap.clear();
         }
 
         Set<RadioManager.ProgramInfo> modified = new ArraySet<>();
-        Set<ProgramSelector.Identifier> removed = new ArraySet<>();
-        for (RadioManager.ProgramInfo info : chunk.getModified()) {
-            ProgramSelector.Identifier id = info.getSelector().getPrimaryId();
-            if (!passesFilter(id) || !shouldIncludeInModified(info)) {
+        for (int i = 0; i < chunk.modified.length; i++) {
+            RadioManager.ProgramInfo info =
+                    ConversionUtils.programInfoFromHalProgramInfo(chunk.modified[i]);
+            if (info == null) {
+                Slogf.w(TAG, "Program info %s in program list chunk is not valid",
+                        chunk.modified[i]);
                 continue;
             }
-            mProgramInfoMap.put(id, info);
+            Identifier primaryId = info.getSelector().getPrimaryId();
+            if (!passesFilter(primaryId) || !shouldIncludeInModified(info)) {
+                continue;
+            }
+            putInfo(info);
             modified.add(info);
         }
-        for (ProgramSelector.Identifier id : chunk.getRemoved()) {
-            if (mProgramInfoMap.containsKey(id)) {
-                mProgramInfoMap.remove(id);
-                removed.add(id);
+        Set<UniqueProgramIdentifier> removed = new ArraySet<>();
+        if (chunk.removed != null) {
+            for (int i = 0; i < chunk.removed.length; i++) {
+                Identifier removedId = ConversionUtils.identifierFromHalProgramIdentifier(
+                        chunk.removed[i]);
+                if (removedId == null) {
+                    Slogf.w(TAG, "Removed identifier %s in program list chunk is not valid",
+                            chunk.modified[i]);
+                    continue;
+                }
+                if (mProgramInfoMap.containsKey(removedId)) {
+                    removed.addAll(mProgramInfoMap.get(removedId).keySet());
+                    mProgramInfoMap.remove(removedId);
+                }
             }
         }
-        if (modified.isEmpty() && removed.isEmpty() && mComplete == chunk.isComplete()
-                && !chunk.isPurge()) {
+        if (modified.isEmpty() && removed.isEmpty() && mComplete == chunk.complete
+                && !chunk.purge) {
             return null;
         }
-        mComplete = chunk.isComplete();
-        return buildChunks(chunk.isPurge(), mComplete, modified, maxNumModifiedPerChunk, removed,
+        mComplete = chunk.complete;
+        return buildChunks(chunk.purge, mComplete, modified, maxNumModifiedPerChunk, removed,
                 maxNumRemovedPerChunk);
     }
 
-    private boolean passesFilter(ProgramSelector.Identifier id) {
+    private boolean passesFilter(Identifier id) {
         if (mFilter == null) {
             return true;
         }
@@ -233,9 +265,32 @@
         return mFilter.areCategoriesIncluded() || !id.isCategoryType();
     }
 
+    private void putInfo(RadioManager.ProgramInfo info) {
+        Identifier primaryId = info.getSelector().getPrimaryId();
+        if (!mProgramInfoMap.containsKey(primaryId)) {
+            mProgramInfoMap.put(primaryId, new ArrayMap<>());
+        }
+        mProgramInfoMap.get(primaryId).put(new UniqueProgramIdentifier(info.getSelector()), info);
+    }
+
+    private void removeUniqueId(UniqueProgramIdentifier uniqueId) {
+        Identifier primaryId =  uniqueId.getPrimaryId();
+        if (!mProgramInfoMap.containsKey(primaryId)) {
+            return;
+        }
+        mProgramInfoMap.get(primaryId).remove(uniqueId);
+        if (mProgramInfoMap.get(primaryId).isEmpty()) {
+            mProgramInfoMap.remove(primaryId);
+        }
+    }
+
     private boolean shouldIncludeInModified(RadioManager.ProgramInfo newInfo) {
-        RadioManager.ProgramInfo oldInfo = mProgramInfoMap.get(
-                newInfo.getSelector().getPrimaryId());
+        Identifier primaryId = newInfo.getSelector().getPrimaryId();
+        RadioManager.ProgramInfo oldInfo = null;
+        if (mProgramInfoMap.containsKey(primaryId)) {
+            UniqueProgramIdentifier uniqueId = new UniqueProgramIdentifier(newInfo.getSelector());
+            oldInfo = mProgramInfoMap.get(primaryId).get(uniqueId);
+        }
         if (oldInfo == null) {
             return true;
         }
@@ -251,7 +306,7 @@
 
     private static List<ProgramList.Chunk> buildChunks(boolean purge, boolean complete,
             @Nullable Collection<RadioManager.ProgramInfo> modified, int maxNumModifiedPerChunk,
-            @Nullable Collection<ProgramSelector.Identifier> removed, int maxNumRemovedPerChunk) {
+            @Nullable Collection<UniqueProgramIdentifier> removed, int maxNumRemovedPerChunk) {
         // Communication protocol requires that if purge is set, removed is empty.
         if (purge) {
             removed = null;
@@ -275,7 +330,7 @@
         int modifiedPerChunk = 0;
         int removedPerChunk = 0;
         Iterator<RadioManager.ProgramInfo> modifiedIter = null;
-        Iterator<ProgramSelector.Identifier> removedIter = null;
+        Iterator<UniqueProgramIdentifier> removedIter = null;
         if (modified != null) {
             modifiedPerChunk = roundUpFraction(modified.size(), numChunks);
             modifiedIter = modified.iterator();
@@ -287,7 +342,7 @@
         List<ProgramList.Chunk> chunks = new ArrayList<>(numChunks);
         for (int i = 0; i < numChunks; i++) {
             ArraySet<RadioManager.ProgramInfo> modifiedChunk = new ArraySet<>();
-            ArraySet<ProgramSelector.Identifier> removedChunk = new ArraySet<>();
+            ArraySet<UniqueProgramIdentifier> removedChunk = new ArraySet<>();
             if (modifiedIter != null) {
                 for (int j = 0; j < modifiedPerChunk && modifiedIter.hasNext(); j++) {
                     modifiedChunk.add(modifiedIter.next());
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
index 7c87c6c..2ae7f95 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
@@ -142,12 +142,11 @@
         public void onProgramListUpdated(ProgramListChunk programListChunk) {
             fireLater(() -> {
                 synchronized (mLock) {
-                    android.hardware.radio.ProgramList.Chunk chunk =
-                            ConversionUtils.chunkFromHalProgramListChunk(programListChunk);
-                    mProgramInfoCache.filterAndApplyChunk(chunk);
+                    mProgramInfoCache.filterAndApplyChunk(programListChunk);
 
                     for (int i = 0; i < mAidlTunerSessions.size(); i++) {
-                        mAidlTunerSessions.valueAt(i).onMergedProgramListUpdateFromHal(chunk);
+                        mAidlTunerSessions.valueAt(i).onMergedProgramListUpdateFromHal(
+                                programListChunk);
                     }
                 }
             });
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java b/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java
index beff7bd..4ed36ec 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java
@@ -20,6 +20,7 @@
 import android.graphics.Bitmap;
 import android.hardware.broadcastradio.ConfigFlag;
 import android.hardware.broadcastradio.IBroadcastRadio;
+import android.hardware.broadcastradio.ProgramListChunk;
 import android.hardware.radio.ITuner;
 import android.hardware.radio.ProgramList;
 import android.hardware.radio.ProgramSelector;
@@ -297,7 +298,7 @@
         }
     }
 
-    void onMergedProgramListUpdateFromHal(ProgramList.Chunk mergedChunk) {
+    void onMergedProgramListUpdateFromHal(ProgramListChunk mergedChunk) {
         List<ProgramList.Chunk> clientUpdateChunks;
         synchronized (mLock) {
             if (mProgramInfoCache == null) {
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
index e6908b1..fb1138f 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
@@ -28,7 +28,6 @@
 import android.hardware.broadcastradio.V2_0.ProgramFilter;
 import android.hardware.broadcastradio.V2_0.ProgramIdentifier;
 import android.hardware.broadcastradio.V2_0.ProgramInfo;
-import android.hardware.broadcastradio.V2_0.ProgramListChunk;
 import android.hardware.broadcastradio.V2_0.Properties;
 import android.hardware.broadcastradio.V2_0.Result;
 import android.hardware.broadcastradio.V2_0.VendorKeyValue;
@@ -425,16 +424,6 @@
         return hwFilter;
     }
 
-    static @NonNull ProgramList.Chunk programListChunkFromHal(@NonNull ProgramListChunk chunk) {
-        Set<RadioManager.ProgramInfo> modified = chunk.modified.stream().
-                map(info -> programInfoFromHal(info)).collect(Collectors.toSet());
-        Set<ProgramSelector.Identifier> removed = chunk.removed.stream().
-                map(id -> Objects.requireNonNull(programIdentifierFromHal(id))).
-                collect(Collectors.toSet());
-
-        return new ProgramList.Chunk(chunk.purge, chunk.complete, modified, removed);
-    }
-
     public static @NonNull android.hardware.radio.Announcement announcementFromHal(
             @NonNull Announcement hwAnnouncement) {
         return new android.hardware.radio.Announcement(
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/ProgramInfoCache.java b/services/core/java/com/android/server/broadcastradio/hal2/ProgramInfoCache.java
index 9831af6..111953d 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/ProgramInfoCache.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/ProgramInfoCache.java
@@ -16,21 +16,21 @@
 
 package com.android.server.broadcastradio.hal2;
 
-import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.hardware.broadcastradio.V2_0.ProgramListChunk;
 import android.hardware.radio.ProgramList;
-import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.ProgramSelector.Identifier;
 import android.hardware.radio.RadioManager;
+import android.hardware.radio.UniqueProgramIdentifier;
+import android.util.ArrayMap;
+import android.util.ArraySet;
 
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 final class ProgramInfoCache {
@@ -40,13 +40,14 @@
     private static final int MAX_NUM_MODIFIED_PER_CHUNK = 100;
 
     // Maximum number of ProgramSelector.Identifier elements that will be put into a
-    // ProgramList.Chunk.mRemoved array. Used to try to ensure a single ProgramList.Chunk stays
+    // ProgramList.Chunk.mRemoved array. Use to attempt and keep the single ProgramList.Chunk
     // within the AIDL data size limit.
     private static final int MAX_NUM_REMOVED_PER_CHUNK = 500;
 
-    // Map from primary identifier to corresponding ProgramInfo.
-    private final Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> mProgramInfoMap =
-            new HashMap<>();
+    // Map from primary identifier to a map of unique identifiers and program info, where the
+    // containing map has unique identifiers to program info.
+    private final ArrayMap<Identifier, ArrayMap<UniqueProgramIdentifier, RadioManager.ProgramInfo>>
+            mProgramInfoMap = new ArrayMap<>();
 
     // Flag indicating whether mProgramInfoMap is considered complete based upon the received
     // updates.
@@ -66,18 +67,18 @@
             RadioManager.ProgramInfo... programInfos) {
         mFilter = filter;
         mComplete = complete;
-        for (RadioManager.ProgramInfo programInfo : programInfos) {
-            mProgramInfoMap.put(programInfo.getSelector().getPrimaryId(), programInfo);
+        for (int i = 0; i < programInfos.length; i++) {
+            putInfo(programInfos[i]);
         }
     }
 
     @VisibleForTesting
-    boolean programInfosAreExactly(RadioManager.ProgramInfo... programInfos) {
-        Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> expectedMap = new HashMap<>();
-        for (RadioManager.ProgramInfo programInfo : programInfos) {
-            expectedMap.put(programInfo.getSelector().getPrimaryId(), programInfo);
+    List<RadioManager.ProgramInfo> toProgramInfoList() {
+        List<RadioManager.ProgramInfo> programInfoList = new ArrayList<>();
+        for (int index = 0; index < mProgramInfoMap.size(); index++) {
+            programInfoList.addAll(mProgramInfoMap.valueAt(index).values());
         }
-        return expectedMap.equals(mProgramInfoMap);
+        return programInfoList;
     }
 
     @Override
@@ -87,10 +88,14 @@
         sb.append(", mFilter = ");
         sb.append(mFilter);
         sb.append(", mProgramInfoMap = [");
-        mProgramInfoMap.forEach((id, programInfo) -> {
-            sb.append("\n");
-            sb.append(programInfo.toString());
-        });
+        for (int index = 0; index < mProgramInfoMap.size(); index++) {
+            ArrayMap<UniqueProgramIdentifier, RadioManager.ProgramInfo> entries =
+                    mProgramInfoMap.valueAt(index);
+            for (int entryIndex = 0; entryIndex < entries.size(); entryIndex++) {
+                sb.append(", ");
+                sb.append(entries.valueAt(entryIndex));
+            }
+        }
         sb.append("]");
         return sb.toString();
     }
@@ -103,14 +108,13 @@
         return mFilter;
     }
 
-    void updateFromHalProgramListChunk(
-            @NonNull android.hardware.broadcastradio.V2_0.ProgramListChunk chunk) {
+    void updateFromHalProgramListChunk(ProgramListChunk chunk) {
         if (chunk.purge) {
             mProgramInfoMap.clear();
         }
         for (android.hardware.broadcastradio.V2_0.ProgramInfo halProgramInfo : chunk.modified) {
             RadioManager.ProgramInfo programInfo = Convert.programInfoFromHal(halProgramInfo);
-            mProgramInfoMap.put(programInfo.getSelector().getPrimaryId(), programInfo);
+            putInfo(programInfo);
         }
         for (android.hardware.broadcastradio.V2_0.ProgramIdentifier halProgramId : chunk.removed) {
             mProgramInfoMap.remove(Convert.programIdentifierFromHal(halProgramId));
@@ -118,14 +122,13 @@
         mComplete = chunk.complete;
     }
 
-    @NonNull List<ProgramList.Chunk> filterAndUpdateFrom(@NonNull ProgramInfoCache other,
-            boolean purge) {
+    List<ProgramList.Chunk> filterAndUpdateFrom(ProgramInfoCache other, boolean purge) {
         return filterAndUpdateFromInternal(other, purge, MAX_NUM_MODIFIED_PER_CHUNK,
                 MAX_NUM_REMOVED_PER_CHUNK);
     }
 
     @VisibleForTesting
-    @NonNull List<ProgramList.Chunk> filterAndUpdateFromInternal(@NonNull ProgramInfoCache other,
+    List<ProgramList.Chunk> filterAndUpdateFromInternal(ProgramInfoCache other,
             boolean purge, int maxNumModifiedPerChunk, int maxNumRemovedPerChunk) {
         if (purge) {
             mProgramInfoMap.clear();
@@ -136,69 +139,82 @@
             purge = true;
         }
 
-        Set<RadioManager.ProgramInfo> modified = new HashSet<>();
-        Set<ProgramSelector.Identifier> removed = new HashSet<>(mProgramInfoMap.keySet());
-        for (Map.Entry<ProgramSelector.Identifier, RadioManager.ProgramInfo> entry
-                : other.mProgramInfoMap.entrySet()) {
-            ProgramSelector.Identifier id = entry.getKey();
+        ArraySet<RadioManager.ProgramInfo> modified = new ArraySet<>();
+        ArraySet<UniqueProgramIdentifier> removed = new ArraySet<>();
+        for (int index = 0; index < mProgramInfoMap.size(); index++) {
+            removed.addAll(mProgramInfoMap.valueAt(index).keySet());
+        }
+        for (int index = 0; index < other.mProgramInfoMap.size(); index++) {
+            Identifier id = other.mProgramInfoMap.keyAt(index);
             if (!passesFilter(id)) {
                 continue;
             }
-            removed.remove(id);
+            ArrayMap<UniqueProgramIdentifier, RadioManager.ProgramInfo> entries =
+                    other.mProgramInfoMap.valueAt(index);
+            for (int entryIndex = 0; entryIndex < entries.size(); entryIndex++) {
+                removed.remove(entries.keyAt(entryIndex));
 
-            RadioManager.ProgramInfo newInfo = entry.getValue();
-            if (!shouldIncludeInModified(newInfo)) {
-                continue;
+                RadioManager.ProgramInfo newInfo = entries.valueAt(entryIndex);
+                if (!shouldIncludeInModified(newInfo)) {
+                    continue;
+                }
+                putInfo(newInfo);
+                modified.add(newInfo);
             }
-            mProgramInfoMap.put(id, newInfo);
-            modified.add(newInfo);
         }
-        for (ProgramSelector.Identifier rem : removed) {
-            mProgramInfoMap.remove(rem);
+        for (int removedIndex = 0; removedIndex < removed.size(); removedIndex++) {
+            removeUniqueId(removed.valueAt(removedIndex));
         }
         mComplete = other.mComplete;
         return buildChunks(purge, mComplete, modified, maxNumModifiedPerChunk, removed,
                 maxNumRemovedPerChunk);
     }
 
-    @Nullable List<ProgramList.Chunk> filterAndApplyChunk(@NonNull ProgramList.Chunk chunk) {
+    @Nullable
+    List<ProgramList.Chunk> filterAndApplyChunk(ProgramListChunk chunk) {
         return filterAndApplyChunkInternal(chunk, MAX_NUM_MODIFIED_PER_CHUNK,
                 MAX_NUM_REMOVED_PER_CHUNK);
     }
 
     @VisibleForTesting
-    @Nullable List<ProgramList.Chunk> filterAndApplyChunkInternal(@NonNull ProgramList.Chunk chunk,
+    @Nullable
+    List<ProgramList.Chunk> filterAndApplyChunkInternal(ProgramListChunk chunk,
             int maxNumModifiedPerChunk, int maxNumRemovedPerChunk) {
-        if (chunk.isPurge()) {
+        if (chunk.purge) {
             mProgramInfoMap.clear();
         }
 
-        Set<RadioManager.ProgramInfo> modified = new HashSet<>();
-        Set<ProgramSelector.Identifier> removed = new HashSet<>();
-        for (RadioManager.ProgramInfo info : chunk.getModified()) {
-            ProgramSelector.Identifier id = info.getSelector().getPrimaryId();
-            if (!passesFilter(id) || !shouldIncludeInModified(info)) {
+        Set<RadioManager.ProgramInfo> modified = new ArraySet<>();
+        for (android.hardware.broadcastradio.V2_0.ProgramInfo halProgramInfo : chunk.modified) {
+            RadioManager.ProgramInfo info = Convert.programInfoFromHal(halProgramInfo);
+            Identifier primaryId = info.getSelector().getPrimaryId();
+            if (!passesFilter(primaryId) || !shouldIncludeInModified(info)) {
                 continue;
             }
-            mProgramInfoMap.put(id, info);
+            putInfo(info);
             modified.add(info);
         }
-        for (ProgramSelector.Identifier id : chunk.getRemoved()) {
-            if (mProgramInfoMap.containsKey(id)) {
-                mProgramInfoMap.remove(id);
-                removed.add(id);
+        Set<UniqueProgramIdentifier> removed = new ArraySet<>();
+        for (android.hardware.broadcastradio.V2_0.ProgramIdentifier halProgramId : chunk.removed) {
+            Identifier removedId = Convert.programIdentifierFromHal(halProgramId);
+            if (removedId == null) {
+                continue;
+            }
+            if (mProgramInfoMap.containsKey(removedId)) {
+                removed.addAll(mProgramInfoMap.get(removedId).keySet());
+                mProgramInfoMap.remove(removedId);
             }
         }
-        if (modified.isEmpty() && removed.isEmpty() && mComplete == chunk.isComplete()
-                && !chunk.isPurge()) {
+        if (modified.isEmpty() && removed.isEmpty() && mComplete == chunk.complete
+                && !chunk.purge) {
             return null;
         }
-        mComplete = chunk.isComplete();
-        return buildChunks(chunk.isPurge(), mComplete, modified, maxNumModifiedPerChunk, removed,
+        mComplete = chunk.complete;
+        return buildChunks(chunk.purge, mComplete, modified, maxNumModifiedPerChunk, removed,
                 maxNumRemovedPerChunk);
     }
 
-    private boolean passesFilter(ProgramSelector.Identifier id) {
+    private boolean passesFilter(Identifier id) {
         if (mFilter == null) {
             return true;
         }
@@ -215,9 +231,33 @@
         return true;
     }
 
+    private void putInfo(RadioManager.ProgramInfo info) {
+        Identifier primaryId = info.getSelector().getPrimaryId();
+        if (!mProgramInfoMap.containsKey(primaryId)) {
+            mProgramInfoMap.put(primaryId, new ArrayMap<>());
+        }
+        mProgramInfoMap.get(primaryId).put(new UniqueProgramIdentifier(
+                info.getSelector()), info);
+    }
+
+    private void removeUniqueId(UniqueProgramIdentifier uniqueId) {
+        Identifier primaryId =  uniqueId.getPrimaryId();
+        if (!mProgramInfoMap.containsKey(primaryId)) {
+            return;
+        }
+        mProgramInfoMap.get(primaryId).remove(uniqueId);
+        if (mProgramInfoMap.get(primaryId).isEmpty()) {
+            mProgramInfoMap.remove(primaryId);
+        }
+    }
+
     private boolean shouldIncludeInModified(RadioManager.ProgramInfo newInfo) {
-        RadioManager.ProgramInfo oldInfo = mProgramInfoMap.get(
-                newInfo.getSelector().getPrimaryId());
+        Identifier primaryId = newInfo.getSelector().getPrimaryId();
+        RadioManager.ProgramInfo oldInfo = null;
+        if (mProgramInfoMap.containsKey(primaryId)) {
+            UniqueProgramIdentifier uniqueId = new UniqueProgramIdentifier(newInfo.getSelector());
+            oldInfo = mProgramInfoMap.get(primaryId).get(uniqueId);
+        }
         if (oldInfo == null) {
             return true;
         }
@@ -231,9 +271,9 @@
         return (numerator / denominator) + (numerator % denominator > 0 ? 1 : 0);
     }
 
-    private static @NonNull List<ProgramList.Chunk> buildChunks(boolean purge, boolean complete,
+    private static List<ProgramList.Chunk> buildChunks(boolean purge, boolean complete,
             @Nullable Collection<RadioManager.ProgramInfo> modified, int maxNumModifiedPerChunk,
-            @Nullable Collection<ProgramSelector.Identifier> removed, int maxNumRemovedPerChunk) {
+            @Nullable Collection<UniqueProgramIdentifier> removed, int maxNumRemovedPerChunk) {
         // Communication protocol requires that if purge is set, removed is empty.
         if (purge) {
             removed = null;
@@ -257,7 +297,7 @@
         int modifiedPerChunk = 0;
         int removedPerChunk = 0;
         Iterator<RadioManager.ProgramInfo> modifiedIter = null;
-        Iterator<ProgramSelector.Identifier> removedIter = null;
+        Iterator<UniqueProgramIdentifier> removedIter = null;
         if (modified != null) {
             modifiedPerChunk = roundUpFraction(modified.size(), numChunks);
             modifiedIter = modified.iterator();
@@ -268,8 +308,8 @@
         }
         List<ProgramList.Chunk> chunks = new ArrayList<ProgramList.Chunk>(numChunks);
         for (int i = 0; i < numChunks; i++) {
-            HashSet<RadioManager.ProgramInfo> modifiedChunk = new HashSet<>();
-            HashSet<ProgramSelector.Identifier> removedChunk = new HashSet<>();
+            ArraySet<RadioManager.ProgramInfo> modifiedChunk = new ArraySet<>();
+            ArraySet<UniqueProgramIdentifier> removedChunk = new ArraySet<>();
             if (modifiedIter != null) {
                 for (int j = 0; j < modifiedPerChunk && modifiedIter.hasNext(); j++) {
                     modifiedChunk.add(modifiedIter.next());
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
index 7b5cb898..a54af2e 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
@@ -111,13 +111,11 @@
         @Override
         public void onProgramListUpdated(ProgramListChunk programListChunk) {
             fireLater(() -> {
-                android.hardware.radio.ProgramList.Chunk chunk =
-                        Convert.programListChunkFromHal(programListChunk);
                 synchronized (mLock) {
-                    mProgramInfoCache.filterAndApplyChunk(chunk);
+                    mProgramInfoCache.filterAndApplyChunk(programListChunk);
 
                     for (TunerSession tunerSession : mAidlTunerSessions) {
-                        tunerSession.onMergedProgramListUpdateFromHal(chunk);
+                        tunerSession.onMergedProgramListUpdateFromHal(programListChunk);
                     }
                 }
             });
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
index 1efc4a5..978dc01 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
@@ -21,6 +21,7 @@
 import android.graphics.Bitmap;
 import android.hardware.broadcastradio.V2_0.ConfigFlag;
 import android.hardware.broadcastradio.V2_0.ITunerSession;
+import android.hardware.broadcastradio.V2_0.ProgramListChunk;
 import android.hardware.broadcastradio.V2_0.Result;
 import android.hardware.radio.ITuner;
 import android.hardware.radio.ProgramList;
@@ -267,7 +268,7 @@
         }
     }
 
-    void onMergedProgramListUpdateFromHal(ProgramList.Chunk mergedChunk) {
+    void onMergedProgramListUpdateFromHal(ProgramListChunk mergedChunk) {
         List<ProgramList.Chunk> clientUpdateChunks = null;
         synchronized (mLock) {
             if (mProgramInfoCache == null) {
diff --git a/services/core/java/com/android/server/content/SyncStorageEngine.java b/services/core/java/com/android/server/content/SyncStorageEngine.java
index b890bbd..9805fd3 100644
--- a/services/core/java/com/android/server/content/SyncStorageEngine.java
+++ b/services/core/java/com/android/server/content/SyncStorageEngine.java
@@ -1754,7 +1754,7 @@
                     eventType = parser.next();
                 } while (eventType != XmlPullParser.END_DOCUMENT);
             }
-        } catch (XmlPullParserException e) {
+        } catch (XmlPullParserException | ArrayIndexOutOfBoundsException e) {
             Slog.w(TAG, "Error reading accounts", e);
             return;
         } catch (java.io.IOException e) {
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 4d3bf400..b5a373e 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -464,10 +464,7 @@
                         if (DEBUG_REMOVE) Slog.d(TAG, "Still installed by other users");
                         clearPackageStateAndReturn = true;
                     } else {
-                        // We need to set it back to 'installed' so the uninstall
-                        // broadcasts will be sent correctly.
                         if (DEBUG_REMOVE) Slog.d(TAG, "Not installed by other users, full delete");
-                        ps.setInstalled(true, userId);
                         mPm.mSettings.writeKernelMappingLPr(ps);
                         clearPackageStateAndReturn = false;
                     }
diff --git a/services/core/java/com/android/server/pm/PackageArchiverService.java b/services/core/java/com/android/server/pm/PackageArchiverService.java
index c7f067b..e052407 100644
--- a/services/core/java/com/android/server/pm/PackageArchiverService.java
+++ b/services/core/java/com/android/server/pm/PackageArchiverService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.pm;
 
+import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
 import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
 import static android.os.PowerExemptionManager.REASON_PACKAGE_UNARCHIVE;
 import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
@@ -24,6 +25,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
+import android.annotation.UserIdInt;
 import android.app.AppOpsManager;
 import android.app.BroadcastOptions;
 import android.content.Context;
@@ -33,24 +35,37 @@
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageArchiver;
+import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.content.pm.VersionedPackage;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
 import android.os.Binder;
 import android.os.Bundle;
+import android.os.Environment;
 import android.os.ParcelableException;
+import android.os.SELinux;
 import android.os.UserHandle;
 import android.text.TextUtils;
+import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.pm.pkg.ArchiveState;
 import com.android.server.pm.pkg.ArchiveState.ArchiveActivityInfo;
 import com.android.server.pm.pkg.PackageStateInternal;
 import com.android.server.pm.pkg.PackageUserStateInternal;
 
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
 
 /**
  * Responsible archiving apps and returning information about archived apps.
@@ -61,6 +76,8 @@
  */
 public class PackageArchiverService extends IPackageArchiverService.Stub {
 
+    private static final String TAG = "PackageArchiverService";
+
     /**
      * The maximum time granted for an app store to start a foreground service when unarchival
      * is requested.
@@ -68,6 +85,8 @@
     // TODO(b/297358628) Make this configurable through a flag.
     private static final int DEFAULT_UNARCHIVE_FOREGROUND_TIMEOUT_MS = 120 * 1000;
 
+    private static final String ARCHIVE_ICONS_DIR = "package_archiver";
+
     private final Context mContext;
     private final PackageManagerService mPm;
 
@@ -97,25 +116,44 @@
         snapshot.enforceCrossUserPermission(binderUid, userId, true, true,
                 "archiveApp");
         verifyCaller(providedUid, binderUid);
-        ArchiveState archiveState;
+        CompletableFuture<ArchiveState> archiveStateFuture;
         try {
-            archiveState = createArchiveState(packageName, userId);
-            // TODO(b/282952870) Should be reverted if uninstall fails/cancels
-            storeArchiveState(packageName, archiveState, userId);
+            archiveStateFuture = createArchiveState(packageName, userId);
         } catch (PackageManager.NameNotFoundException e) {
+            Slog.d(TAG, TextUtils.formatSimple("Failed to archive %s with message %s",
+                    packageName, e.getMessage()));
             throw new ParcelableException(e);
         }
 
-        // TODO(b/278553670) Add special strings for the delete dialog
-        mPm.mInstallerService.uninstall(
-                new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST),
-                callerPackageName, DELETE_KEEP_DATA, intentSender, userId);
+        archiveStateFuture
+                .thenAccept(
+                        archiveState -> {
+                            // TODO(b/282952870) Should be reverted if uninstall fails/cancels
+                            try {
+                                storeArchiveState(packageName, archiveState, userId);
+                            } catch (PackageManager.NameNotFoundException e) {
+                                sendFailureStatus(intentSender, packageName, e.getMessage());
+                                return;
+                            }
+
+                            // TODO(b/278553670) Add special strings for the delete dialog
+                            mPm.mInstallerService.uninstall(
+                                    new VersionedPackage(packageName,
+                                            PackageManager.VERSION_CODE_HIGHEST),
+                                    callerPackageName, DELETE_KEEP_DATA, intentSender, userId,
+                                    binderUid);
+                        })
+                .exceptionally(
+                        e -> {
+                            sendFailureStatus(intentSender, packageName, e.getMessage());
+                            return null;
+                        });
     }
 
     /**
      * Creates archived state for the package and user.
      */
-    public ArchiveState createArchiveState(String packageName, int userId)
+    public CompletableFuture<ArchiveState> createArchiveState(String packageName, int userId)
             throws PackageManager.NameNotFoundException {
         PackageStateInternal ps = getPackageState(packageName, mPm.snapshotComputer(),
                 Binder.getCallingUid(), userId);
@@ -123,16 +161,56 @@
         verifyInstaller(responsibleInstallerPackage);
 
         List<LauncherActivityInfo> mainActivities = getLauncherActivityInfos(ps, userId);
+        final CompletableFuture<ArchiveState> archiveState = new CompletableFuture<>();
+        mPm.mHandler.post(() -> {
+            try {
+                archiveState.complete(
+                        createArchiveStateInternal(packageName, userId, mainActivities,
+                                responsibleInstallerPackage));
+            } catch (IOException e) {
+                archiveState.completeExceptionally(e);
+            }
+        });
+        return archiveState;
+    }
+
+    private ArchiveState createArchiveStateInternal(String packageName, int userId,
+            List<LauncherActivityInfo> mainActivities, String installerPackage)
+            throws IOException {
         List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>();
         for (int i = 0; i < mainActivities.size(); i++) {
-            // TODO(b/278553670) Extract and store launcher icons
+            LauncherActivityInfo mainActivity = mainActivities.get(i);
+            Path iconPath = storeIcon(packageName, mainActivity, userId);
             ArchiveActivityInfo activityInfo = new ArchiveActivityInfo(
-                    mainActivities.get(i).getLabel().toString(),
-                    Path.of("/TODO"), null);
+                    mainActivity.getLabel().toString(), iconPath, null);
             archiveActivityInfos.add(activityInfo);
         }
 
-        return new ArchiveState(archiveActivityInfos, responsibleInstallerPackage);
+        return new ArchiveState(archiveActivityInfos, installerPackage);
+    }
+
+    // TODO(b/298452477) Handle monochrome icons.
+    @VisibleForTesting
+    Path storeIcon(String packageName, LauncherActivityInfo mainActivity,
+            @UserIdInt int userId)
+            throws IOException {
+        int iconResourceId = mainActivity.getActivityInfo().getIconResource();
+        if (iconResourceId == 0) {
+            // The app doesn't define an icon. No need to store anything.
+            return null;
+        }
+        File iconsDir = createIconsDir(userId);
+        File iconFile = new File(iconsDir, packageName + "-" + mainActivity.getName() + ".png");
+        Bitmap icon = drawableToBitmap(mainActivity.getIcon(/* density= */ 0));
+        try (FileOutputStream out = new FileOutputStream(iconFile)) {
+            // Note: Quality is ignored for PNGs.
+            if (!icon.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, out)) {
+                throw new IOException(TextUtils.formatSimple("Failure to store icon file %s",
+                        iconFile.getName()));
+            }
+            out.flush();
+        }
+        return iconFile.toPath();
     }
 
     private void verifyInstaller(String installerPackage)
@@ -313,6 +391,29 @@
         return ps;
     }
 
+    private void sendFailureStatus(IntentSender statusReceiver, String packageName,
+            String message) {
+        Slog.d(TAG, TextUtils.formatSimple("Failed to archive %s with message %s", packageName,
+                message));
+        final Intent fillIn = new Intent();
+        fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName);
+        fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
+        fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, message);
+        try {
+            final BroadcastOptions options = BroadcastOptions.makeBasic();
+            options.setPendingIntentBackgroundActivityStartMode(
+                    MODE_BACKGROUND_ACTIVITY_START_DENIED);
+            statusReceiver.sendIntent(mContext, 0, fillIn, /* onFinished= */ null,
+                    /* handler= */ null, /* requiredPermission= */ null, options.toBundle());
+        } catch (IntentSender.SendIntentException e) {
+            Slog.e(
+                    TAG,
+                    TextUtils.formatSimple("Failed to send failure status for %s with message %s",
+                            packageName, message),
+                    e);
+        }
+    }
+
     private static void verifyCaller(int providedUid, int binderUid) {
         if (providedUid != binderUid) {
             throw new SecurityException(
@@ -323,4 +424,44 @@
                             binderUid));
         }
     }
+
+    private File createIconsDir(@UserIdInt int userId) throws IOException {
+        File iconsDir = getIconsDir(userId);
+        if (!iconsDir.isDirectory()) {
+            iconsDir.delete();
+            iconsDir.mkdirs();
+            if (!iconsDir.isDirectory()) {
+                throw new IOException("Unable to create directory " + iconsDir);
+            }
+        }
+        SELinux.restorecon(iconsDir);
+        return iconsDir;
+    }
+
+    private File getIconsDir(int userId) {
+        return new File(Environment.getDataSystemCeDirectory(userId), ARCHIVE_ICONS_DIR);
+    }
+
+    private static Bitmap drawableToBitmap(Drawable drawable) {
+        if (drawable instanceof BitmapDrawable) {
+            return ((BitmapDrawable) drawable).getBitmap();
+
+        }
+
+        Bitmap bitmap;
+        if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
+            // Needed for drawables that are just a single color.
+            bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+        } else {
+            bitmap =
+                    Bitmap.createBitmap(
+                            drawable.getIntrinsicWidth(),
+                            drawable.getIntrinsicHeight(),
+                            Bitmap.Config.ARGB_8888);
+        }
+        Canvas canvas = new Canvas(bitmap);
+        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+        drawable.draw(canvas);
+        return bitmap;
+    }
 }
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index e360256..fabef76 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -1240,8 +1240,18 @@
     @Override
     public void uninstall(VersionedPackage versionedPackage, String callerPackageName, int flags,
                 IntentSender statusReceiver, int userId) {
+        uninstall(
+                versionedPackage,
+                callerPackageName,
+                flags,
+                statusReceiver,
+                userId,
+                Binder.getCallingUid());
+    }
+
+    void uninstall(VersionedPackage versionedPackage, String callerPackageName, int flags,
+            IntentSender statusReceiver, int userId, int callingUid) {
         final Computer snapshot = mPm.snapshotComputer();
-        final int callingUid = Binder.getCallingUid();
         snapshot.enforceCrossUserPermission(callingUid, userId, true, true, "uninstall");
         if (!PackageManagerServiceUtils.isRootOrShell(callingUid)) {
             mAppOps.checkPackage(callingUid, callerPackageName);
@@ -1257,7 +1267,7 @@
         final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext,
                 statusReceiver, versionedPackage.getPackageName(),
                 canSilentlyInstallPackage, userId);
-        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DELETE_PACKAGES)
+        if (mContext.checkCallingOrSelfPermission(Manifest.permission.DELETE_PACKAGES)
                     == PackageManager.PERMISSION_GRANTED) {
             // Sweet, call straight through!
             mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags);
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index e8e6470..0dd4111ad 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -3641,9 +3641,6 @@
     @GuardedBy("mLock")
     private void maybeStageFsveritySignatureLocked(File origFile, File targetFile,
             boolean fsVerityRequired) throws PackageManagerException {
-        if (com.android.server.security.Flags.deprecateFsvSig()) {
-            return;
-        }
         final File originalSignature = new File(
                 VerityUtils.getFsveritySignatureFilePath(origFile.getPath()));
         if (originalSignature.exists()) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index 0423249..2028231 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -543,9 +543,6 @@
 
     /** Returns true if standard APK Verity is enabled. */
     static boolean isApkVerityEnabled() {
-        if (com.android.server.security.Flags.deprecateFsvSig()) {
-            return false;
-        }
         return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R
                 || SystemProperties.getInt("ro.apk_verity.mode", FSVERITY_DISABLED)
                         == FSVERITY_ENABLED;
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index d9f1df5..a0c9cd1 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -2784,33 +2784,111 @@
     private int runGrantRevokePermission(boolean grant) throws RemoteException {
         int userId = UserHandle.USER_SYSTEM;
 
-        String opt = null;
+        String opt;
+        boolean allPermissions = false;
         while ((opt = getNextOption()) != null) {
             if (opt.equals("--user")) {
                 userId = UserHandle.parseUserArg(getNextArgRequired());
             }
+            if (opt.equals("--all-permissions")) {
+                allPermissions = true;
+            }
         }
 
         String pkg = getNextArg();
-        if (pkg == null) {
+        if (!allPermissions && pkg == null) {
             getErrPrintWriter().println("Error: no package specified");
             return 1;
         }
         String perm = getNextArg();
-        if (perm == null) {
+        if (!allPermissions && perm == null) {
             getErrPrintWriter().println("Error: no permission specified");
             return 1;
         }
+        if (allPermissions && perm != null) {
+            getErrPrintWriter().println("Error: permission specified but not expected");
+            return 1;
+        }
         final UserHandle translatedUser = UserHandle.of(translateUserId(userId,
                 UserHandle.USER_NULL, "runGrantRevokePermission"));
-        if (grant) {
-            mPermissionManager.grantRuntimePermission(pkg, perm, translatedUser);
+
+        List<PackageInfo> packageInfos;
+        if (pkg == null) {
+            packageInfos = mContext.getPackageManager().getInstalledPackages(
+                    PackageManager.GET_PERMISSIONS);
         } else {
-            mPermissionManager.revokeRuntimePermission(pkg, perm, translatedUser, null);
+            try {
+                packageInfos = Collections.singletonList(
+                        mContext.getPackageManager().getPackageInfo(pkg,
+                                PackageManager.GET_PERMISSIONS));
+            } catch (NameNotFoundException e) {
+                getErrPrintWriter().println("Error: package not found");
+                return 1;
+            }
+        }
+
+        for (PackageInfo packageInfo : packageInfos) {
+            List<String> permissions = Collections.singletonList(perm);
+            if (allPermissions) {
+                permissions = getRequestedRuntimePermissions(packageInfo);
+            }
+            for (String permission : permissions) {
+                if (grant) {
+                    try {
+                        mPermissionManager.grantRuntimePermission(packageInfo.packageName,
+                                permission,
+                                translatedUser);
+                    } catch (Exception e) {
+                        if (!allPermissions) {
+                            throw e;
+                        } else {
+                            Slog.w(TAG, "Could not grant permission " + permission, e);
+                        }
+                    }
+                } else {
+                    try {
+                        mPermissionManager.revokeRuntimePermission(packageInfo.packageName,
+                                permission,
+                                translatedUser, null);
+                    } catch (Exception e) {
+                        if (!allPermissions) {
+                            throw e;
+                        } else {
+                            Slog.w(TAG, "Could not grant permission " + permission, e);
+                        }
+                    }
+                }
+            }
         }
         return 0;
     }
 
+    private List<String> getRequestedRuntimePermissions(PackageInfo info) {
+        // No requested permissions
+        if (info.requestedPermissions == null) {
+            return new ArrayList<>();
+        }
+        List<String> result = new ArrayList<>();
+        PackageManager pm = mContext.getPackageManager();
+        // Iterate through requested permissions for denied ones
+        for (String permission : info.requestedPermissions) {
+            PermissionInfo pi = null;
+            try {
+                pi = pm.getPermissionInfo(permission, 0);
+            } catch (NameNotFoundException nnfe) {
+                // ignore
+            }
+            if (pi == null) {
+                continue;
+            }
+            if (pi.getProtection() != PermissionInfo.PROTECTION_DANGEROUS) {
+                continue;
+            }
+            result.add(permission);
+        }
+        return result;
+    }
+
     private int runResetPermissions() throws RemoteException {
         mLegacyPermissionManager.resetRuntimePermissions();
         return 0;
@@ -4643,11 +4721,15 @@
         pw.println("  get-distracting-restriction [--user USER_ID] PACKAGE [PACKAGE...]");
         pw.println("    Gets the specified restriction flags of given package(s) (of the user).");
         pw.println("");
-        pw.println("  grant [--user USER_ID] PACKAGE PERMISSION");
-        pw.println("  revoke [--user USER_ID] PACKAGE PERMISSION");
+        pw.println("  grant [--user USER_ID] [--all-permissions] PACKAGE PERMISSION");
+        pw.println("  revoke [--user USER_ID] [--all-permissions] PACKAGE PERMISSION");
         pw.println("    These commands either grant or revoke permissions to apps.  The permissions");
         pw.println("    must be declared as used in the app's manifest, be runtime permissions");
         pw.println("    (protection level dangerous), and the app targeting SDK greater than Lollipop MR1.");
+        pw.println("    Flags are:");
+        pw.println("    --user: Specifies the user for which the operation needs to be performed");
+        pw.println("    --all-permissions: If specified all the missing runtime permissions will");
+        pw.println("       be granted to the PACKAGE or to all the packages if none is specified.");
         pw.println("");
         pw.println("  set-permission-flags [--user USER_ID] PACKAGE PERMISSION [FLAGS..]");
         pw.println("  clear-permission-flags [--user USER_ID] PACKAGE PERMISSION [FLAGS..]");
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 88184c0..114f80d 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -35,6 +35,7 @@
 import android.os.UserHandle;
 import android.os.incremental.IncrementalManager;
 import android.service.pm.PackageProto;
+import android.service.pm.PackageProto.UserInfoProto.ArchiveState.ArchiveActivityInfo;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -1161,18 +1162,15 @@
         for (ArchiveState.ArchiveActivityInfo activityInfo : archiveState.getActivityInfos()) {
             long activityInfoToken = proto.start(
                     PackageProto.UserInfoProto.ArchiveState.ACTIVITY_INFOS);
-            proto.write(PackageProto.UserInfoProto.ArchiveState.ArchiveActivityInfo.TITLE,
-                    activityInfo.getTitle());
-            proto.write(
-                    PackageProto.UserInfoProto.ArchiveState.ArchiveActivityInfo.ICON_BITMAP_PATH,
-                    activityInfo.getIconBitmap().toAbsolutePath().toString());
-            proto.write(
-                    PackageProto
-                            .UserInfoProto
-                            .ArchiveState
-                            .ArchiveActivityInfo
-                            .MONOCHROME_ICON_BITMAP_PATH,
-                    activityInfo.getMonochromeIconBitmap().toAbsolutePath().toString());
+            proto.write(ArchiveActivityInfo.TITLE, activityInfo.getTitle());
+            if (activityInfo.getIconBitmap() != null) {
+                proto.write(ArchiveActivityInfo.ICON_BITMAP_PATH,
+                        activityInfo.getIconBitmap().toAbsolutePath().toString());
+            }
+            if (activityInfo.getMonochromeIconBitmap() != null) {
+                proto.write(ArchiveActivityInfo.MONOCHROME_ICON_BITMAP_PATH,
+                        activityInfo.getMonochromeIconBitmap().toAbsolutePath().toString());
+            }
             proto.end(activityInfoToken);
         }
 
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index 2aedf0d..8dec425 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -22,7 +22,6 @@
 import static android.os.storage.StorageManager.FLAG_STORAGE_CE;
 import static android.os.storage.StorageManager.FLAG_STORAGE_DE;
 import static android.os.storage.StorageManager.FLAG_STORAGE_EXTERNAL;
-
 import static com.android.server.pm.InstructionSets.getDexCodeInstructionSets;
 import static com.android.server.pm.PackageManagerService.DEBUG_INSTALL;
 import static com.android.server.pm.PackageManagerService.DEBUG_REMOVE;
@@ -400,6 +399,21 @@
                         changedUsers);
                 mPm.postPreferredActivityChangedBroadcast(UserHandle.USER_ALL);
             }
+        } else if (!deletedPs.isSystem() && outInfo != null && !outInfo.mIsUpdate
+                && outInfo.mRemovedUsers != null) {
+            // For non-system uninstalls with DELETE_KEEP_DATA, set the installed state to false
+            // for affected users. This does not apply to app updates where the old apk is replaced
+            // but the old data remains.
+            if (DEBUG_REMOVE) {
+                Slog.d(TAG, "Updating installed state to false because of DELETE_KEEP_DATA");
+            }
+            for (int userId : outInfo.mRemovedUsers) {
+                if (DEBUG_REMOVE) {
+                    final boolean wasInstalled = deletedPs.getInstalled(userId);
+                    Slog.d(TAG, "    user " + userId + ": " + wasInstalled + " => " + false);
+                }
+                deletedPs.setInstalled(/* installed= */ false, userId);
+            }
         }
         // make sure to preserve per-user installed state if this removal was just
         // a downgrade of a system app to the factory package
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 1137681..111a32d 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -2062,8 +2062,9 @@
             if (tagName.equals(TAG_ARCHIVE_ACTIVITY_INFO)) {
                 String title = parser.getAttributeValue(null,
                         ATTR_ARCHIVE_ACTIVITY_TITLE);
-                Path iconPath = Path.of(parser.getAttributeValue(null,
-                        ATTR_ARCHIVE_ICON_PATH));
+                String iconAttribute = parser.getAttributeValue(null,
+                        ATTR_ARCHIVE_ICON_PATH);
+                Path iconPath = iconAttribute == null ? null : Path.of(iconAttribute);
                 String monochromeAttribute = parser.getAttributeValue(null,
                         ATTR_ARCHIVE_MONOCHROME_ICON_PATH);
                 Path monochromeIconPath = monochromeAttribute == null ? null : Path.of(
@@ -2447,8 +2448,10 @@
         for (ArchiveState.ArchiveActivityInfo activityInfo : archiveState.getActivityInfos()) {
             serializer.startTag(null, TAG_ARCHIVE_ACTIVITY_INFO);
             serializer.attribute(null, ATTR_ARCHIVE_ACTIVITY_TITLE, activityInfo.getTitle());
-            serializer.attribute(null, ATTR_ARCHIVE_ICON_PATH,
-                    activityInfo.getIconBitmap().toAbsolutePath().toString());
+            if (activityInfo.getIconBitmap() != null) {
+                serializer.attribute(null, ATTR_ARCHIVE_ICON_PATH,
+                        activityInfo.getIconBitmap().toAbsolutePath().toString());
+            }
             if (activityInfo.getMonochromeIconBitmap() != null) {
                 serializer.attribute(null, ATTR_ARCHIVE_MONOCHROME_ICON_PATH,
                         activityInfo.getMonochromeIconBitmap().toAbsolutePath().toString());
diff --git a/services/core/java/com/android/server/pm/pkg/ArchiveState.java b/services/core/java/com/android/server/pm/pkg/ArchiveState.java
index d44ae16..4916a4a 100644
--- a/services/core/java/com/android/server/pm/pkg/ArchiveState.java
+++ b/services/core/java/com/android/server/pm/pkg/ArchiveState.java
@@ -56,8 +56,11 @@
         @NonNull
         private final String mTitle;
 
-        /** The path to the stored icon of the activity in the app's locale. */
-        @NonNull
+        /**
+         * The path to the stored icon of the activity in the app's locale. Null if the app does
+         * not define any icon (default icon would be shown on the launcher).
+         */
+        @Nullable
         private final Path mIconBitmap;
 
         /** See {@link #mIconBitmap}. Only set if the app defined a monochrome icon. */
@@ -85,21 +88,20 @@
          * @param title
          *   Corresponds to the activity's android:label in the app's locale.
          * @param iconBitmap
-         *   The path to the stored icon of the activity in the app's locale.
+         *   The path to the stored icon of the activity in the app's locale. Null if the app does
+         *   not define any icon (default icon would be shown on the launcher).
          * @param monochromeIconBitmap
          *   See {@link #mIconBitmap}. Only set if the app defined a monochrome icon.
          */
         @DataClass.Generated.Member
         public ArchiveActivityInfo(
                 @NonNull String title,
-                @NonNull Path iconBitmap,
+                @Nullable Path iconBitmap,
                 @Nullable Path monochromeIconBitmap) {
             this.mTitle = title;
             com.android.internal.util.AnnotationValidations.validate(
                     NonNull.class, null, mTitle);
             this.mIconBitmap = iconBitmap;
-            com.android.internal.util.AnnotationValidations.validate(
-                    NonNull.class, null, mIconBitmap);
             this.mMonochromeIconBitmap = monochromeIconBitmap;
 
             // onConstructed(); // You can define this method to get a callback
@@ -114,10 +116,11 @@
         }
 
         /**
-         * The path to the stored icon of the activity in the app's locale.
+         * The path to the stored icon of the activity in the app's locale. Null if the app does
+         * not define any icon (default icon would be shown on the launcher).
          */
         @DataClass.Generated.Member
-        public @NonNull Path getIconBitmap() {
+        public @Nullable Path getIconBitmap() {
             return mIconBitmap;
         }
 
@@ -174,10 +177,10 @@
         }
 
         @DataClass.Generated(
-                time = 1689169065133L,
+                time = 1693590309015L,
                 codegenVersion = "1.0.23",
                 sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/ArchiveState.java",
-                inputSignatures = "private final @android.annotation.NonNull java.lang.String mTitle\nprivate final @android.annotation.NonNull java.nio.file.Path mIconBitmap\nprivate final @android.annotation.Nullable java.nio.file.Path mMonochromeIconBitmap\nclass ArchiveActivityInfo extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true)")
+                inputSignatures = "private final @android.annotation.NonNull java.lang.String mTitle\nprivate final @android.annotation.Nullable java.nio.file.Path mIconBitmap\nprivate final @android.annotation.Nullable java.nio.file.Path mMonochromeIconBitmap\nclass ArchiveActivityInfo extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true)")
         @Deprecated
         private void __metadata() {}
 
@@ -292,7 +295,7 @@
     }
 
     @DataClass.Generated(
-            time = 1689169065144L,
+            time = 1693590309027L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/ArchiveState.java",
             inputSignatures = "private final @android.annotation.NonNull java.util.List<com.android.server.pm.pkg.ArchiveActivityInfo> mActivityInfos\nprivate final @android.annotation.NonNull java.lang.String mInstallerTitle\nclass ArchiveState extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true)")
diff --git a/services/core/java/com/android/server/security/FileIntegrityService.java b/services/core/java/com/android/server/security/FileIntegrityService.java
index 0879d95..3aed6e3 100644
--- a/services/core/java/com/android/server/security/FileIntegrityService.java
+++ b/services/core/java/com/android/server/security/FileIntegrityService.java
@@ -90,13 +90,6 @@
                 @NonNull String packageName) {
             checkCallerPermission(packageName);
 
-            if (Flags.deprecateFsvSig()) {
-                // When deprecated, stop telling the caller that any app source certificate is
-                // trusted on the current device. This behavior is also consistent with devices
-                // without this feature support.
-                return false;
-            }
-
             try {
                 if (!VerityUtils.isFsVeritySupported()) {
                     return false;
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index 9905ddf..635e11b 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -159,10 +159,26 @@
     private VirtualDeviceManagerInternal mVirtualDeviceManager;
 
     private enum TrustState {
-        UNTRUSTED, // the phone is not unlocked by any trustagents
-        TRUSTABLE, // the phone is in a semi-locked state that can be unlocked if
-        // FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE is passed and a trustagent is trusted
-        TRUSTED // the phone is unlocked
+        // UNTRUSTED means that TrustManagerService is currently *not* giving permission for the
+        // user's Keyguard to be dismissed, and grants of trust by trust agents are remembered in
+        // the corresponding TrustAgentWrapper but are not recognized until the device is unlocked
+        // for the user.  I.e., if the device is locked and the state is UNTRUSTED, it cannot be
+        // unlocked by a trust agent.  Automotive devices are an exception; grants of trust are
+        // always recognized on them.
+        UNTRUSTED,
+
+        // TRUSTABLE is the same as UNTRUSTED except that new grants of trust using
+        // FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE are recognized for moving to TRUSTED.  I.e., if
+        // the device is locked and the state is TRUSTABLE, it can be unlocked by a trust agent,
+        // provided that the trust agent chooses to use Active Unlock.  The TRUSTABLE state is only
+        // possible as a result of a downgrade from TRUSTED, after a trust agent used
+        // FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE in its most recent grant.
+        TRUSTABLE,
+
+        // TRUSTED means that TrustManagerService is currently giving permission for the user's
+        // Keyguard to be dismissed.  This implies that the device is unlocked for the user (where
+        // the case of Keyguard showing but dismissible just with swipe counts as "unlocked").
+        TRUSTED
     };
 
     @GuardedBy("mUserTrustState")
@@ -744,6 +760,12 @@
         }
     }
 
+    private TrustState getUserTrustStateInner(int userId) {
+        synchronized (mUserTrustState) {
+            return mUserTrustState.get(userId, TrustState.UNTRUSTED);
+        }
+    }
+
     boolean isDeviceLockedInner(int userId) {
         synchronized (mDeviceLockedForUser) {
             return mDeviceLockedForUser.get(userId, true);
@@ -806,7 +828,12 @@
                 continue;
             }
 
-            boolean trusted = aggregateIsTrusted(id);
+            final boolean trusted;
+            if (android.security.Flags.fixUnlockedDeviceRequiredKeys()) {
+                trusted = getUserTrustStateInner(id) == TrustState.TRUSTED;
+            } else {
+                trusted = aggregateIsTrusted(id);
+            }
             boolean showingKeyguard = true;
             boolean biometricAuthenticated = false;
             boolean currentUserIsUnlocked = false;
@@ -1627,7 +1654,7 @@
             if (isCurrent) {
                 fout.print(" (current)");
             }
-            fout.print(": trusted=" + dumpBool(aggregateIsTrusted(user.id)));
+            fout.print(": trustState=" + getUserTrustStateInner(user.id));
             fout.print(", trustManaged=" + dumpBool(aggregateIsTrustManaged(user.id)));
             fout.print(", deviceLocked=" + dumpBool(isDeviceLockedInner(user.id)));
             fout.print(", isActiveUnlockRunning=" + dumpBool(
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index d430dda..b823e73 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -1170,9 +1170,7 @@
                 fullscreenRequest, r);
         reportMultiwindowFullscreenRequestValidatingResult(callback, validateResult);
         if (validateResult != RESULT_APPROVED) {
-            if (queued) {
-                transition.abort();
-            }
+            transition.abort();
             return;
         }
         transition.collect(topFocusedRootTask);
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index fd42077..4aea70c 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -68,7 +68,6 @@
 import static android.view.WindowManager.TRANSIT_PIP;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_TO_LAUNCHER_CLEAR_SNAPSHOT;
-
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_DREAM;
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_FOCUS;
@@ -3660,6 +3659,9 @@
             synchronized (mGlobalLock) {
                 if (r.getParent() == null) {
                     Slog.e(TAG, "Skip enterPictureInPictureMode, destroyed " + r);
+                    if (transition != null) {
+                        transition.abort();
+                    }
                     return;
                 }
                 EventLogTags.writeWmEnterPip(r.mUserId, System.identityHashCode(r),
@@ -5628,6 +5630,15 @@
         }
     }
 
+    void registerCompatScaleProvider(@CompatScaleProvider.CompatScaleModeOrderId int id,
+            @NonNull CompatScaleProvider provider) {
+        mCompatModePackages.registerCompatScaleProvider(id, provider);
+    }
+
+    void unregisterCompatScaleProvider(@CompatScaleProvider.CompatScaleModeOrderId int id) {
+        mCompatModePackages.unregisterCompatScaleProvider(id);
+    }
+
     /**
      * Returns {@code true} if the process represented by the pid passed as argument is
      * instrumented and the instrumentation source was granted with the permission also
diff --git a/services/core/java/com/android/server/wm/CompatModePackages.java b/services/core/java/com/android/server/wm/CompatModePackages.java
index c6978fd..e906b18 100644
--- a/services/core/java/com/android/server/wm/CompatModePackages.java
+++ b/services/core/java/com/android/server/wm/CompatModePackages.java
@@ -20,7 +20,10 @@
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS;
+import static com.android.server.wm.CompatScaleProvider.COMPAT_SCALE_MODE_SYSTEM_FIRST;
+import static com.android.server.wm.CompatScaleProvider.COMPAT_SCALE_MODE_SYSTEM_LAST;
 
+import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.app.AppGlobals;
 import android.app.GameManagerInternal;
@@ -32,6 +35,7 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.res.CompatibilityInfo;
+import android.content.res.CompatibilityInfo.CompatScale;
 import android.content.res.Configuration;
 import android.os.Build;
 import android.os.Handler;
@@ -332,6 +336,8 @@
     private final HashMap<String, Integer> mPackages = new HashMap<>();
     private final CompatHandler mHandler;
 
+    private final SparseArray<CompatScaleProvider> mProviders = new SparseArray<>();
+
     public CompatModePackages(ActivityTaskManagerService service, File systemDir, Handler handler) {
         mService = service;
         mFile = new AtomicFile(new File(systemDir, "packages-compat.xml"), "compat-mode");
@@ -441,13 +447,38 @@
 
     public CompatibilityInfo compatibilityInfoForPackageLocked(ApplicationInfo ai) {
         final boolean forceCompat = getPackageCompatModeEnabledLocked(ai);
-        final float compatScale = getCompatScale(ai.packageName, ai.uid);
+        final CompatScale compatScale = getCompatScaleFromProvider(ai.packageName, ai.uid);
+        final float appScale = compatScale != null
+                ? compatScale.mScaleFactor
+                : getCompatScale(ai.packageName, ai.uid, /* checkProvider= */ false);
+        final float densityScale = compatScale != null ? compatScale.mDensityScaleFactor : 1f;
         final Configuration config = mService.getGlobalConfiguration();
         return new CompatibilityInfo(ai, config.screenLayout, config.smallestScreenWidthDp,
-                forceCompat, compatScale);
+                forceCompat, appScale, densityScale);
     }
 
     float getCompatScale(String packageName, int uid) {
+        return getCompatScale(packageName, uid, /* checkProvider= */ true);
+    }
+
+    private CompatScale getCompatScaleFromProvider(String packageName, int uid) {
+        for (int i = 0; i < mProviders.size(); i++) {
+            final CompatScaleProvider provider = mProviders.valueAt(i);
+            final CompatScale compatScale = provider.getCompatScale(packageName, uid);
+            if (compatScale != null) {
+                return compatScale;
+            }
+        }
+        return null;
+    }
+
+    private float getCompatScale(String packageName, int uid, boolean checkProviders) {
+        if (checkProviders) {
+            final CompatScale compatScale = getCompatScaleFromProvider(packageName, uid);
+            if (compatScale != null) {
+                return compatScale.mScaleFactor;
+            }
+        }
         final UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
         if (mGameManager == null) {
             mGameManager = LocalServices.getService(GameManagerInternal.class);
@@ -487,6 +518,36 @@
         return 1f;
     }
 
+    void registerCompatScaleProvider(@CompatScaleProvider.CompatScaleModeOrderId int id,
+            @NonNull CompatScaleProvider provider) {
+        synchronized (mService.mGlobalLock) {
+            if (mProviders.contains(id)) {
+                throw new IllegalArgumentException("Duplicate id provided: " + id);
+            }
+            if (provider == null) {
+                throw new IllegalArgumentException("The passed CompatScaleProvider "
+                        + "can not be null");
+            }
+            if (!CompatScaleProvider.isValidOrderId(id)) {
+                throw new IllegalArgumentException(
+                        "Provided id " + id + " is not in range of valid ids for system "
+                                + "services [" + COMPAT_SCALE_MODE_SYSTEM_FIRST + ","
+                                + COMPAT_SCALE_MODE_SYSTEM_LAST + "]");
+            }
+            mProviders.put(id, provider);
+        }
+    }
+
+    void unregisterCompatScaleProvider(@CompatScaleProvider.CompatScaleModeOrderId int id) {
+        synchronized (mService.mGlobalLock) {
+            if (!mProviders.contains(id)) {
+                throw new IllegalArgumentException(
+                        "CompatScaleProvider with id (" + id + ") is not registered");
+            }
+            mProviders.remove(id);
+        }
+    }
+
     private static float getScalingFactor(String packageName, UserHandle userHandle) {
         if (CompatChanges.isChangeEnabled(DOWNSCALE_90, packageName, userHandle)) {
             return 0.9f;
diff --git a/services/core/java/com/android/server/wm/CompatScaleProvider.java b/services/core/java/com/android/server/wm/CompatScaleProvider.java
new file mode 100644
index 0000000..5474ece
--- /dev/null
+++ b/services/core/java/com/android/server/wm/CompatScaleProvider.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.wm;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.CompatibilityInfo.CompatScale;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An interface for services that need to provide compatibility scale different than
+ * the default android compatibility.
+ */
+public interface CompatScaleProvider {
+
+    /**
+     * The unique id of each provider registered by a system service which determines the order
+     * it will execute in.
+     */
+    @IntDef(prefix = { "COMPAT_SCALE_MODE_" }, value = {
+        // Order Ids for system services
+        COMPAT_SCALE_MODE_SYSTEM_FIRST,
+        COMPAT_SCALE_MODE_GAME,
+        COMPAT_SCALE_MODE_PRODUCT,
+        COMPAT_SCALE_MODE_SYSTEM_LAST, // Update this when adding new ids
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface CompatScaleModeOrderId {}
+
+    /**
+     * The first id, used by the framework to determine the valid range of ids.
+     * @hide
+     */
+    int COMPAT_SCALE_MODE_SYSTEM_FIRST = 0;
+
+    /**
+     * TODO(b/295207384)
+     * The identifier for {@link android.app.GameManagerInternal} provider
+     * @hide
+     */
+    int COMPAT_SCALE_MODE_GAME = 1;
+
+    /**
+     * The identifier for a provider which is specific to the type of android product like
+     * Automotive, Wear, TV etc.
+     * @hide
+     */
+    int COMPAT_SCALE_MODE_PRODUCT = 2;
+
+    /**
+     * The final id, used by the framework to determine the valid range of ids. Update this when
+     * adding new ids.
+     * @hide
+     */
+    int COMPAT_SCALE_MODE_SYSTEM_LAST = COMPAT_SCALE_MODE_PRODUCT;
+
+    /**
+     * Returns {@code true} if the id is in the range of valid system services
+     * @hide
+     */
+    static boolean isValidOrderId(int id) {
+        return (id >= COMPAT_SCALE_MODE_SYSTEM_FIRST && id <= COMPAT_SCALE_MODE_SYSTEM_LAST);
+    }
+
+    /**
+     * @return an instance of {@link CompatScale} to apply for the given package
+     */
+    @Nullable
+    CompatScale getCompatScale(@NonNull String packageName, int uid);
+}
diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java
index 5aa7c97..f0e4149 100644
--- a/services/core/java/com/android/server/wm/ContentRecorder.java
+++ b/services/core/java/com/android/server/wm/ContentRecorder.java
@@ -35,6 +35,7 @@
 import android.os.ServiceManager;
 import android.provider.DeviceConfig;
 import android.view.ContentRecordingSession;
+import android.view.ContentRecordingSession.RecordContent;
 import android.view.Display;
 import android.view.SurfaceControl;
 
@@ -84,6 +85,7 @@
     /**
      * The last configuration orientation.
      */
+    @Configuration.Orientation
     private int mLastOrientation = ORIENTATION_UNDEFINED;
 
     ContentRecorder(@NonNull DisplayContent displayContent) {
@@ -156,7 +158,8 @@
             // Retrieve the size of the region to record, and continue with the update
             // if the bounds or orientation has changed.
             final Rect recordedContentBounds = mRecordedWindowContainer.getBounds();
-            int recordedContentOrientation = mRecordedWindowContainer.getOrientation();
+            @Configuration.Orientation int recordedContentOrientation =
+                    mRecordedWindowContainer.getConfiguration().orientation;
             if (!mLastRecordedBounds.equals(recordedContentBounds)
                     || lastOrientation != recordedContentOrientation) {
                 Point surfaceSize = fetchSurfaceSizeIfPresent();
@@ -356,7 +359,7 @@
      */
     @Nullable
     private WindowContainer retrieveRecordedWindowContainer() {
-        final int contentToRecord = mContentRecordingSession.getContentToRecord();
+        @RecordContent final int contentToRecord = mContentRecordingSession.getContentToRecord();
         final IBinder tokenToRecord = mContentRecordingSession.getTokenToRecord();
         switch (contentToRecord) {
             case RECORD_CONTENT_DISPLAY:
@@ -472,6 +475,12 @@
             shiftedY = (surfaceSize.y - scaledHeight) / 2;
         }
 
+        ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
+                "Content Recording: Apply transformations of shift %d x %d, scale %f, crop %d x "
+                        + "%d for display %d",
+                shiftedX, shiftedY, scale, recordedContentBounds.width(),
+                recordedContentBounds.height(), mDisplayContent.getDisplayId());
+
         transaction
                 // Crop the area to capture to exclude the 'extra' wallpaper that is used
                 // for parallax (b/189930234).
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 707b779..395ab3a 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -269,6 +269,8 @@
 
     private boolean mIsFreeformWindowOverlappingWithNavBar;
 
+    private @InsetsType int mForciblyShownTypes;
+
     private boolean mIsImmersiveMode;
 
     // The windows we were told about in focusChanged.
@@ -1402,6 +1404,7 @@
         mAllowLockscreenWhenOn = false;
         mShowingDream = false;
         mIsFreeformWindowOverlappingWithNavBar = false;
+        mForciblyShownTypes = 0;
     }
 
     /**
@@ -1459,6 +1462,10 @@
             }
         }
 
+        if (win.mSession.mCanForceShowingInsets) {
+            mForciblyShownTypes |= win.mAttrs.forciblyShownTypes;
+        }
+
         if (!affectsSystemUi) {
             return;
         }
@@ -1640,6 +1647,10 @@
         mService.mPolicy.setAllowLockscreenWhenOn(getDisplayId(), mAllowLockscreenWhenOn);
     }
 
+    boolean areTypesForciblyShownTransiently(@InsetsType int types) {
+        return (mForciblyShownTypes & types) == types;
+    }
+
     /**
      * Applies the keyguard policy to a specific window.
      *
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 835c92d..03f025b 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -23,8 +23,6 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.view.InsetsSource.ID_IME;
 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
 
 import android.annotation.NonNull;
@@ -473,7 +471,7 @@
             // we will dispatch the real visibility of status bar to the client.
             return mPermanentControlTarget;
         }
-        if (forceShowsStatusBarTransiently() && !fake) {
+        if (mPolicy.areTypesForciblyShownTransiently(Type.statusBars()) && !fake) {
             // Status bar is forcibly shown transiently, and its new visibility won't be
             // dispatched to the client so that we can keep the layout stable. We will dispatch the
             // fake control to the client, so that it can re-show the bar during this scenario.
@@ -505,7 +503,7 @@
         if (imeWin != null && imeWin.isVisible() && !mHideNavBarForKeyboard) {
             // Force showing navigation bar while IME is visible and if navigation bar is not
             // configured to be hidden by the IME.
-            return null;
+            return mPermanentControlTarget;
         }
         if (!fake && isTransient(Type.navigationBars())) {
             return mTransientControlTarget;
@@ -533,7 +531,7 @@
             // bar, and we will dispatch the real visibility of navigation bar to the client.
             return mPermanentControlTarget;
         }
-        if (forceShowsNavigationBarTransiently() && !fake) {
+        if (mPolicy.areTypesForciblyShownTransiently(Type.navigationBars()) && !fake) {
             // Navigation bar is forcibly shown transiently, and its new visibility won't be
             // dispatched to the client so that we can keep the layout stable. We will dispatch the
             // fake control to the client, so that it can re-show the bar during this scenario.
@@ -603,17 +601,6 @@
                 && focusedWin.getAttrs().type <= WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
     }
 
-    private boolean forceShowsStatusBarTransiently() {
-        final WindowState win = mPolicy.getStatusBar();
-        return win != null && (win.mAttrs.privateFlags & PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR) != 0;
-    }
-
-    private boolean forceShowsNavigationBarTransiently() {
-        final WindowState win = mPolicy.getNotificationShade();
-        return win != null
-                && (win.mAttrs.privateFlags & PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION) != 0;
-    }
-
     private void dispatchTransientSystemBarsVisibilityChanged(
             @Nullable WindowState focusedWindow,
             boolean areVisible,
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 1845ae8..0c45eea 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -23,6 +23,7 @@
 import static android.Manifest.permission.SET_UNRESTRICTED_GESTURE_EXCLUSION;
 import static android.Manifest.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS;
 import static android.Manifest.permission.START_TASKS_FROM_RECENTS;
+import static android.Manifest.permission.STATUS_BAR_SERVICE;
 import static android.Manifest.permission.SYSTEM_APPLICATION_OVERLAY;
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY;
@@ -108,6 +109,7 @@
     private final ArraySet<WindowSurfaceController> mAlertWindowSurfaces = new ArraySet<>();
     private final DragDropController mDragDropController;
     final boolean mCanAddInternalSystemWindow;
+    final boolean mCanForceShowingInsets;
     private final boolean mCanStartTasksFromRecents;
 
     final boolean mCanCreateSystemApplicationOverlay;
@@ -131,6 +133,9 @@
         mLastReportedAnimatorScale = service.getCurrentAnimatorScale();
         mCanAddInternalSystemWindow = service.mContext.checkCallingOrSelfPermission(
                 INTERNAL_SYSTEM_WINDOW) == PERMISSION_GRANTED;
+        mCanForceShowingInsets = service.mAtmService.isCallerRecents(mUid)
+                || service.mContext.checkCallingOrSelfPermission(STATUS_BAR_SERVICE)
+                == PERMISSION_GRANTED;
         mCanHideNonSystemOverlayWindows = service.mContext.checkCallingOrSelfPermission(
                 HIDE_NON_SYSTEM_OVERLAY_WINDOWS) == PERMISSION_GRANTED
                 || service.mContext.checkCallingOrSelfPermission(HIDE_OVERLAY_WINDOWS)
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index a6c6491..843e6d1 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1487,6 +1487,11 @@
             return;
         }
 
+        if (mState != STATE_STARTED) {
+            Slog.e(TAG, "Playing a Transition which hasn't started! #" + mSyncId + " This will "
+                    + "likely cause an exception in Shell");
+        }
+
         mState = STATE_PLAYING;
         mStartTransaction = transaction;
         mFinishTransaction = mController.mAtm.mWindowManager.mTransactionFactory.get();
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 55deb22..176bc283 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -42,6 +42,7 @@
 #include <android_view_VerifiedMotionEvent.h>
 #include <batteryservice/include/batteryservice/BatteryServiceConstants.h>
 #include <binder/IServiceManager.h>
+#include <com_android_input_flags.h>
 #include <input/Input.h>
 #include <input/PointerController.h>
 #include <input/SpriteController.h>
@@ -81,6 +82,8 @@
 static constexpr std::chrono::milliseconds MAX_VIBRATE_PATTERN_DELAY_MILLIS =
         std::chrono::duration_cast<std::chrono::milliseconds>(MAX_VIBRATE_PATTERN_DELAY);
 
+namespace input_flags = com::android::input::flags;
+
 namespace android {
 
 // The exponent used to calculate the pointer speed scaling factor.
@@ -733,7 +736,7 @@
         ensureSpriteControllerLocked();
 
         static const bool ENABLE_POINTER_CHOREOGRAPHER =
-                sysprop::InputProperties::enable_pointer_choreographer().value_or(false);
+                input_flags::enable_pointer_choreographer();
 
         // Disable the functionality of the legacy PointerController if PointerChoreographer is
         // enabled.
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index b4a66bd..76b41b7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -1895,6 +1895,13 @@
         assertProcStates(app2, PROCESS_STATE_FOREGROUND_SERVICE, PERCEPTIBLE_APP_ADJ,
                 SCHED_GROUP_DEFAULT);
         assertBfsl(app2);
+
+        bindService(client2, app1, null, 0, mock(IBinder.class));
+        bindService(app1, client2, null, 0, mock(IBinder.class));
+        client2.mServices.setHasForegroundServices(false, 0, /* hasNoneType=*/false);
+        updateOomAdj(app1, client1, client2);
+        assertProcStates(app1, PROCESS_STATE_IMPORTANT_FOREGROUND, VISIBLE_APP_ADJ,
+                SCHED_GROUP_TOP_APP);
     }
 
     @SuppressWarnings("GuardedBy")
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverServiceTest.java
index 80576a6..60b28d3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverServiceTest.java
@@ -25,6 +25,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
@@ -34,11 +35,15 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentSender;
+import android.content.pm.ActivityInfo;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageArchiver;
+import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
 import android.content.pm.VersionedPackage;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
@@ -62,6 +67,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.io.IOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
@@ -74,9 +80,10 @@
     private static final String PACKAGE = "com.example";
     private static final String CALLER_PACKAGE = "com.caller";
     private static final String INSTALLER_PACKAGE = "com.installer";
+    private static final Path ICON_PATH = Path.of("icon.png");
 
     @Rule
-    public final MockSystemRule mMockSystem = new MockSystemRule();
+    public final MockSystemRule rule = new MockSystemRule();
 
     @Mock
     private IntentSender mIntentSender;
@@ -87,9 +94,13 @@
     @Mock
     private LauncherApps mLauncherApps;
     @Mock
+    private PackageManager mPackageManager;
+    @Mock
     private PackageInstallerService mInstallerService;
     @Mock
     private PackageStateInternal mPackageState;
+    @Mock
+    private Bitmap mIcon;
 
     private final InstallSource mInstallSource =
             InstallSource.create(
@@ -102,7 +113,6 @@
                     /* packageSource= */ 0);
 
     private final List<LauncherActivityInfo> mLauncherActivityInfos = createLauncherActivities();
-
     private final int mUserId = UserHandle.CURRENT.getIdentifier();
 
     private PackageUserStateImpl mUserState;
@@ -114,10 +124,10 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mMockSystem.system().stageNominalSystemState();
-        when(mMockSystem.mocks().getInjector().getPackageInstallerService()).thenReturn(
+        rule.system().stageNominalSystemState();
+        when(rule.mocks().getInjector().getPackageInstallerService()).thenReturn(
                 mInstallerService);
-        PackageManagerService pm = spy(new PackageManagerService(mMockSystem.mocks().getInjector(),
+        PackageManagerService pm = spy(new PackageManagerService(rule.mocks().getInjector(),
                 /* factoryTest= */false,
                 MockSystem.Companion.getDEFAULT_VERSION_INFO().fingerprint,
                 /* isEngBuild= */ false,
@@ -132,18 +142,27 @@
         when(mPackageState.getPackageName()).thenReturn(PACKAGE);
         when(mPackageState.getInstallSource()).thenReturn(mInstallSource);
         mPackageSetting = createBasicPackageSetting();
-        when(mMockSystem.mocks().getSettings().getPackageLPr(eq(PACKAGE))).thenReturn(
+        when(rule.mocks().getSettings().getPackageLPr(eq(PACKAGE))).thenReturn(
                 mPackageSetting);
         mUserState = new PackageUserStateImpl().setInstalled(true);
         mPackageSetting.setUserState(mUserId, mUserState);
         when(mPackageState.getUserStateOrDefault(eq(mUserId))).thenReturn(mUserState);
+
         when(mContext.getSystemService(LauncherApps.class)).thenReturn(mLauncherApps);
         when(mLauncherApps.getActivityList(eq(PACKAGE), eq(UserHandle.CURRENT))).thenReturn(
                 mLauncherActivityInfos);
         doReturn(mComputer).when(pm).snapshotComputer();
         when(mComputer.getPackageUid(eq(CALLER_PACKAGE), eq(0L), eq(mUserId))).thenReturn(
                 Binder.getCallingUid());
-        mArchiveService = new PackageArchiverService(mContext, pm);
+
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mPackageManager.getResourcesForApplication(eq(PACKAGE))).thenReturn(
+                mock(Resources.class));
+        when(mIcon.compress(eq(Bitmap.CompressFormat.PNG), eq(100), any())).thenReturn(true);
+
+        mArchiveService = spy(new PackageArchiverService(mContext, pm));
+        doReturn(ICON_PATH).when(mArchiveService).storeIcon(eq(PACKAGE),
+                any(LauncherActivityInfo.class), eq(mUserId));
     }
 
     @Test
@@ -175,15 +194,20 @@
     }
 
     @Test
-    public void archiveApp_packageNotInstalledForUser() {
+    public void archiveApp_packageNotInstalledForUser() throws IntentSender.SendIntentException {
         mPackageSetting.modifyUserState(UserHandle.CURRENT.getIdentifier()).setInstalled(false);
 
-        Exception e = assertThrows(
-                ParcelableException.class,
-                () -> mArchiveService.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender,
-                        UserHandle.CURRENT));
-        assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class);
-        assertThat(e.getCause()).hasMessageThat().isEqualTo(
+        mArchiveService.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT);
+        rule.mocks().getHandler().flush();
+
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mIntentSender).sendIntent(any(), anyInt(), intentCaptor.capture(), any(), any(),
+                any(), any());
+        Intent value = intentCaptor.getValue();
+        assertThat(value.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)).isEqualTo(PACKAGE);
+        assertThat(value.getIntExtra(PackageInstaller.EXTRA_STATUS, 0)).isEqualTo(
+                PackageInstaller.STATUS_FAILURE);
+        assertThat(value.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)).isEqualTo(
                 String.format("Package %s not found.", PACKAGE));
     }
 
@@ -223,13 +247,34 @@
     }
 
     @Test
+    public void archiveApp_storeIconFails() throws IntentSender.SendIntentException, IOException {
+        IOException e = new IOException("IO");
+        doThrow(e).when(mArchiveService).storeIcon(eq(PACKAGE),
+                any(LauncherActivityInfo.class), eq(mUserId));
+
+        mArchiveService.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT);
+        rule.mocks().getHandler().flush();
+
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mIntentSender).sendIntent(any(), anyInt(), intentCaptor.capture(), any(), any(),
+                any(), any());
+        Intent value = intentCaptor.getValue();
+        assertThat(value.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)).isEqualTo(PACKAGE);
+        assertThat(value.getIntExtra(PackageInstaller.EXTRA_STATUS, 0)).isEqualTo(
+                PackageInstaller.STATUS_FAILURE);
+        assertThat(value.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)).isEqualTo(
+                e.toString());
+    }
+
+    @Test
     public void archiveApp_success() {
         mArchiveService.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT);
+        rule.mocks().getHandler().flush();
 
         verify(mInstallerService).uninstall(
                 eq(new VersionedPackage(PACKAGE, PackageManager.VERSION_CODE_HIGHEST)),
                 eq(CALLER_PACKAGE), eq(DELETE_KEEP_DATA), eq(mIntentSender),
-                eq(UserHandle.CURRENT.getIdentifier()));
+                eq(UserHandle.CURRENT.getIdentifier()), anyInt());
         assertThat(mPackageSetting.readUserState(
                 UserHandle.CURRENT.getIdentifier()).getArchiveState()).isEqualTo(
                 createArchiveState());
@@ -305,7 +350,7 @@
         mUserState.setArchiveState(createArchiveState()).setInstalled(false);
 
         mArchiveService.requestUnarchive(PACKAGE, CALLER_PACKAGE, UserHandle.CURRENT);
-        mMockSystem.mocks().getHandler().flush();
+        rule.mocks().getHandler().flush();
 
         ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
         verify(mContext).sendOrderedBroadcastAsUser(
@@ -331,20 +376,22 @@
     private static ArchiveState createArchiveState() {
         List<ArchiveState.ArchiveActivityInfo> activityInfos = new ArrayList<>();
         for (LauncherActivityInfo mainActivity : createLauncherActivities()) {
-            // TODO(b/278553670) Extract and store launcher icons
             ArchiveState.ArchiveActivityInfo activityInfo = new ArchiveState.ArchiveActivityInfo(
                     mainActivity.getLabel().toString(),
-                    Path.of("/TODO"), null);
+                    ICON_PATH, null);
             activityInfos.add(activityInfo);
         }
         return new ArchiveState(activityInfos, INSTALLER_PACKAGE);
     }
 
     private static List<LauncherActivityInfo> createLauncherActivities() {
+        ActivityInfo activityInfo = mock(ActivityInfo.class);
         LauncherActivityInfo activity1 = mock(LauncherActivityInfo.class);
         when(activity1.getLabel()).thenReturn("activity1");
+        when(activity1.getActivityInfo()).thenReturn(activityInfo);
         LauncherActivityInfo activity2 = mock(LauncherActivityInfo.class);
         when(activity2.getLabel()).thenReturn("activity2");
+        when(activity2.getActivityInfo()).thenReturn(activityInfo);
         return List.of(activity1, activity2);
     }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java
index 94fff22..a3917765 100644
--- a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java
@@ -55,6 +55,7 @@
     @Mock
     private InternalResourceService mIrs;
 
+    private Agent mAgent;
     private Scribe mScribe;
 
     private static class MockScribe extends Scribe {
@@ -80,10 +81,13 @@
         doReturn(mIrs).when(mIrs).getLock();
         doReturn(mock(AlarmManager.class)).when(mContext).getSystemService(Context.ALARM_SERVICE);
         mScribe = new MockScribe(mIrs, mAnalyst);
+        mAgent = new Agent(mIrs, mScribe, mAnalyst);
     }
 
     @After
     public void tearDown() {
+        mAgent.tearDownLocked();
+
         if (mMockingSession != null) {
             mMockingSession.finishMocking();
         }
@@ -99,7 +103,6 @@
 
         final int userId = 0;
         final String pkgName = "com.test";
-        final Agent agent = new Agent(mIrs, mScribe, mAnalyst);
         final Ledger ledger = mScribe.getLedgerLocked(userId, pkgName);
 
         doReturn(consumptionLimit).when(mIrs).getConsumptionLimitLocked();
@@ -107,66 +110,64 @@
                 .getMaxSatiatedBalance(anyInt(), anyString());
 
         Ledger.Transaction transaction = new Ledger.Transaction(0, 0, 0, null, 5, 10);
-        agent.recordTransactionLocked(userId, pkgName, ledger, transaction, false);
+        mAgent.recordTransactionLocked(userId, pkgName, ledger, transaction, false);
         assertEquals(5, ledger.getCurrentBalance());
         assertEquals(remainingCakes - 10, mScribe.getRemainingConsumableCakesLocked());
 
-        agent.onPackageRemovedLocked(userId, pkgName);
+        mAgent.onPackageRemovedLocked(userId, pkgName);
         assertEquals(remainingCakes - 10, mScribe.getRemainingConsumableCakesLocked());
         assertLedgersEqual(new Ledger(), mScribe.getLedgerLocked(userId, pkgName));
     }
 
     @Test
     public void testRecordTransaction_UnderMax() {
-        Agent agent = new Agent(mIrs, mScribe, mAnalyst);
         Ledger ledger = new Ledger();
 
         doReturn(1_000_000L).when(mIrs).getConsumptionLimitLocked();
         doReturn(1_000_000L).when(mEconomicPolicy).getMaxSatiatedBalance(anyInt(), anyString());
 
         Ledger.Transaction transaction = new Ledger.Transaction(0, 0, 0, null, 5, 0);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(5, ledger.getCurrentBalance());
 
         transaction = new Ledger.Transaction(0, 0, 0, null, 995, 0);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(1000, ledger.getCurrentBalance());
 
         transaction = new Ledger.Transaction(0, 0, 0, null, -500, 250);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(500, ledger.getCurrentBalance());
 
         transaction = new Ledger.Transaction(0, 0, 0, null, 999_500L, 500);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(1_000_000L, ledger.getCurrentBalance());
 
         transaction = new Ledger.Transaction(0, 0, 0, null, -1_000_001L, 1000);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(-1, ledger.getCurrentBalance());
     }
 
     @Test
     public void testRecordTransaction_MaxConsumptionLimit() {
-        Agent agent = new Agent(mIrs, mScribe, mAnalyst);
         Ledger ledger = new Ledger();
 
         doReturn(1000L).when(mIrs).getConsumptionLimitLocked();
         doReturn(1_000_000L).when(mEconomicPolicy).getMaxSatiatedBalance(anyInt(), anyString());
 
         Ledger.Transaction transaction = new Ledger.Transaction(0, 0, 0, null, 5, 0);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(5, ledger.getCurrentBalance());
 
         transaction = new Ledger.Transaction(0, 0, 0, null, 995, 0);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(1000, ledger.getCurrentBalance());
 
         transaction = new Ledger.Transaction(0, 0, 0, null, -500, 250);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(500, ledger.getCurrentBalance());
 
         transaction = new Ledger.Transaction(0, 0, 0, null, 2000, 0);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(2500, ledger.getCurrentBalance());
 
         // ConsumptionLimit can change as the battery level changes. Ledger balances shouldn't be
@@ -174,57 +175,56 @@
         doReturn(900L).when(mIrs).getConsumptionLimitLocked();
 
         transaction = new Ledger.Transaction(0, 0, 0, null, 100, 0);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(2600, ledger.getCurrentBalance());
 
         transaction = new Ledger.Transaction(0, 0, 0, null, -50, 50);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(2550, ledger.getCurrentBalance());
 
         transaction = new Ledger.Transaction(0, 0, 0, null, -200, 100);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(2350, ledger.getCurrentBalance());
 
         doReturn(800L).when(mIrs).getConsumptionLimitLocked();
 
         transaction = new Ledger.Transaction(0, 0, 0, null, 100, 0);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(2450, ledger.getCurrentBalance());
     }
 
     @Test
     public void testRecordTransaction_MaxSatiatedBalance() {
-        Agent agent = new Agent(mIrs, mScribe, mAnalyst);
         Ledger ledger = new Ledger();
 
         doReturn(1_000_000L).when(mIrs).getConsumptionLimitLocked();
         doReturn(1000L).when(mEconomicPolicy).getMaxSatiatedBalance(anyInt(), anyString());
 
         Ledger.Transaction transaction = new Ledger.Transaction(0, 0, 0, null, 5, 0);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(5, ledger.getCurrentBalance());
 
         transaction = new Ledger.Transaction(0, 0, 0, null, 995, 0);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(1000, ledger.getCurrentBalance());
 
         transaction = new Ledger.Transaction(0, 0, 0, null, -500, 250);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(500, ledger.getCurrentBalance());
 
         transaction = new Ledger.Transaction(0, 0, 0, null, 999_500L, 1000);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(1_000, ledger.getCurrentBalance());
 
         // Shouldn't change in normal operation, but adding test case in case it does.
         doReturn(900L).when(mEconomicPolicy).getMaxSatiatedBalance(anyInt(), anyString());
 
         transaction = new Ledger.Transaction(0, 0, 0, null, 500, 0);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(1_000, ledger.getCurrentBalance());
 
         transaction = new Ledger.Transaction(0, 0, 0, null, -1001, 500);
-        agent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
+        mAgent.recordTransactionLocked(0, "com.test", ledger, transaction, false);
         assertEquals(-1, ledger.getCurrentBalance());
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java
index 6c7b995..035bef6 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/ProxyManagerTest.java
@@ -23,7 +23,14 @@
 import static com.android.server.accessibility.ProxyManager.PROXY_COMPONENT_CLASS_NAME;
 import static com.android.server.accessibility.ProxyManager.PROXY_COMPONENT_PACKAGE_NAME;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.accessibilityservice.AccessibilityGestureEvent;
 import android.accessibilityservice.AccessibilityServiceInfo;
@@ -40,6 +47,7 @@
 import android.os.IBinder;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.ArraySet;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
@@ -48,6 +56,8 @@
 import android.view.accessibility.IAccessibilityManagerClient;
 import android.view.inputmethod.EditorInfo;
 
+import androidx.test.InstrumentationRegistry;
+
 import com.android.internal.R;
 import com.android.internal.inputmethod.IAccessibilityInputMethodSession;
 import com.android.internal.inputmethod.IAccessibilityInputMethodSessionCallback;
@@ -58,20 +68,12 @@
 import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
 import com.android.server.wm.WindowManagerInternal;
 
-import static com.google.common.truth.Truth.assertThat;
-
-import androidx.test.InstrumentationRegistry;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
@@ -87,6 +89,10 @@
     private static final int DEVICE_ID = 10;
     private static final int STREAMED_CALLING_UID = 9876;
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+
     @Mock private Context mMockContext;
     @Mock private AccessibilitySecurityPolicy mMockSecurityPolicy;
     @Mock private AccessibilityWindowManager mMockA11yWindowManager;
@@ -110,6 +116,8 @@
         MockitoAnnotations.initMocks(this);
         final Resources resources = InstrumentationRegistry.getContext().getResources();
 
+        mSetFlagsRule.enableFlags(Flags.FLAG_PROXY_USE_APPS_ON_VIRTUAL_DEVICE_LISTENER);
+
         mFocusStrokeWidthDefaultValue =
                 resources.getDimensionPixelSize(R.dimen.accessibility_focus_highlight_stroke_width);
         mFocusColorDefaultValue = resources.getColor(R.color.accessibility_focus_highlight_color);
@@ -218,6 +226,39 @@
     }
 
     /**
+     * Tests that the manager's AppsOnVirtualDeviceListener implementation propagates the running
+     * app changes to the proxy device.
+     */
+    @Test
+    public void testUpdateProxyOfRunningAppsChange_changedUidIsStreamedApp_propagatesChange() {
+        final VirtualDeviceManagerInternal localVdm =
+                Mockito.mock(VirtualDeviceManagerInternal.class);
+        when(localVdm.getDeviceIdsForUid(anyInt())).thenReturn(new ArraySet(Set.of(DEVICE_ID)));
+
+        mProxyManager.setLocalVirtualDeviceManager(localVdm);
+        registerProxy(DISPLAY_ID);
+        verify(localVdm).registerAppsOnVirtualDeviceListener(any());
+
+        final ArraySet<Integer> runningUids = new ArraySet(Set.of(STREAMED_CALLING_UID));
+
+        // Flush any existing messages. The messages after this come from onProxyChanged.
+        mMessageCapturingHandler.sendAllMessages();
+
+        // The virtual device has been updated with the streamed app's UID, so the proxy is
+        // updated.
+        mProxyManager.notifyProxyOfRunningAppsChange(runningUids);
+
+        verify(localVdm).getDeviceIdsForUid(STREAMED_CALLING_UID);
+        verify(mMockProxySystemSupport).getCurrentUserClientsLocked();
+        verify(mMockProxySystemSupport).getGlobalClientsLocked();
+        // Messages to notify IAccessibilityManagerClients should be posted.
+        assertThat(mMessageCapturingHandler.hasMessages()).isTrue();
+
+        mProxyManager.unregisterProxy(DISPLAY_ID);
+        verify(localVdm).unregisterAppsOnVirtualDeviceListener(any());
+    }
+
+    /**
      * Tests that getting the first device id for an app uid, such as when an app queries for
      * device-specific state, returns the right device id.
      */
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java
index 0b730f1..fa6e7f6 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java
@@ -115,6 +115,11 @@
         // Assert that the user doesn't exist in the map initially.
         assertThat(mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1)).isNull();
 
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+                .thenReturn(true);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+        when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+
         mAuthenticationStatsCollector.authenticate(USER_ID_1, true /* authenticated */);
 
         AuthenticationStats authenticationStats =
@@ -130,6 +135,11 @@
         // Assert that the user doesn't exist in the map initially.
         assertThat(mAuthenticationStatsCollector.getAuthenticationStatsForUser(USER_ID_1)).isNull();
 
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+                .thenReturn(true);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+        when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+
         mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
 
         AuthenticationStats authenticationStats =
@@ -176,6 +186,11 @@
                         40 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
                         0 /* modality */));
 
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+                .thenReturn(true);
+        when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+        when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+
         mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
 
         // Assert that no notification should be sent.
@@ -233,13 +248,13 @@
         // Assert that no notification should be sent.
         verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
         verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
-        // Assert that data has been reset.
+        // Assert that data hasn't been reset.
         AuthenticationStats authenticationStats = mAuthenticationStatsCollector
                 .getAuthenticationStatsForUser(USER_ID_1);
-        assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0);
-        assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0);
+        assertThat(authenticationStats.getTotalAttempts()).isEqualTo(500);
+        assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400);
         assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
-        assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f);
+        assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f);
     }
 
     @Test
@@ -260,13 +275,13 @@
         // Assert that no notification should be sent.
         verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
         verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
-        // Assert that data has been reset.
+        // Assert that data hasn't been reset.
         AuthenticationStats authenticationStats = mAuthenticationStatsCollector
                 .getAuthenticationStatsForUser(USER_ID_1);
-        assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0);
-        assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0);
+        assertThat(authenticationStats.getTotalAttempts()).isEqualTo(500);
+        assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400);
         assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
-        assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f);
+        assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
index 78655a5..c40ad28 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/audio/VirtualAudioControllerTest.java
@@ -79,9 +79,9 @@
                         SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
                         /* allowedUsers= */ new ArraySet<>(),
                         /* activityLaunchAllowedByDefault= */ true,
-                        /* activityPolicyExceptions= */ new ArraySet<>(),
+                        /* activityPolicyExemptions= */ new ArraySet<>(),
                         /* crossTaskNavigationAllowedByDefault= */ true,
-                        /* crossTaskNavigationExceptions= */ new ArraySet<>(),
+                        /* crossTaskNavigationExemptions= */ new ArraySet<>(),
                         /* activityListener= */ null,
                         /* pipBlockedCallback= */ null,
                         /* activityBlockedCallback= */ null,
diff --git a/services/tests/wmtests/src/com/android/server/wm/CompatScaleProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/CompatScaleProviderTest.java
new file mode 100644
index 0000000..96e3cb1
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/CompatScaleProviderTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+
+/**
+ * Tests for the {@link CompatScaleProvider} interface.
+ * See {@link CompatModePackages} class for implementation.
+ *
+ * Build/Install/Run:
+ * atest WmTests:CompatScaleProviderTest
+ */
+@SmallTest
+@Presubmit
+public class CompatScaleProviderTest extends SystemServiceTestsBase {
+    private static final String TEST_PACKAGE = "compat.mode.packages";
+    static final int TEST_USER_ID = 1;
+
+    private ActivityTaskManagerService mAtm;
+
+    /**
+     * setup method before every test.
+     */
+    @Before
+    public void setUp() {
+        mAtm = mSystemServicesTestRule.getActivityTaskManagerService();
+    }
+
+    /**
+     * Registering a {@link CompatScaleProvider} with an invalid id should throw an exception.
+     */
+    @Test
+    public void registerCompatScaleProviderWithInvalidId() {
+        CompatScaleProvider compatScaleProvider = mock(CompatScaleProvider.class);
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->  mAtm.registerCompatScaleProvider(-1, compatScaleProvider)
+        );
+    }
+
+    /**
+     * Registering a {@code null} {@link CompatScaleProvider} should throw an exception.
+     */
+    @Test
+    public void registerCompatScaleProviderFailIfCallbackIsNull() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->  mAtm.registerCompatScaleProvider(
+                            CompatScaleProvider.COMPAT_SCALE_MODE_PRODUCT, null)
+        );
+    }
+
+    /**
+     * Registering a {@link CompatScaleProvider} with a already registered id should throw an
+     * exception.
+     */
+    @Test
+    public void registerCompatScaleProviderFailIfIdIsAlreadyRegistered() {
+        CompatScaleProvider compatScaleProvider = mock(CompatScaleProvider.class);
+        mAtm.registerCompatScaleProvider(CompatScaleProvider.COMPAT_SCALE_MODE_PRODUCT,
+                compatScaleProvider);
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->  mAtm.registerCompatScaleProvider(
+                            CompatScaleProvider.COMPAT_SCALE_MODE_PRODUCT, compatScaleProvider)
+        );
+        mAtm.unregisterCompatScaleProvider(CompatScaleProvider.COMPAT_SCALE_MODE_PRODUCT);
+    }
+
+    /**
+     * Successfully registering a {@link CompatScaleProvider} with should result in callbacks
+     * getting called.
+     */
+    @Test
+    public void registerCompatScaleProviderSuccessfully() {
+        CompatScaleProvider compatScaleProvider = mock(CompatScaleProvider.class);
+        mAtm.registerCompatScaleProvider(CompatScaleProvider.COMPAT_SCALE_MODE_PRODUCT,
+                compatScaleProvider);
+        mAtm.mCompatModePackages.getCompatScale(TEST_PACKAGE, TEST_USER_ID);
+        verify(compatScaleProvider, times(1)).getCompatScale(TEST_PACKAGE, TEST_USER_ID);
+        mAtm.unregisterCompatScaleProvider(CompatScaleProvider.COMPAT_SCALE_MODE_PRODUCT);
+    }
+
+    /**
+     * Unregistering a {@link CompatScaleProvider} with a unregistered id should throw an exception.
+     */
+    @Test
+    public void unregisterCompatScaleProviderFailIfIdNotRegistered() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->  mAtm.unregisterCompatScaleProvider(
+                            CompatScaleProvider.COMPAT_SCALE_MODE_PRODUCT)
+        );
+    }
+
+    /**
+     * Unregistering a {@link CompatScaleProvider} with an invalid id should throw an exception.
+     */
+    @Test
+    public void unregisterCompatScaleProviderFailIfIdNotInRange() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->  mAtm.unregisterCompatScaleProvider(-1)
+        );
+    }
+
+    /**
+     * Successfully unregistering a {@link CompatScaleProvider} should stop the callbacks from
+     * getting called.
+     */
+    @Test
+    public void unregisterCompatScaleProviderSuccessfully() {
+        CompatScaleProvider compatScaleProvider = mock(CompatScaleProvider.class);
+        mAtm.registerCompatScaleProvider(CompatScaleProvider.COMPAT_SCALE_MODE_PRODUCT,
+                compatScaleProvider);
+        mAtm.unregisterCompatScaleProvider(CompatScaleProvider.COMPAT_SCALE_MODE_PRODUCT);
+        mAtm.mCompatModePackages.getCompatScale(TEST_PACKAGE, TEST_USER_ID);
+        verify(compatScaleProvider, never()).getCompatScale(TEST_PACKAGE, TEST_USER_ID);
+    }
+
+    /**
+     * Order of calling {@link CompatScaleProvider} is same as the id that was used for
+     * registering it.
+     */
+    @Test
+    public void registerCompatScaleProviderRespectsOrderId() {
+        CompatScaleProvider gameModeCompatScaleProvider = mock(CompatScaleProvider.class);
+        CompatScaleProvider productCompatScaleProvider = mock(CompatScaleProvider.class);
+        mAtm.registerCompatScaleProvider(CompatScaleProvider.COMPAT_SCALE_MODE_GAME,
+                gameModeCompatScaleProvider);
+        mAtm.registerCompatScaleProvider(CompatScaleProvider.COMPAT_SCALE_MODE_PRODUCT,
+                productCompatScaleProvider);
+        mAtm.mCompatModePackages.getCompatScale(TEST_PACKAGE, TEST_USER_ID);
+        InOrder inOrder = inOrder(gameModeCompatScaleProvider, productCompatScaleProvider);
+        inOrder.verify(gameModeCompatScaleProvider).getCompatScale(TEST_PACKAGE, TEST_USER_ID);
+        inOrder.verify(productCompatScaleProvider).getCompatScale(TEST_PACKAGE, TEST_USER_ID);
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
index c84eab3..622e81e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
@@ -16,6 +16,8 @@
 
 package com.android.server.wm;
 
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
@@ -77,6 +79,7 @@
 @RunWith(WindowTestRunner.class)
 public class ContentRecorderTests extends WindowTestsBase {
     private static IBinder sTaskWindowContainerToken;
+    private DisplayContent mVirtualDisplayContent;
     private Task mTask;
     private final ContentRecordingSession mDisplaySession =
             ContentRecordingSession.createDisplaySession(DEFAULT_DISPLAY);
@@ -107,11 +110,11 @@
         displayInfo.logicalWidth = sSurfaceSize.x;
         displayInfo.logicalHeight = sSurfaceSize.y;
         displayInfo.state = STATE_ON;
-        final DisplayContent virtualDisplayContent = createNewDisplay(displayInfo);
-        final int displayId = virtualDisplayContent.getDisplayId();
-        mContentRecorder = new ContentRecorder(virtualDisplayContent,
+        mVirtualDisplayContent = createNewDisplay(displayInfo);
+        final int displayId = mVirtualDisplayContent.getDisplayId();
+        mContentRecorder = new ContentRecorder(mVirtualDisplayContent,
                 mMediaProjectionManagerWrapper);
-        spyOn(virtualDisplayContent);
+        spyOn(mVirtualDisplayContent);
 
         // GIVEN MediaProjection has already initialized the WindowToken of the DisplayArea to
         // record.
@@ -119,7 +122,7 @@
         mDisplaySession.setDisplayToRecord(mDefaultDisplay.mDisplayId);
 
         // GIVEN there is a window token associated with a task to record.
-        sTaskWindowContainerToken = setUpTaskWindowContainerToken(virtualDisplayContent);
+        sTaskWindowContainerToken = setUpTaskWindowContainerToken(mVirtualDisplayContent);
         mTaskSession = ContentRecordingSession.createTaskSession(sTaskWindowContainerToken);
         mTaskSession.setVirtualDisplayId(displayId);
 
@@ -252,7 +255,11 @@
     public void testOnConfigurationChanged_resizesSurface() {
         mContentRecorder.setContentRecordingSession(mDisplaySession);
         mContentRecorder.updateRecording();
-        mContentRecorder.onConfigurationChanged(ORIENTATION_PORTRAIT);
+        // Ensure a different orientation when we check if something has changed.
+        @Configuration.Orientation final int lastOrientation =
+                mDisplayContent.getConfiguration().orientation == ORIENTATION_PORTRAIT
+                        ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT;
+        mContentRecorder.onConfigurationChanged(lastOrientation);
 
         verify(mTransaction, atLeast(2)).setPosition(eq(mRecordedSurface), anyFloat(),
                 anyFloat());
@@ -261,12 +268,53 @@
     }
 
     @Test
+    public void testOnConfigurationChanged_resizesVirtualDisplay() {
+        final int newWidth = 55;
+        mContentRecorder.setContentRecordingSession(mDisplaySession);
+        mContentRecorder.updateRecording();
+
+        // The user rotates the device, so the host app resizes the virtual display for the capture.
+        resizeDisplay(mDisplayContent, newWidth, sSurfaceSize.y);
+        resizeDisplay(mVirtualDisplayContent, newWidth, sSurfaceSize.y);
+        mContentRecorder.onConfigurationChanged(mDisplayContent.getConfiguration().orientation);
+
+        verify(mTransaction, atLeast(2)).setPosition(eq(mRecordedSurface), anyFloat(),
+                anyFloat());
+        verify(mTransaction, atLeast(2)).setMatrix(eq(mRecordedSurface), anyFloat(), anyFloat(),
+                anyFloat(), anyFloat());
+    }
+
+    @Test
+    public void testOnConfigurationChanged_rotateVirtualDisplay() {
+        mContentRecorder.setContentRecordingSession(mDisplaySession);
+        mContentRecorder.updateRecording();
+
+        // Change a value that we shouldn't rely upon; it has the wrong type.
+        mVirtualDisplayContent.setOverrideOrientation(SCREEN_ORIENTATION_FULL_SENSOR);
+        mContentRecorder.onConfigurationChanged(
+                mVirtualDisplayContent.getConfiguration().orientation);
+
+        // No resize is issued, only the initial transformations when we started recording.
+        verify(mTransaction).setPosition(eq(mRecordedSurface), anyFloat(),
+                anyFloat());
+        verify(mTransaction).setMatrix(eq(mRecordedSurface), anyFloat(), anyFloat(),
+                anyFloat(), anyFloat());
+    }
+
+    @Test
     public void testOnTaskOrientationConfigurationChanged_resizesSurface() {
         mContentRecorder.setContentRecordingSession(mTaskSession);
         mContentRecorder.updateRecording();
 
         Configuration config = mTask.getConfiguration();
-        config.orientation = ORIENTATION_PORTRAIT;
+        // Ensure a different orientation when we compare.
+        @Configuration.Orientation final int orientation =
+                config.orientation == ORIENTATION_PORTRAIT ? ORIENTATION_LANDSCAPE
+                        : ORIENTATION_PORTRAIT;
+        final Rect lastBounds = config.windowConfiguration.getBounds();
+        config.orientation = orientation;
+        config.windowConfiguration.setBounds(
+                new Rect(0, 0, lastBounds.height(), lastBounds.width()));
         mTask.onConfigurationChanged(config);
 
         verify(mTransaction, atLeast(2)).setPosition(eq(mRecordedSurface), anyFloat(),
@@ -279,13 +327,15 @@
     public void testOnTaskBoundsConfigurationChanged_notifiesCallback() {
         mTask.getRootTask().setWindowingMode(WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW);
 
+        final int minWidth = 222;
+        final int minHeight = 777;
         final int recordedWidth = 333;
         final int recordedHeight = 999;
 
         final ActivityInfo info = new ActivityInfo();
         info.windowLayout = new ActivityInfo.WindowLayout(-1 /* width */,
                 -1 /* widthFraction */, -1 /* height */, -1 /* heightFraction */,
-                Gravity.NO_GRAVITY, recordedWidth, recordedHeight);
+                Gravity.NO_GRAVITY, minWidth, minHeight);
         mTask.setMinDimensions(info);
 
         // WHEN a recording is ongoing.
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
index 994dcf1..411712e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
@@ -23,8 +23,6 @@
 import static android.view.WindowInsets.Type.navigationBars;
 import static android.view.WindowInsets.Type.statusBars;
 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
 import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
@@ -108,7 +106,7 @@
 
     @Test
     public void testControlsForDispatch_forceStatusBarVisible() {
-        addStatusBar().mAttrs.privateFlags |= PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
+        addStatusBar().mAttrs.forciblyShownTypes |= statusBars();
         addNavigationBar();
 
         final InsetsSourceControl[] controls = addAppWindowAndGetControlsForDispatch();
@@ -120,8 +118,8 @@
 
     @Test
     public void testControlsForDispatch_statusBarForceShowNavigation() {
-        addWindow(TYPE_NOTIFICATION_SHADE, "notificationShade").mAttrs.privateFlags |=
-                PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION;
+        addWindow(TYPE_NOTIFICATION_SHADE, "notificationShade").mAttrs.forciblyShownTypes |=
+                navigationBars();
         addStatusBar();
         addNavigationBar();
 
@@ -135,7 +133,7 @@
     @Test
     public void testControlsForDispatch_statusBarForceShowNavigation_butFocusedAnyways() {
         WindowState notifShade = addWindow(TYPE_NOTIFICATION_SHADE, "notificationShade");
-        notifShade.mAttrs.privateFlags |= PRIVATE_FLAG_STATUS_FORCE_SHOW_NAVIGATION;
+        notifShade.mAttrs.forciblyShownTypes |= navigationBars();
         addNavigationBar();
 
         mDisplayContent.getInsetsPolicy().updateBarControlTarget(notifShade);
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 0cdd9b8..8f68c0f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -112,6 +112,7 @@
 import android.view.WindowInsets;
 import android.view.WindowManager;
 
+import androidx.test.filters.FlakyTest;
 import androidx.test.filters.MediumTest;
 
 import com.android.internal.policy.SystemBarUtils;
@@ -2361,6 +2362,7 @@
     }
 
     @Test
+    @FlakyTest(bugId = 299220009)
     public void testUserOverrideAspectRatioNotEnabled() {
         setUpDisplaySizeWithApp(/* dw */ 1600, /* dh */ 1400);
 
@@ -2409,8 +2411,9 @@
                 .setUid(android.os.Process.myUid())
                 .build();
         activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
-        activity.mWmService.mLetterboxConfiguration
-                .setUserAppAspectRatioSettingsOverrideEnabled(enabled);
+        spyOn(activity.mWmService.mLetterboxConfiguration);
+        doReturn(enabled).when(activity.mWmService.mLetterboxConfiguration)
+                .isUserAppAspectRatioSettingsEnabled();
         // Set user aspect ratio override
         final IPackageManager pm = mAtm.getPackageManager();
         try {
@@ -4249,6 +4252,7 @@
         // Set up a display in landscape with a fixed-orientation PORTRAIT app
         setUpDisplaySizeWithApp(2800, 1400);
         mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        mWm.mLetterboxConfiguration.setIsAutomaticReachabilityInBookModeEnabled(false);
         mWm.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(0.5f);
         prepareUnresizable(mActivity, 1.75f, SCREEN_ORIENTATION_PORTRAIT);
 
diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
index f1c5865..b028b47 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
@@ -22,6 +22,7 @@
 import android.app.usage.UsageStats;
 import android.app.usage.UsageStatsManager;
 import android.os.Build;
+import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -1567,6 +1568,13 @@
         }
     }
 
+    void deleteDataFor(String pkg) {
+        // reuse the existing prune method to delete data for the specified package.
+        // we'll use the current timestamp so that all events before now get pruned.
+        prunePackagesDataOnUpgrade(
+                new HashMap<>(Collections.singletonMap(pkg, SystemClock.elapsedRealtime())));
+    }
+
     IntervalStats readIntervalStatsForFile(int interval, long fileName) {
         synchronized (mLock) {
             final IntervalStats stats = new IntervalStats();
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 90b798c..7db32a9 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -2043,6 +2043,12 @@
         mAppStandby.clearLastUsedTimestampsForTest(packageName, userId);
     }
 
+    void deletePackageData(@NonNull String packageName, @UserIdInt int userId) {
+        synchronized (mLock) {
+            mUserState.get(userId).deleteDataFor(packageName);
+        }
+    }
+
     private final class BinderService extends IUsageStatsManager.Stub {
 
         private boolean hasPermission(String callingPackage) {
diff --git a/services/usage/java/com/android/server/usage/UsageStatsShellCommand.java b/services/usage/java/com/android/server/usage/UsageStatsShellCommand.java
index 772b22a..4cb31f9 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsShellCommand.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsShellCommand.java
@@ -38,6 +38,8 @@
         switch (cmd) {
             case "clear-last-used-timestamps":
                 return runClearLastUsedTimestamps();
+            case "delete-package-data":
+                return deletePackageData();
             default:
                 return handleDefaultCommands(cmd);
         }
@@ -51,14 +53,38 @@
         pw.println("    Print this help text.");
         pw.println();
         pw.println("clear-last-used-timestamps PACKAGE_NAME [-u | --user USER_ID]");
-        pw.println("    Clears any existing usage data for the given package.");
+        pw.println("    Clears the last used timestamps for the given package.");
+        pw.println();
+        pw.println("delete-package-data PACKAGE_NAME [-u | --user USER_ID]");
+        pw.println("    Deletes all the usage stats for the given package.");
         pw.println();
     }
 
     @SuppressLint("AndroidFrameworkRequiresPermission")
     private int runClearLastUsedTimestamps() {
         final String packageName = getNextArgRequired();
+        final int userId = getUserId();
+        if (userId == -1) {
+            return -1;
+        }
 
+        mService.clearLastUsedTimestamps(packageName, userId);
+        return 0;
+    }
+
+    @SuppressLint("AndroidFrameworkRequiresPermission")
+    private int deletePackageData() {
+        final String packageName = getNextArgRequired();
+        final int userId = getUserId();
+        if (userId == -1) {
+            return -1;
+        }
+
+        mService.deletePackageData(packageName, userId);
+        return 0;
+    }
+
+    private int getUserId() {
         int userId = UserHandle.USER_CURRENT;
         String opt;
         while ((opt = getNextOption()) != null) {
@@ -72,8 +98,6 @@
         if (userId == UserHandle.USER_CURRENT) {
             userId = ActivityManager.getCurrentUser();
         }
-
-        mService.clearLastUsedTimestamps(packageName, userId);
-        return 0;
+        return userId;
     }
 }
diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
index fd56b6e..7d2e1a4 100644
--- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
@@ -974,6 +974,10 @@
         mDatabase.dumpMappings(ipw);
     }
 
+    void deleteDataFor(String pkg) {
+        mDatabase.deleteDataFor(pkg);
+    }
+
     void dumpFile(IndentingPrintWriter ipw, String[] args) {
         if (args == null || args.length == 0) {
             // dump all files for every interval for specified user
diff --git a/tests/TrustTests/Android.bp b/tests/TrustTests/Android.bp
index a1b888a..c216bce 100644
--- a/tests/TrustTests/Android.bp
+++ b/tests/TrustTests/Android.bp
@@ -25,6 +25,7 @@
         "androidx.test.rules",
         "androidx.test.ext.junit",
         "androidx.test.uiautomator_uiautomator",
+        "flag-junit",
         "mockito-target-minus-junit4",
         "servicestests-utils",
         "truth-prebuilt",
diff --git a/tests/TrustTests/src/android/trust/test/GrantAndRevokeTrustTest.kt b/tests/TrustTests/src/android/trust/test/GrantAndRevokeTrustTest.kt
index f864fed..1dfd5c0 100644
--- a/tests/TrustTests/src/android/trust/test/GrantAndRevokeTrustTest.kt
+++ b/tests/TrustTests/src/android/trust/test/GrantAndRevokeTrustTest.kt
@@ -16,6 +16,10 @@
 
 package android.trust.test
 
+import android.content.pm.PackageManager
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
 import android.service.trust.GrantTrustResult
 import android.trust.BaseTrustAgentService
 import android.trust.TrustTestActivity
@@ -27,6 +31,7 @@
 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import androidx.test.uiautomator.UiDevice
 import com.android.server.testutils.mock
+import org.junit.Assume.assumeFalse
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -45,6 +50,7 @@
     private val activityScenarioRule = ActivityScenarioRule(TrustTestActivity::class.java)
     private val lockStateTrackingRule = LockStateTrackingRule()
     private val trustAgentRule = TrustAgentRule<GrantAndRevokeTrustAgent>()
+    private val packageManager = getInstrumentation().getTargetContext().getPackageManager()
 
     @get:Rule
     val rule: RuleChain = RuleChain
@@ -52,6 +58,7 @@
         .around(ScreenLockRule())
         .around(lockStateTrackingRule)
         .around(trustAgentRule)
+        .around(DeviceFlagsValueProvider.createCheckFlagsRule())
 
     @Before
     fun manageTrust() {
@@ -72,7 +79,7 @@
         trustAgentRule.agent.grantTrust(GRANT_MESSAGE, 10000, 0) {}
         uiDevice.sleep()
 
-        lockStateTrackingRule.assertUnlocked()
+        lockStateTrackingRule.assertUnlockedAndTrusted()
     }
 
     @Test
@@ -86,6 +93,51 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS)
+    fun grantCannotActivelyUnlockDevice() {
+        // On automotive, trust agents can actively unlock the device.
+        assumeFalse(packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE))
+
+        // Lock the device.
+        uiDevice.sleep()
+        lockStateTrackingRule.assertLocked()
+
+        // Grant trust.
+        trustAgentRule.agent.grantTrust(GRANT_MESSAGE, 10000, 0) {}
+
+        // The grant should not have unlocked the device.  Wait a bit so that
+        // TrustManagerService probably will have finished processing the grant.
+        await()
+        lockStateTrackingRule.assertLocked()
+
+        // Turn the screen on and off to cause TrustManagerService to refresh
+        // its deviceLocked state.  Then verify the state is still locked.  This
+        // part failed before the fix for b/296464083.
+        uiDevice.wakeUp()
+        uiDevice.sleep()
+        await()
+        lockStateTrackingRule.assertLocked()
+    }
+
+    @Test
+    @RequiresFlagsDisabled(android.security.Flags.FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS)
+    fun grantCouldCauseWrongDeviceLockedStateDueToBug() {
+        // On automotive, trust agents can actively unlock the device.
+        assumeFalse(packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE))
+
+        // Verify that b/296464083 exists.  That is, when the device is locked
+        // and a trust agent grants trust, the deviceLocked state incorrectly
+        // becomes false even though the device correctly remains locked.
+        uiDevice.sleep()
+        lockStateTrackingRule.assertLocked()
+        trustAgentRule.agent.grantTrust(GRANT_MESSAGE, 10000, 0) {}
+        uiDevice.wakeUp()
+        uiDevice.sleep()
+        await()
+        lockStateTrackingRule.assertUnlockedButNotReally()
+    }
+
+    @Test
     fun grantDoesNotCallBack() {
         val callback = mock<(GrantTrustResult) -> Unit>()
         trustAgentRule.agent.grantTrust(GRANT_MESSAGE, 0, 0, callback)
diff --git a/tests/TrustTests/src/android/trust/test/TemporaryAndRenewableTrustTest.kt b/tests/TrustTests/src/android/trust/test/TemporaryAndRenewableTrustTest.kt
index ae72247..96362b8 100644
--- a/tests/TrustTests/src/android/trust/test/TemporaryAndRenewableTrustTest.kt
+++ b/tests/TrustTests/src/android/trust/test/TemporaryAndRenewableTrustTest.kt
@@ -102,7 +102,7 @@
         trustAgentRule.agent.grantTrust(
             GRANT_MESSAGE, 0, FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE) {}
 
-        lockStateTrackingRule.assertUnlocked()
+        lockStateTrackingRule.assertUnlockedAndTrusted()
     }
 
     @Test
@@ -125,7 +125,7 @@
             Log.i(TAG, "Callback received; status=${it.status}")
             result = it
         }
-        lockStateTrackingRule.assertUnlocked()
+        lockStateTrackingRule.assertUnlockedAndTrusted()
 
         wait("callback triggered") { result?.status == STATUS_UNLOCKED_BY_GRANT }
     }
diff --git a/tests/TrustTests/src/android/trust/test/lib/LockStateTrackingRule.kt b/tests/TrustTests/src/android/trust/test/lib/LockStateTrackingRule.kt
index c1a7bd9..5a8f828 100644
--- a/tests/TrustTests/src/android/trust/test/lib/LockStateTrackingRule.kt
+++ b/tests/TrustTests/src/android/trust/test/lib/LockStateTrackingRule.kt
@@ -16,6 +16,7 @@
 
 package android.trust.test.lib
 
+import android.app.KeyguardManager
 import android.app.trust.TrustManager
 import android.content.Context
 import android.util.Log
@@ -26,18 +27,23 @@
 import org.junit.runners.model.Statement
 
 /**
- * Rule for tracking the lock state of the device based on events emitted to [TrustListener].
+ * Rule for tracking the trusted state of the device based on events emitted to
+ * [TrustListener].  Provides helper methods for verifying that the trusted
+ * state has a particular value and is consistent with (a) the keyguard "locked"
+ * (i.e. showing) value when applicable, and (b) the device locked value that is
+ * tracked by TrustManagerService and is queryable via KeyguardManager.
  */
 class LockStateTrackingRule : TestRule {
     private val context: Context = getApplicationContext()
     private val windowManager = checkNotNull(WindowManagerGlobal.getWindowManagerService())
+    private val keyguardManager = context.getSystemService(KeyguardManager::class.java) as KeyguardManager
 
-    @Volatile lateinit var lockState: LockState
+    @Volatile lateinit var trustState: TrustState
         private set
 
     override fun apply(base: Statement, description: Description) = object : Statement() {
         override fun evaluate() {
-            lockState = LockState(locked = windowManager.isKeyguardLocked)
+            trustState = TrustState()
             val trustManager = context.getSystemService(TrustManager::class.java) as TrustManager
             val listener = Listener()
 
@@ -51,12 +57,25 @@
     }
 
     fun assertLocked() {
-        wait("un-locked per TrustListener") { lockState.locked == true }
-        wait("keyguard lock") { windowManager.isKeyguardLocked }
+        wait("device locked") { keyguardManager.isDeviceLocked }
+        // isDeviceLocked implies isKeyguardLocked && !trusted.
+        wait("keyguard locked") { windowManager.isKeyguardLocked }
+        wait("not trusted") { trustState.trusted == false }
     }
 
-    fun assertUnlocked() {
-        wait("locked per TrustListener") { lockState.locked == false }
+    // TODO(b/299298338) remove this when removing FLAG_FIX_UNLOCKED_DEVICE_REQUIRED_KEYS
+    fun assertUnlockedButNotReally() {
+        wait("device unlocked") { !keyguardManager.isDeviceLocked }
+        wait("not trusted") { trustState.trusted == false }
+        wait("keyguard locked") { windowManager.isKeyguardLocked }
+    }
+
+    fun assertUnlockedAndTrusted() {
+        wait("device unlocked") { !keyguardManager.isDeviceLocked }
+        wait("trusted") { trustState.trusted == true }
+        // Can't check for !isKeyguardLocked here, since isKeyguardLocked
+        // returns true in the case where the keyguard is dismissible with
+        // swipe, which is considered "device unlocked"!
     }
 
     inner class Listener : TestTrustListener() {
@@ -68,12 +87,12 @@
             trustGrantedMessages: MutableList<String>
         ) {
             Log.d(TAG, "Device became trusted=$enabled")
-            lockState = lockState.copy(locked = !enabled)
+            trustState = trustState.copy(trusted=enabled)
         }
     }
 
-    data class LockState(
-        val locked: Boolean? = null
+    data class TrustState(
+        val trusted: Boolean? = null
     )
 
     companion object {
diff --git a/tools/aapt/ZipEntry.cpp b/tools/aapt/ZipEntry.cpp
index 5339285..6886993 100644
--- a/tools/aapt/ZipEntry.cpp
+++ b/tools/aapt/ZipEntry.cpp
@@ -18,6 +18,8 @@
 // Access to entries in a Zip archive.
 //
 
+#define _POSIX_THREAD_SAFE_FUNCTIONS // For mingw localtime_r().
+
 #define LOG_TAG "zip"
 
 #include "ZipEntry.h"
@@ -337,39 +339,26 @@
 /*
  * Set the CDE/LFH timestamp from UNIX time.
  */
-void ZipEntry::setModWhen(time_t when)
-{
-#if !defined(_WIN32)
-    struct tm tmResult;
-#endif
-    time_t even;
-    unsigned short zdate, ztime;
-
-    struct tm* ptm;
-
+void ZipEntry::setModWhen(time_t when) {
     /* round up to an even number of seconds */
-    even = (time_t)(((unsigned long)(when) + 1) & (~1));
+    time_t even = (time_t)(((unsigned long)(when) + 1) & (~1));
 
     /* expand */
-#if !defined(_WIN32)
-    ptm = localtime_r(&even, &tmResult);
-#else
-    ptm = localtime(&even);
-#endif
+    struct tm tmResult;
+    struct tm* ptm = localtime_r(&even, &tmResult);
 
     int year;
     year = ptm->tm_year;
     if (year < 80)
         year = 80;
 
-    zdate = (year - 80) << 9 | (ptm->tm_mon+1) << 5 | ptm->tm_mday;
-    ztime = ptm->tm_hour << 11 | ptm->tm_min << 5 | ptm->tm_sec >> 1;
+    unsigned short zdate = (year - 80) << 9 | (ptm->tm_mon + 1) << 5 | ptm->tm_mday;
+    unsigned short ztime = ptm->tm_hour << 11 | ptm->tm_min << 5 | ptm->tm_sec >> 1;
 
     mCDE.mLastModFileTime = mLFH.mLastModFileTime = ztime;
     mCDE.mLastModFileDate = mLFH.mLastModFileDate = zdate;
 }
 
-
 /*
  * ===========================================================================
  *      ZipEntry::LocalFileHeader
diff --git a/tools/aapt2/compile/InlineXmlFormatParser.h b/tools/aapt2/compile/InlineXmlFormatParser.h
index 4300023..3a5161b 100644
--- a/tools/aapt2/compile/InlineXmlFormatParser.h
+++ b/tools/aapt2/compile/InlineXmlFormatParser.h
@@ -21,8 +21,8 @@
 #include <vector>
 
 #include "android-base/macros.h"
-
 #include "process/IResourceTableConsumer.h"
+#include "xml/XmlDom.h"
 
 namespace aapt {
 
diff --git a/tools/aapt2/format/Archive_test.cpp b/tools/aapt2/format/Archive_test.cpp
index 3c44da7..fd50af9 100644
--- a/tools/aapt2/format/Archive_test.cpp
+++ b/tools/aapt2/format/Archive_test.cpp
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#include <stdlib.h>
+
 #include "test/Test.h"
 
 namespace aapt {
@@ -34,6 +36,29 @@
   std::string error_;
 };
 
+class TzSetter {
+ public:
+  explicit TzSetter(const std::string& new_tz) {
+    old_tz_ = getenv("TZ");
+    new_tz_ = "TZ=" + new_tz;
+    putenv(const_cast<char*>(new_tz_.c_str()));
+    tzset();
+  }
+
+  ~TzSetter() {
+    if (old_tz_) {
+      putenv(old_tz_);
+    } else {
+      putenv(const_cast<char*>("TZ"));
+    }
+    tzset();
+  }
+
+ private:
+  char* old_tz_;
+  std::string new_tz_;
+};
+
 std::unique_ptr<uint8_t[]> MakeTestArray() {
   auto array = std::make_unique<uint8_t[]>(kTestDataLength);
   for (int index = 0; index < kTestDataLength; ++index) {
@@ -86,6 +111,22 @@
   }
 }
 
+void VerifyZipFileTimestamps(const std::string& output_path) {
+  std::unique_ptr<io::ZipFileCollection> zip = io::ZipFileCollection::Create(output_path, nullptr);
+  auto it = zip->Iterator();
+  while (it->HasNext()) {
+    auto file = it->Next();
+    struct tm modification_time;
+    ASSERT_TRUE(file->GetModificationTime(&modification_time));
+    EXPECT_EQ(modification_time.tm_year, 80);
+    EXPECT_EQ(modification_time.tm_mon, 0);
+    EXPECT_EQ(modification_time.tm_mday, 1);
+    EXPECT_EQ(modification_time.tm_hour, 0);
+    EXPECT_EQ(modification_time.tm_min, 0);
+    EXPECT_EQ(modification_time.tm_sec, 0);
+  }
+}
+
 TEST_F(ArchiveTest, DirectoryWriteEntrySuccess) {
   std::string output_path = GetTestPath("output");
   std::unique_ptr<IArchiveWriter> writer = MakeDirectoryWriter(output_path);
@@ -206,4 +247,73 @@
   ASSERT_EQ("ZipFileWriteFileError", writer->GetError());
 }
 
+TEST_F(ArchiveTest, ZipFileTimeZoneUTC) {
+  TzSetter tz("UTC0");
+  std::string output_path = GetTestPath("output.apk");
+  std::unique_ptr<IArchiveWriter> writer = MakeZipFileWriter(output_path);
+  std::unique_ptr<uint8_t[]> data1 = MakeTestArray();
+  std::unique_ptr<uint8_t[]> data2 = MakeTestArray();
+
+  ASSERT_TRUE(writer->StartEntry("test1", 0));
+  ASSERT_TRUE(writer->Write(static_cast<const void*>(data1.get()), kTestDataLength));
+  ASSERT_TRUE(writer->FinishEntry());
+  ASSERT_FALSE(writer->HadError());
+
+  ASSERT_TRUE(writer->StartEntry("test2", 0));
+  ASSERT_TRUE(writer->Write(static_cast<const void*>(data2.get()), kTestDataLength));
+  ASSERT_TRUE(writer->FinishEntry());
+  ASSERT_FALSE(writer->HadError());
+
+  writer.reset();
+
+  // All zip file entries must have the same timestamp, regardless of time zone. See: b/277978832
+  VerifyZipFileTimestamps(output_path);
+}
+
+TEST_F(ArchiveTest, ZipFileTimeZoneWestOfUTC) {
+  TzSetter tz("PST8");
+  std::string output_path = GetTestPath("output.apk");
+  std::unique_ptr<IArchiveWriter> writer = MakeZipFileWriter(output_path);
+  std::unique_ptr<uint8_t[]> data1 = MakeTestArray();
+  std::unique_ptr<uint8_t[]> data2 = MakeTestArray();
+
+  ASSERT_TRUE(writer->StartEntry("test1", 0));
+  ASSERT_TRUE(writer->Write(static_cast<const void*>(data1.get()), kTestDataLength));
+  ASSERT_TRUE(writer->FinishEntry());
+  ASSERT_FALSE(writer->HadError());
+
+  ASSERT_TRUE(writer->StartEntry("test2", 0));
+  ASSERT_TRUE(writer->Write(static_cast<const void*>(data2.get()), kTestDataLength));
+  ASSERT_TRUE(writer->FinishEntry());
+  ASSERT_FALSE(writer->HadError());
+
+  writer.reset();
+
+  // All zip file entries must have the same timestamp, regardless of time zone. See: b/277978832
+  VerifyZipFileTimestamps(output_path);
+}
+
+TEST_F(ArchiveTest, ZipFileTimeZoneEastOfUTC) {
+  TzSetter tz("EET-2");
+  std::string output_path = GetTestPath("output.apk");
+  std::unique_ptr<IArchiveWriter> writer = MakeZipFileWriter(output_path);
+  std::unique_ptr<uint8_t[]> data1 = MakeTestArray();
+  std::unique_ptr<uint8_t[]> data2 = MakeTestArray();
+
+  ASSERT_TRUE(writer->StartEntry("test1", 0));
+  ASSERT_TRUE(writer->Write(static_cast<const void*>(data1.get()), kTestDataLength));
+  ASSERT_TRUE(writer->FinishEntry());
+  ASSERT_FALSE(writer->HadError());
+
+  ASSERT_TRUE(writer->StartEntry("test2", 0));
+  ASSERT_TRUE(writer->Write(static_cast<const void*>(data2.get()), kTestDataLength));
+  ASSERT_TRUE(writer->FinishEntry());
+  ASSERT_FALSE(writer->HadError());
+
+  writer.reset();
+
+  // All zip file entries must have the same timestamp, regardless of time zone. See: b/277978832
+  VerifyZipFileTimestamps(output_path);
+}
+
 }  // namespace aapt
diff --git a/tools/aapt2/io/File.h b/tools/aapt2/io/File.h
index 08d497d..673d1b7 100644
--- a/tools/aapt2/io/File.h
+++ b/tools/aapt2/io/File.h
@@ -57,6 +57,11 @@
     return false;
   }
 
+  // Fills in buf with the last modification time of the file. Returns true if successful,
+  // otherwise false (i.e., the operation is not supported or the file system is unable to provide
+  // a last modification time).
+  virtual bool GetModificationTime(struct tm* buf) const = 0;
+
  private:
   // Any segments created from this IFile need to be owned by this IFile, so
   // keep them
@@ -79,6 +84,10 @@
     return file_->GetSource();
   }
 
+  bool GetModificationTime(struct tm* buf) const override {
+    return file_->GetModificationTime(buf);
+  };
+
  private:
   DISALLOW_COPY_AND_ASSIGN(FileSegment);
 
diff --git a/tools/aapt2/io/FileSystem.cpp b/tools/aapt2/io/FileSystem.cpp
index a64982a..6a692e4 100644
--- a/tools/aapt2/io/FileSystem.cpp
+++ b/tools/aapt2/io/FileSystem.cpp
@@ -14,9 +14,12 @@
  * limitations under the License.
  */
 
+#define _POSIX_THREAD_SAFE_FUNCTIONS  // For mingw localtime_r().
+
 #include "io/FileSystem.h"
 
 #include <dirent.h>
+#include <sys/stat.h>
 
 #include "android-base/errors.h"
 #include "androidfw/Source.h"
@@ -54,6 +57,23 @@
   return source_;
 }
 
+bool RegularFile::GetModificationTime(struct tm* buf) const {
+  if (buf == nullptr) {
+    return false;
+  }
+  struct stat stat_buf;
+  if (stat(source_.path.c_str(), &stat_buf) != 0) {
+    return false;
+  }
+
+  struct tm* ptm;
+  struct tm tm_result;
+  ptm = localtime_r(&stat_buf.st_mtime, &tm_result);
+
+  *buf = *ptm;
+  return true;
+}
+
 FileCollectionIterator::FileCollectionIterator(FileCollection* collection)
     : current_(collection->files_.begin()), end_(collection->files_.end()) {}
 
diff --git a/tools/aapt2/io/FileSystem.h b/tools/aapt2/io/FileSystem.h
index 0e798fc..f975196 100644
--- a/tools/aapt2/io/FileSystem.h
+++ b/tools/aapt2/io/FileSystem.h
@@ -32,6 +32,7 @@
   std::unique_ptr<IData> OpenAsData() override;
   std::unique_ptr<io::InputStream> OpenInputStream() override;
   const android::Source& GetSource() const override;
+  bool GetModificationTime(struct tm* buf) const override;
 
  private:
   DISALLOW_COPY_AND_ASSIGN(RegularFile);
diff --git a/tools/aapt2/io/ZipArchive.cpp b/tools/aapt2/io/ZipArchive.cpp
index 4a5385d..cb5bbe9 100644
--- a/tools/aapt2/io/ZipArchive.cpp
+++ b/tools/aapt2/io/ZipArchive.cpp
@@ -75,6 +75,14 @@
   return zip_entry_.method != kCompressStored;
 }
 
+bool ZipFile::GetModificationTime(struct tm* buf) const {
+  if (buf == nullptr) {
+    return false;
+  }
+  *buf = zip_entry_.GetModificationTime();
+  return true;
+}
+
 ZipFileCollectionIterator::ZipFileCollectionIterator(
     ZipFileCollection* collection)
     : current_(collection->files_.begin()), end_(collection->files_.end()) {}
diff --git a/tools/aapt2/io/ZipArchive.h b/tools/aapt2/io/ZipArchive.h
index c263aa4..ac125d0 100644
--- a/tools/aapt2/io/ZipArchive.h
+++ b/tools/aapt2/io/ZipArchive.h
@@ -38,6 +38,7 @@
   std::unique_ptr<io::InputStream> OpenInputStream() override;
   const android::Source& GetSource() const override;
   bool WasCompressed() override;
+  bool GetModificationTime(struct tm* buf) const override;
 
  private:
   ::ZipArchiveHandle zip_handle_;
diff --git a/tools/aapt2/test/Common.h b/tools/aapt2/test/Common.h
index 83a0f3f..e48668c 100644
--- a/tools/aapt2/test/Common.h
+++ b/tools/aapt2/test/Common.h
@@ -98,6 +98,10 @@
     return source_;
   }
 
+  bool GetModificationTime(struct tm* buf) const override {
+    return false;
+  };
+
  private:
   DISALLOW_COPY_AND_ASSIGN(TestFile);
 
diff --git a/tools/lint/common/Android.bp b/tools/lint/common/Android.bp
index 898f88b..8bfbfe5 100644
--- a/tools/lint/common/Android.bp
+++ b/tools/lint/common/Android.bp
@@ -27,3 +27,30 @@
     libs: ["lint_api"],
     kotlincflags: ["-Xjvm-default=all"],
 }
+
+java_defaults {
+    name: "AndroidLintCheckerTestDefaults",
+    srcs: ["checks/src/test/java/**/*.kt"],
+    static_libs: [
+        "junit",
+        "lint",
+        "lint_tests",
+    ],
+    test_options: {
+        unit_test: true,
+        tradefed_options: [
+            {
+                // lint bundles in some classes that were built with older versions
+                // of libraries, and no longer load. Since tradefed tries to load
+                // all classes in the jar to look for tests, it crashes loading them.
+                // Exclude these classes from tradefed's search.
+                name: "exclude-paths",
+                value: "org/apache",
+            },
+            {
+                name: "exclude-paths",
+                value: "META-INF",
+            },
+        ],
+    },
+}
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt b/tools/lint/common/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt
similarity index 100%
rename from tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt
rename to tools/lint/common/src/main/java/com/google/android/lint/aidl/AidlImplementationDetector.kt
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt b/tools/lint/common/src/main/java/com/google/android/lint/aidl/Constants.kt
similarity index 99%
rename from tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt
rename to tools/lint/common/src/main/java/com/google/android/lint/aidl/Constants.kt
index f1727b7..a18ed15 100644
--- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt
+++ b/tools/lint/common/src/main/java/com/google/android/lint/aidl/Constants.kt
@@ -29,9 +29,10 @@
 const val BINDER_CLASS = "android.os.Binder"
 const val IINTERFACE_INTERFACE = "android.os.IInterface"
 
-const val AIDL_PERMISSION_HELPER_SUFFIX = "_enforcePermission"
 const val PERMISSION_PREFIX_LITERAL = "android.permission."
 
+const val AIDL_PERMISSION_HELPER_SUFFIX = "_enforcePermission"
+
 /**
  * If a non java (e.g. c++) backend is enabled, the @EnforcePermission
  * annotation cannot be used.  At time of writing, the mechanism
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt b/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
similarity index 100%
rename from tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
rename to tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
diff --git a/tools/lint/fix/soong_lint_fix.py b/tools/lint/fix/soong_lint_fix.py
index cd4d778d..acc0ad0 100644
--- a/tools/lint/fix/soong_lint_fix.py
+++ b/tools/lint/fix/soong_lint_fix.py
@@ -29,6 +29,39 @@
 PATH_SUFFIX = "android_common/lint"
 FIX_ZIP = "suggested-fixes.zip"
 
+
+class SoongModule:
+    """A Soong module to lint.
+
+    The constructor takes the name of the module (for example,
+    "framework-minus-apex"). find() must be called to extract the intermediate
+    module path from Soong's module-info.json
+    """
+    def __init__(self, name):
+        self._name = name
+
+    def find(self, module_info):
+        """Finds the module in the loaded module_info.json."""
+        if self._name not in module_info:
+            raise Exception(f"Module {self._name} not found!")
+
+        partial_path = module_info[self._name]["path"][0]
+        print(f"Found module {partial_path}/{self._name}.")
+        self._path = f"{PATH_PREFIX}/{partial_path}/{self._name}/{PATH_SUFFIX}"
+
+    @property
+    def name(self):
+        return self._name
+
+    @property
+    def lint_report(self):
+        return f"{self._path}/lint-report.txt"
+
+    @property
+    def suggested_fixes(self):
+        return f"{self._path}/{FIX_ZIP}"
+
+
 class SoongLintFix:
     """
     This class creates a command line tool that will
@@ -53,16 +86,14 @@
         self._parser = _setup_parser()
         self._args = None
         self._kwargs = None
-        self._path = None
-        self._target = None
+        self._modules = []
 
-
-    def run(self, additional_setup=None, custom_fix=None):
+    def run(self):
         """
         Run the script
         """
         self._setup()
-        self._find_module()
+        self._find_modules()
         self._lint()
 
         if not self._args.no_fix:
@@ -87,8 +118,6 @@
 
         os.chdir(ANDROID_BUILD_TOP)
 
-
-    def _find_module(self):
         print("Refreshing soong modules...")
         try:
             os.mkdir(ANDROID_PRODUCT_OUT)
@@ -97,48 +126,47 @@
         subprocess.call(f"{SOONG_UI} --make-mode {PRODUCT_OUT}/module-info.json", **self._kwargs)
         print("done.")
 
+
+    def _find_modules(self):
         with open(f"{ANDROID_PRODUCT_OUT}/module-info.json") as f:
             module_info = json.load(f)
 
-        if self._args.module not in module_info:
-            sys.exit(f"Module {self._args.module} not found!")
-
-        module_path = module_info[self._args.module]["path"][0]
-        print(f"Found module {module_path}/{self._args.module}.")
-
-        self._path = f"{PATH_PREFIX}/{module_path}/{self._args.module}/{PATH_SUFFIX}"
-        self._target = f"{self._path}/lint-report.txt"
-
+        for module_name in self._args.modules:
+            module = SoongModule(module_name)
+            module.find(module_info)
+            self._modules.append(module)
 
     def _lint(self):
         print("Cleaning up any old lint results...")
-        try:
-            os.remove(f"{self._target}")
-            os.remove(f"{self._path}/{FIX_ZIP}")
-        except FileNotFoundError:
-            pass
+        for module in self._modules:
+            try:
+                os.remove(f"{module.lint_report}")
+                os.remove(f"{module.suggested_fixes}")
+            except FileNotFoundError:
+                pass
         print("done.")
 
-        print(f"Generating {self._target}")
-        subprocess.call(f"{SOONG_UI} --make-mode {self._target}", **self._kwargs)
+        target = " ".join([ module.lint_report for module in self._modules ])
+        print(f"Generating {target}")
+        subprocess.call(f"{SOONG_UI} --make-mode {target}", **self._kwargs)
         print("done.")
 
-
     def _fix(self):
-        print("Copying suggested fixes to the tree...")
-        with zipfile.ZipFile(f"{self._path}/{FIX_ZIP}") as zip:
-            for name in zip.namelist():
-                if name.startswith("out") or not name.endswith(".java"):
-                    continue
-                with zip.open(name) as src, open(f"{ANDROID_BUILD_TOP}/{name}", "wb") as dst:
-                    shutil.copyfileobj(src, dst)
+        for module in self._modules:
+            print(f"Copying suggested fixes for {module.name} to the tree...")
+            with zipfile.ZipFile(f"{module.suggested_fixes}") as zip:
+                for name in zip.namelist():
+                    if name.startswith("out") or not name.endswith(".java"):
+                        continue
+                    with zip.open(name) as src, open(f"{ANDROID_BUILD_TOP}/{name}", "wb") as dst:
+                        shutil.copyfileobj(src, dst)
             print("done.")
 
-
     def _print(self):
-        print("### lint-report.txt ###", end="\n\n")
-        with open(self._target, "r") as f:
-            print(f.read())
+        for module in self._modules:
+            print(f"### lint-report.txt {module.name} ###", end="\n\n")
+            with open(module.lint_report, "r") as f:
+                print(f.read())
 
 
 def _setup_parser():
@@ -151,7 +179,8 @@
         **Gotcha**: You must have run `source build/envsetup.sh` and `lunch` first.
         """, formatter_class=argparse.RawTextHelpFormatter)
 
-    parser.add_argument('module',
+    parser.add_argument('modules',
+                        nargs='+',
                         help='The soong build module to run '
                              '(e.g. framework-minus-apex or services.core.unboosted)')
 
@@ -170,4 +199,4 @@
     return parser
 
 if __name__ == "__main__":
-    SoongLintFix().run()
\ No newline at end of file
+    SoongLintFix().run()
diff --git a/tools/lint/framework/Android.bp b/tools/lint/framework/Android.bp
index 30a6daa..5acdf43 100644
--- a/tools/lint/framework/Android.bp
+++ b/tools/lint/framework/Android.bp
@@ -37,28 +37,9 @@
 
 java_test_host {
     name: "AndroidFrameworkLintCheckerTest",
+    defaults: ["AndroidLintCheckerTestDefaults"],
     srcs: ["checks/src/test/java/**/*.kt"],
     static_libs: [
         "AndroidFrameworkLintChecker",
-        "junit",
-        "lint",
-        "lint_tests",
     ],
-    test_options: {
-        unit_test: true,
-        tradefed_options: [
-            {
-                // lint bundles in some classes that were built with older versions
-                // of libraries, and no longer load. Since tradefed tries to load
-                // all classes in the jar to look for tests, it crashes loading them.
-                // Exclude these classes from tradefed's search.
-                name: "exclude-paths",
-                value: "org/apache",
-            },
-            {
-                name: "exclude-paths",
-                value: "META-INF",
-            },
-        ],
-    },
 }
diff --git a/tools/lint/global/Android.bp b/tools/lint/global/Android.bp
index bedb7bd..3e74171 100644
--- a/tools/lint/global/Android.bp
+++ b/tools/lint/global/Android.bp
@@ -38,28 +38,9 @@
 
 java_test_host {
     name: "AndroidGlobalLintCheckerTest",
+    defaults: ["AndroidLintCheckerTestDefaults"],
     srcs: ["checks/src/test/java/**/*.kt"],
     static_libs: [
         "AndroidGlobalLintChecker",
-        "junit",
-        "lint",
-        "lint_tests",
     ],
-    test_options: {
-        unit_test: true,
-        tradefed_options: [
-            {
-                // lint bundles in some classes that were built with older versions
-                // of libraries, and no longer load. Since tradefed tries to load
-                // all classes in the jar to look for tests, it crashes loading them.
-                // Exclude these classes from tradefed's search.
-                name: "exclude-paths",
-                value: "org/apache",
-            },
-            {
-                name: "exclude-paths",
-                value: "META-INF",
-            },
-        ],
-    },
 }
diff --git a/tools/lint/utils/Android.bp b/tools/lint/utils/Android.bp
new file mode 100644
index 0000000..75e8d68
--- /dev/null
+++ b/tools/lint/utils/Android.bp
@@ -0,0 +1,45 @@
+// Copyright (C) 2023 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library_host {
+    name: "AndroidUtilsLintChecker",
+    srcs: ["checks/src/main/java/**/*.kt"],
+    plugins: ["auto_service_plugin"],
+    libs: [
+        "auto_service_annotations",
+        "lint_api",
+    ],
+    static_libs: [
+        "AndroidCommonLint",
+    ],
+    kotlincflags: ["-Xjvm-default=all"],
+}
+
+java_test_host {
+    name: "AndroidUtilsLintCheckerTest",
+    defaults: ["AndroidLintCheckerTestDefaults"],
+    srcs: ["checks/src/test/java/**/*.kt"],
+    static_libs: [
+        "AndroidUtilsLintChecker",
+    ],
+}
diff --git a/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt b/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt
new file mode 100644
index 0000000..fa61c42
--- /dev/null
+++ b/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 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.google.android.lint
+
+import com.android.tools.lint.client.api.IssueRegistry
+import com.android.tools.lint.client.api.Vendor
+import com.android.tools.lint.detector.api.CURRENT_API
+import com.google.android.lint.aidl.AnnotatedAidlCounter
+import com.google.auto.service.AutoService
+
+@AutoService(IssueRegistry::class)
+@Suppress("UnstableApiUsage")
+class AndroidUtilsIssueRegistry : IssueRegistry() {
+    override val issues = listOf(
+        AnnotatedAidlCounter.ISSUE_ANNOTATED_AIDL_COUNTER,
+    )
+
+    override val api: Int
+        get() = CURRENT_API
+
+    override val minApi: Int
+        get() = 8
+
+    override val vendor: Vendor = Vendor(
+        vendorName = "Android",
+        feedbackUrl = "http://b/issues/new?component=315013",
+        contact = "tweek@google.com"
+    )
+}
diff --git a/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/AnnotatedAidlCounter.kt b/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/AnnotatedAidlCounter.kt
new file mode 100644
index 0000000..f0ec3f4
--- /dev/null
+++ b/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/AnnotatedAidlCounter.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 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.google.android.lint.aidl
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Context
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Location
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UMethod
+
+import java.util.TreeMap
+
+/**
+ *  Count the number of AIDL interfaces. Reports the number of annotated and
+ *  non-annotated methods.
+ */
+@Suppress("UnstableApiUsage")
+class AnnotatedAidlCounter : AidlImplementationDetector() {
+
+    private data class Stat(
+        var unannotated: Int = 0,
+        var enforced: Int = 0,
+        var notRequired: Int = 0,
+    )
+
+    private var packagesStats: TreeMap<String, Stat> = TreeMap<String, Stat>()
+
+    override fun visitAidlMethod(
+            context: JavaContext,
+            node: UMethod,
+            interfaceName: String,
+            body: UBlockExpression
+    ) {
+        val packageName = context.uastFile?.packageName ?: "<unknown>"
+        var packageStat = packagesStats.getOrDefault(packageName, Stat())
+        when {
+            node.hasAnnotation(ANNOTATION_ENFORCE_PERMISSION) -> packageStat.enforced += 1
+            node.hasAnnotation(ANNOTATION_REQUIRES_NO_PERMISSION) -> packageStat.notRequired += 1
+            else -> packageStat.unannotated += 1
+        }
+        packagesStats.put(packageName, packageStat)
+        // context.driver.client.log(null, "%s.%s#%s".format(packageName, interfaceName, node.name))
+    }
+
+    override fun afterCheckRootProject(context: Context) {
+        var total = Stat()
+        for ((packageName, stat) in packagesStats) {
+            context.client.log(null, "package $packageName => $stat")
+            total.unannotated += stat.unannotated
+            total.enforced += stat.enforced
+            total.notRequired += stat.notRequired
+        }
+        val location = Location.create(context.project.dir)
+        context.report(
+            ISSUE_ANNOTATED_AIDL_COUNTER,
+            location,
+            "module ${context.project.name} => $total"
+        )
+    }
+
+    companion object {
+
+        @JvmField
+        val ISSUE_ANNOTATED_AIDL_COUNTER = Issue.create(
+                id = "AnnotatedAidlCounter",
+                briefDescription = "Statistics on the number of annotated AIDL methods.",
+                explanation = "",
+                category = Category.SECURITY,
+                priority = 5,
+                severity = Severity.INFORMATIONAL,
+                implementation = Implementation(
+                        AnnotatedAidlCounter::class.java,
+                        Scope.JAVA_FILE_SCOPE
+                ),
+        )
+    }
+}
diff --git a/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/AnnotatedAidlCounterTest.kt b/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/AnnotatedAidlCounterTest.kt
new file mode 100644
index 0000000..692b7da
--- /dev/null
+++ b/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/AnnotatedAidlCounterTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 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.google.android.lint.aidl
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+@Suppress("UnstableApiUsage")
+class AnnotatedAidlCounterTest : LintDetectorTest() {
+    override fun getDetector(): Detector = AnnotatedAidlCounter()
+
+    override fun getIssues(): List<Issue> = listOf(
+        AnnotatedAidlCounter.ISSUE_ANNOTATED_AIDL_COUNTER,
+    )
+
+    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+    /** No issue scenario */
+
+    fun testDoesNotDetectIssuesCorrectAnnotationOnMethod() {
+        lint().files(java(
+            """
+            package test.pkg;
+            import android.annotation.EnforcePermission;
+            public class TestClass2 extends IFooMethod.Stub {
+                @Override
+                @EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
+                public void testMethod() {}
+            }
+            """).indented(),
+                *stubs
+        )
+        .run()
+        .expect("""
+        app: Information: module app => Stat(unannotated=0, enforced=1, notRequired=0) [AnnotatedAidlCounter]
+        0 errors, 0 warnings
+        """)
+    }
+
+    // A service with permission annotation on the method.
+    private val interfaceIFooMethodStub: TestFile = java(
+        """
+        public interface IFooMethod extends android.os.IInterface {
+         public static abstract class Stub extends android.os.Binder implements IFooMethod {}
+          @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
+          public void testMethod();
+        }
+        """
+    ).indented()
+
+    // A service without any permission annotation.
+    private val interfaceIBarStub: TestFile = java(
+        """
+        public interface IBar extends android.os.IInterface {
+         public static abstract class Stub extends android.os.Binder implements IBar {
+            @Override
+            public void testMethod() {}
+          }
+          public void testMethod();
+        }
+        """
+    ).indented()
+
+    private val manifestPermissionStub: TestFile = java(
+        """
+        package android.Manifest;
+        class permission {
+          public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE";
+        }
+        """
+    ).indented()
+
+    private val enforcePermissionAnnotationStub: TestFile = java(
+        """
+        package android.annotation;
+        public @interface EnforcePermission {}
+        """
+    ).indented()
+
+    private val stubs = arrayOf(interfaceIFooMethodStub, interfaceIBarStub,
+            manifestPermissionStub, enforcePermissionAnnotationStub)
+}