Merge "Wallet quick affordance binder calls on main thread" into main
diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
index e6c94d8..6383ed8 100644
--- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
@@ -1107,7 +1107,7 @@
         private long mDefaultInactiveTimeout =
                 (30 * 60 * 1000L) / (!COMPRESS_TIME ? 1 : 10);
         private static final long DEFAULT_INACTIVE_TIMEOUT_SMALL_BATTERY =
-                (15 * 60 * 1000L) / (!COMPRESS_TIME ? 1 : 10);
+                (60 * 1000L) / (!COMPRESS_TIME ? 1 : 10);
         private long mDefaultSensingTimeout =
                 !COMPRESS_TIME ? 4 * 60 * 1000L : 60 * 1000L;
         private long mDefaultLocatingTimeout =
@@ -1120,7 +1120,7 @@
         private long mDefaultIdleAfterInactiveTimeout =
                 (30 * 60 * 1000L) / (!COMPRESS_TIME ? 1 : 10);
         private static final long DEFAULT_IDLE_AFTER_INACTIVE_TIMEOUT_SMALL_BATTERY =
-                (15 * 60 * 1000L) / (!COMPRESS_TIME ? 1 : 10);
+                (60 * 1000L) / (!COMPRESS_TIME ? 1 : 10);
         private long mDefaultIdlePendingTimeout =
                 !COMPRESS_TIME ? 5 * 60 * 1000L : 30 * 1000L;
         private long mDefaultMaxIdlePendingTimeout =
diff --git a/core/api/current.txt b/core/api/current.txt
index 805ecd4..5b339fa 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -36822,7 +36822,7 @@
     field public static final String ACTION_CAST_SETTINGS = "android.settings.CAST_SETTINGS";
     field public static final String ACTION_CHANNEL_NOTIFICATION_SETTINGS = "android.settings.CHANNEL_NOTIFICATION_SETTINGS";
     field public static final String ACTION_CONDITION_PROVIDER_SETTINGS = "android.settings.ACTION_CONDITION_PROVIDER_SETTINGS";
-    field public static final String ACTION_CREDENTIAL_PROVIDER = "android.settings.CREDENTIAL_PROVIDER";
+    field @FlaggedApi("android.credentials.flags.new_settings_intents") public static final String ACTION_CREDENTIAL_PROVIDER = "android.settings.CREDENTIAL_PROVIDER";
     field public static final String ACTION_DATA_ROAMING_SETTINGS = "android.settings.DATA_ROAMING_SETTINGS";
     field public static final String ACTION_DATA_USAGE_SETTINGS = "android.settings.DATA_USAGE_SETTINGS";
     field public static final String ACTION_DATE_SETTINGS = "android.settings.DATE_SETTINGS";
diff --git a/core/api/removed.txt b/core/api/removed.txt
index 285dcc6a..b7714f1 100644
--- a/core/api/removed.txt
+++ b/core/api/removed.txt
@@ -30,36 +30,6 @@
 
 }
 
-package android.app.slice {
-
-  public final class Slice implements android.os.Parcelable {
-    field @Deprecated public static final String EXTRA_SLIDER_VALUE = "android.app.slice.extra.SLIDER_VALUE";
-    field @Deprecated public static final String SUBTYPE_SLIDER = "slider";
-  }
-
-  public static class Slice.Builder {
-    ctor @Deprecated public Slice.Builder(@NonNull android.net.Uri);
-    method @Deprecated public android.app.slice.Slice.Builder addTimestamp(long, @Nullable String, java.util.List<java.lang.String>);
-    method @Deprecated public android.app.slice.Slice.Builder setSpec(android.app.slice.SliceSpec);
-  }
-
-  public final class SliceItem implements android.os.Parcelable {
-    method @Deprecated public long getTimestamp();
-    field @Deprecated public static final String FORMAT_TIMESTAMP = "long";
-  }
-
-  public class SliceManager {
-    method @Deprecated @Nullable public android.app.slice.Slice bindSlice(@NonNull android.net.Uri, @NonNull java.util.List<android.app.slice.SliceSpec>);
-    method @Deprecated @Nullable public android.app.slice.Slice bindSlice(@NonNull android.content.Intent, @NonNull java.util.List<android.app.slice.SliceSpec>);
-    method @Deprecated public void pinSlice(@NonNull android.net.Uri, @NonNull java.util.List<android.app.slice.SliceSpec>);
-  }
-
-  public abstract class SliceProvider extends android.content.ContentProvider {
-    method @Deprecated public android.app.slice.Slice onBindSlice(android.net.Uri, java.util.List<android.app.slice.SliceSpec>);
-  }
-
-}
-
 package android.app.usage {
 
   public class StorageStatsManager {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index e2b2879..0497c60 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -12191,6 +12191,7 @@
     method public boolean hasExpanded();
     method public boolean hasInteracted();
     method public boolean hasSeen();
+    method @FlaggedApi("android.app.lifetime_extension_refactor") public boolean hasSmartReplied();
     method public boolean hasSnoozed();
     method public boolean hasViewedSettings();
     method public void setDirectReplied();
@@ -12198,6 +12199,7 @@
     method public void setDismissalSurface(int);
     method public void setExpanded();
     method public void setSeen();
+    method @FlaggedApi("android.app.lifetime_extension_refactor") public void setSmartReplied();
     method public void setSnoozed();
     method public void setViewedSettings();
     method public void writeToParcel(android.os.Parcel, int);
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 337e3f1..8c5773a 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -745,6 +745,16 @@
     @TestApi
     public static final int FLAG_USER_INITIATED_JOB = 0x00008000;
 
+    /**
+     * Bit to be bitwise-ored into the {@link #flags} field that should be
+     * set if this notification has been lifetime extended due to a direct reply.
+     *
+     * This flag is for internal use only; applications cannot set this flag directly.
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
+    public static final int FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY = 0x00010000;
+
     private static final List<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Arrays.asList(
             BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class,
             DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class,
diff --git a/core/java/android/app/slice/Slice.java b/core/java/android/app/slice/Slice.java
index 823fdd2..475ee7a 100644
--- a/core/java/android/app/slice/Slice.java
+++ b/core/java/android/app/slice/Slice.java
@@ -195,13 +195,6 @@
      */
     public static final String EXTRA_TOGGLE_STATE = "android.app.slice.extra.TOGGLE_STATE";
     /**
-     * Key to retrieve an extra added to an intent when the value of a slider is changed.
-     * @deprecated remove once support lib is update to use EXTRA_RANGE_VALUE instead
-     * @removed
-     */
-    @Deprecated
-    public static final String EXTRA_SLIDER_VALUE = "android.app.slice.extra.SLIDER_VALUE";
-    /**
      * Key to retrieve an extra added to an intent when the value of an input range is changed.
      */
     public static final String EXTRA_RANGE_VALUE = "android.app.slice.extra.RANGE_VALUE";
@@ -223,13 +216,6 @@
      */
     public static final String SUBTYPE_COLOR = "color";
     /**
-     * Subtype to tag an item as representing a slider.
-     * @deprecated remove once support lib is update to use SUBTYPE_RANGE instead
-     * @removed
-     */
-    @Deprecated
-    public static final String SUBTYPE_SLIDER = "slider";
-    /**
      * Subtype to tag an item as representing a range.
      * Expected to be on an item of format {@link SliceItem#FORMAT_SLICE} containing
      * a {@link #SUBTYPE_VALUE} and possibly a {@link #SUBTYPE_MAX}.
@@ -361,15 +347,6 @@
         private SliceSpec mSpec;
 
         /**
-         * @deprecated TO BE REMOVED
-         * @removed
-         */
-        @Deprecated
-        public Builder(@NonNull Uri uri) {
-            mUri = uri;
-        }
-
-        /**
          * Create a builder which will construct a {@link Slice} for the given Uri.
          * @param uri Uri to tag for this slice.
          * @param spec the spec for this slice.
@@ -413,15 +390,6 @@
         }
 
         /**
-         * @deprecated TO BE REMOVED
-         * @removed
-         */
-        public Builder setSpec(SliceSpec spec) {
-            mSpec = spec;
-            return this;
-        }
-
-        /**
          * Add a sub-slice to the slice being constructed
          * @param subType Optional template-specific type information
          * @see SliceItem#getSubType()
@@ -498,16 +466,6 @@
         }
 
         /**
-         * @deprecated TO BE REMOVED.
-         * @removed
-         */
-        @Deprecated
-        public Slice.Builder addTimestamp(long time, @Nullable @SliceSubtype String subType,
-                @SliceHint List<String> hints) {
-            return addLong(time, subType, hints);
-        }
-
-        /**
          * Add a long to the slice being constructed
          * @param subType Optional template-specific type information
          * @see SliceItem#getSubType()
diff --git a/core/java/android/app/slice/SliceItem.java b/core/java/android/app/slice/SliceItem.java
index ed32a1b..2d6f4a6 100644
--- a/core/java/android/app/slice/SliceItem.java
+++ b/core/java/android/app/slice/SliceItem.java
@@ -102,12 +102,6 @@
      */
     public static final String FORMAT_LONG = "long";
     /**
-     * @deprecated TO BE REMOVED
-     * @removed
-     */
-    @Deprecated
-    public static final String FORMAT_TIMESTAMP = FORMAT_LONG;
-    /**
      * A {@link SliceItem} that contains a {@link RemoteInput}.
      */
     public static final String FORMAT_REMOTE_INPUT = "input";
@@ -257,15 +251,6 @@
     }
 
     /**
-     * @deprecated replaced by {@link #getLong()}
-     * @removed
-     */
-    @Deprecated
-    public long getTimestamp() {
-        return (Long) mObj;
-    }
-
-    /**
      * @param hint The hint to check for
      * @return true if this item contains the given hint
      */
@@ -348,7 +333,7 @@
             case FORMAT_INT:
                 dest.writeInt((Integer) obj);
                 break;
-            case FORMAT_TIMESTAMP:
+            case FORMAT_LONG:
                 dest.writeLong((Long) obj);
                 break;
         }
@@ -368,7 +353,7 @@
                         Slice.CREATOR.createFromParcel(in));
             case FORMAT_INT:
                 return in.readInt();
-            case FORMAT_TIMESTAMP:
+            case FORMAT_LONG:
                 return in.readLong();
             case FORMAT_REMOTE_INPUT:
                 return RemoteInput.CREATOR.createFromParcel(in);
diff --git a/core/java/android/app/slice/SliceManager.java b/core/java/android/app/slice/SliceManager.java
index 1e4934e..2e179d0 100644
--- a/core/java/android/app/slice/SliceManager.java
+++ b/core/java/android/app/slice/SliceManager.java
@@ -141,15 +141,6 @@
     }
 
     /**
-     * @deprecated TO BE REMOVED
-     * @removed
-     */
-    @Deprecated
-    public void pinSlice(@NonNull Uri uri, @NonNull List<SliceSpec> specs) {
-        pinSlice(uri, new ArraySet<>(specs));
-    }
-
-    /**
      * Remove a pin for a slice.
      * <p>
      * If the slice has no other pins/callbacks then the slice will be unpinned.
@@ -273,15 +264,6 @@
     }
 
     /**
-     * @deprecated TO BE REMOVED
-     * @removed
-     */
-    @Deprecated
-    public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
-        return bindSlice(uri, new ArraySet<>(supportedSpecs));
-    }
-
-    /**
      * Turns a slice intent into a slice uri. Expects an explicit intent.
      * <p>
      * This goes through a several stage resolution process to determine if any slice
@@ -412,17 +394,6 @@
     }
 
     /**
-     * @deprecated TO BE REMOVED.
-     * @removed
-     */
-    @Deprecated
-    @Nullable
-    public Slice bindSlice(@NonNull Intent intent,
-            @NonNull List<SliceSpec> supportedSpecs) {
-        return bindSlice(intent, new ArraySet<>(supportedSpecs));
-    }
-
-    /**
      * Determine whether a particular process and user ID has been granted
      * permission to access a specific slice URI.
      *
diff --git a/core/java/android/app/slice/SliceProvider.java b/core/java/android/app/slice/SliceProvider.java
index 63835cb..42c3aa6 100644
--- a/core/java/android/app/slice/SliceProvider.java
+++ b/core/java/android/app/slice/SliceProvider.java
@@ -209,15 +209,6 @@
      * @see Slice#HINT_PARTIAL
      */
     public Slice onBindSlice(Uri sliceUri, Set<SliceSpec> supportedSpecs) {
-        return onBindSlice(sliceUri, new ArrayList<>(supportedSpecs));
-    }
-
-    /**
-     * @deprecated TO BE REMOVED
-     * @removed
-     */
-    @Deprecated
-    public Slice onBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs) {
         return null;
     }
 
@@ -479,7 +470,7 @@
         } finally {
             Handler.getMain().removeCallbacks(mAnr);
         }
-        Slice.Builder parent = new Slice.Builder(sliceUri);
+        Slice.Builder parent = new Slice.Builder(sliceUri, null);
         Slice.Builder childAction = new Slice.Builder(parent)
                 .addIcon(Icon.createWithResource(context,
                         com.android.internal.R.drawable.ic_permission), null,
@@ -492,7 +483,8 @@
                 .getTheme().resolveAttribute(android.R.attr.colorAccent, tv, true);
         int deviceDefaultAccent = tv.data;
 
-        parent.addSubSlice(new Slice.Builder(sliceUri.buildUpon().appendPath("permission").build())
+        Uri subSliceUri = sliceUri.buildUpon().appendPath("permission").build();
+        Slice.Builder subSlice = new Slice.Builder(subSliceUri, null)
                 .addIcon(Icon.createWithResource(context,
                         com.android.internal.R.drawable.ic_arrow_forward), null,
                         Collections.emptyList())
@@ -500,8 +492,8 @@
                         Collections.emptyList())
                 .addInt(deviceDefaultAccent, SUBTYPE_COLOR,
                         Collections.emptyList())
-                .addSubSlice(childAction.build(), null)
-                .build(), null);
+                .addSubSlice(childAction.build(), null);
+        parent.addSubSlice(subSlice.build(), null);
         return parent.addHints(Arrays.asList(Slice.HINT_PERMISSION_REQUEST)).build();
     }
 
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 8f18c5f..1a33b768 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -2542,6 +2542,7 @@
      * ComponentName)} and only use this action to start an activity if they return {@code false}.
      */
     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    @FlaggedApi(android.credentials.flags.Flags.FLAG_NEW_SETTINGS_INTENTS)
     public static final String ACTION_CREDENTIAL_PROVIDER =
             "android.settings.CREDENTIAL_PROVIDER";
 
@@ -8845,6 +8846,24 @@
                 "reduce_bright_colors_persist_across_reboots";
 
         /**
+         * Setting that specifies whether Even Dimmer - a feature that allows the brightness
+         * slider to go below what the display can conventionally do, should be enabled.
+         *
+         * @hide
+         */
+        public static final String EVEN_DIMMER_ACTIVATED =
+                "even_dimmer_activated";
+
+        /**
+         * Setting that specifies which nits level Even Dimmer should allow the screen brightness
+         * to go down to.
+         *
+         * @hide
+         */
+        public static final String EVEN_DIMMER_MIN_NITS =
+                "even_dimmer_min_nits";
+
+        /**
          * List of the enabled print services.
          *
          * N and beyond uses {@link #DISABLED_PRINT_SERVICES}. But this might be used in an upgrade
diff --git a/core/java/android/security/OWNERS b/core/java/android/security/OWNERS
index 33a67ae..533d459 100644
--- a/core/java/android/security/OWNERS
+++ b/core/java/android/security/OWNERS
@@ -8,4 +8,4 @@
 per-file Confirmation*.java = file:/keystore/OWNERS
 per-file FileIntegrityManager.java = file:platform/system/security:/fsverity/OWNERS
 per-file IFileIntegrityService.aidl = file:platform/system/security:/fsverity/OWNERS
-per-file *.aconfig = victorhsieh@google.com
+per-file *.aconfig = victorhsieh@google.com,eranm@google.com
diff --git a/core/java/android/service/notification/NotificationRankingUpdate.java b/core/java/android/service/notification/NotificationRankingUpdate.java
index 2a4cbaf..46ea158 100644
--- a/core/java/android/service/notification/NotificationRankingUpdate.java
+++ b/core/java/android/service/notification/NotificationRankingUpdate.java
@@ -15,7 +15,6 @@
  */
 package android.service.notification;
 
-import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.Notification;
 import android.os.Bundle;
@@ -26,6 +25,7 @@
 import android.system.OsConstants;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import java.nio.ByteBuffer;
@@ -75,10 +75,6 @@
                 }
                 // We only need read-only access to the shared memory region.
                 buffer = mRankingMapFd.mapReadOnly();
-                if (buffer == null) {
-                    mRankingMap = null;
-                    return;
-                }
                 byte[] payload = new byte[buffer.remaining()];
                 buffer.get(payload);
                 mapParcel.unmarshall(payload, 0, payload.length);
@@ -98,7 +94,7 @@
             } finally {
                 mapParcel.recycle();
                 if (buffer != null && mRankingMapFd != null) {
-                    mRankingMapFd.unmap(buffer);
+                    SharedMemory.unmap(buffer);
                     mRankingMapFd.close();
                 }
             }
@@ -210,6 +206,7 @@
                                     new NotificationListenerService.Ranking[0]
                             )
                     );
+            ByteBuffer buffer = null;
 
             try {
                 // Parcels the ranking map and measures its size.
@@ -217,13 +214,10 @@
                 int mapSize = mapParcel.dataSize();
 
                 // Creates a new SharedMemory object with enough space to hold the ranking map.
-                SharedMemory mRankingMapFd = SharedMemory.create(mSharedMemoryName, mapSize);
-                if (mRankingMapFd == null) {
-                    return;
-                }
+                mRankingMapFd = SharedMemory.create(mSharedMemoryName, mapSize);
 
                 // Gets a read/write buffer mapping the entire shared memory region.
-                final ByteBuffer buffer = mRankingMapFd.mapReadWrite();
+                buffer = mRankingMapFd.mapReadWrite();
                 // Puts the ranking map into the shared memory region buffer.
                 buffer.put(mapParcel.marshall(), 0, mapSize);
                 // Protects the region from being written to, by setting it to be read-only.
@@ -238,6 +232,12 @@
                 throw new RuntimeException(e);
             } finally {
                 mapParcel.recycle();
+                // To prevent memory leaks, we can close the ranking map fd here.
+                // Because a reference to this still exists
+                if (buffer != null && mRankingMapFd != null) {
+                    SharedMemory.unmap(buffer);
+                    mRankingMapFd.close();
+                }
             }
         } else {
             out.writeParcelable(mRankingMap, flags);
@@ -247,7 +247,7 @@
     /**
      * @hide
      */
-    public static final @android.annotation.NonNull Parcelable.Creator<NotificationRankingUpdate> CREATOR
+    public static final @NonNull Parcelable.Creator<NotificationRankingUpdate> CREATOR
             = new Parcelable.Creator<NotificationRankingUpdate>() {
         public NotificationRankingUpdate createFromParcel(Parcel parcel) {
             return new NotificationRankingUpdate(parcel);
diff --git a/core/java/android/service/notification/NotificationStats.java b/core/java/android/service/notification/NotificationStats.java
index e5ad85c..07367df 100644
--- a/core/java/android/service/notification/NotificationStats.java
+++ b/core/java/android/service/notification/NotificationStats.java
@@ -15,10 +15,12 @@
  */
 package android.service.notification;
 
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.app.Flags;
 import android.app.RemoteInput;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -36,6 +38,7 @@
     private boolean mSeen;
     private boolean mExpanded;
     private boolean mDirectReplied;
+    private boolean mSmartReplied;
     private boolean mSnoozed;
     private boolean mViewedSettings;
     private boolean mInteracted;
@@ -125,6 +128,9 @@
         mSeen = in.readByte() != 0;
         mExpanded = in.readByte() != 0;
         mDirectReplied = in.readByte() != 0;
+        if (Flags.lifetimeExtensionRefactor()) {
+            mSmartReplied = in.readByte() != 0;
+        }
         mSnoozed = in.readByte() != 0;
         mViewedSettings = in.readByte() != 0;
         mInteracted = in.readByte() != 0;
@@ -137,6 +143,9 @@
         dest.writeByte((byte) (mSeen ? 1 : 0));
         dest.writeByte((byte) (mExpanded ? 1 : 0));
         dest.writeByte((byte) (mDirectReplied ? 1 : 0));
+        if (Flags.lifetimeExtensionRefactor()) {
+            dest.writeByte((byte) (mSmartReplied ? 1 : 0));
+        }
         dest.writeByte((byte) (mSnoozed ? 1 : 0));
         dest.writeByte((byte) (mViewedSettings ? 1 : 0));
         dest.writeByte((byte) (mInteracted ? 1 : 0));
@@ -210,6 +219,23 @@
     }
 
     /**
+     * Returns whether the user has replied to a notification that has a smart reply at least once.
+     */
+    @FlaggedApi(Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
+    public boolean hasSmartReplied() {
+        return mSmartReplied;
+    }
+
+    /**
+     * Records that the user has replied to a notification that has a smart reply at least once.
+     */
+    @FlaggedApi(Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
+    public void setSmartReplied() {
+        mSmartReplied = true;
+        mInteracted = true;
+    }
+
+    /**
      * Returns whether the user has snoozed this notification at least once.
      */
     public boolean hasSnoozed() {
@@ -286,6 +312,9 @@
         if (mSeen != that.mSeen) return false;
         if (mExpanded != that.mExpanded) return false;
         if (mDirectReplied != that.mDirectReplied) return false;
+        if (Flags.lifetimeExtensionRefactor()) {
+            if (mSmartReplied != that.mSmartReplied) return false;
+        }
         if (mSnoozed != that.mSnoozed) return false;
         if (mViewedSettings != that.mViewedSettings) return false;
         if (mInteracted != that.mInteracted) return false;
@@ -297,6 +326,9 @@
         int result = (mSeen ? 1 : 0);
         result = 31 * result + (mExpanded ? 1 : 0);
         result = 31 * result + (mDirectReplied ? 1 : 0);
+        if (Flags.lifetimeExtensionRefactor()) {
+            result = 31 * result + (mSmartReplied ? 1 : 0);
+        }
         result = 31 * result + (mSnoozed ? 1 : 0);
         result = 31 * result + (mViewedSettings ? 1 : 0);
         result = 31 * result + (mInteracted ? 1 : 0);
@@ -311,6 +343,9 @@
         sb.append("mSeen=").append(mSeen);
         sb.append(", mExpanded=").append(mExpanded);
         sb.append(", mDirectReplied=").append(mDirectReplied);
+        if (Flags.lifetimeExtensionRefactor()) {
+            sb.append(", mSmartReplied=").append(mSmartReplied);
+        }
         sb.append(", mSnoozed=").append(mSnoozed);
         sb.append(", mViewedSettings=").append(mViewedSettings);
         sb.append(", mInteracted=").append(mInteracted);
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 90663c7..147c15b 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -216,6 +216,14 @@
         default CompatibilityInfo.Translator getTranslator() {
             return null;
         }
+
+        /**
+         * Notifies when the state of running animation is changed. The state is either "running" or
+         * "idle".
+         *
+         * @param running {@code true} if there is any animation running; {@code false} otherwise.
+         */
+        default void notifyAnimationRunningStateChanged(boolean running) {}
     }
 
     private static final String TAG = "InsetsController";
@@ -749,6 +757,9 @@
                     final InsetsAnimationControlRunner runner = new InsetsResizeAnimationRunner(
                             mFrame, state1, mToState, RESIZE_INTERPOLATOR,
                             ANIMATION_DURATION_RESIZE, mTypes, InsetsController.this);
+                    if (mRunningAnimations.isEmpty()) {
+                        mHost.notifyAnimationRunningStateChanged(true);
+                    }
                     mRunningAnimations.add(new RunningAnimation(runner, runner.getAnimationType()));
                 }
             };
@@ -1382,6 +1393,9 @@
             }
         }
         ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_ANIMATION_RUNNING);
+        if (mRunningAnimations.isEmpty()) {
+            mHost.notifyAnimationRunningStateChanged(true);
+        }
         mRunningAnimations.add(new RunningAnimation(runner, animationType));
         if (DEBUG) Log.d(TAG, "Animation added to runner. useInsetsAnimationThread: "
                 + useInsetsAnimationThread);
@@ -1588,6 +1602,9 @@
                 break;
             }
         }
+        if (mRunningAnimations.isEmpty()) {
+            mHost.notifyAnimationRunningStateChanged(false);
+        }
         onAnimationStateChanged(removedTypes, false /* running */);
     }
 
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index cac5387..ff1e831 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -816,6 +816,8 @@
     private long mFpsPrevTime = -1;
     private int mFpsNumFrames;
 
+    private boolean mInsetsAnimationRunning;
+
     /**
      * The resolved pointer icon type requested by this window.
      * A null value indicates the resolved pointer icon has not yet been calculated.
@@ -2179,6 +2181,10 @@
         }
     }
 
+    void notifyInsetsAnimationRunningStateChanged(boolean running) {
+        mInsetsAnimationRunning = running;
+    }
+
     @Override
     public void requestLayout() {
         if (!mHandlingLayoutInLayoutRequest) {
diff --git a/core/java/android/view/ViewRootInsetsControllerHost.java b/core/java/android/view/ViewRootInsetsControllerHost.java
index a2708ee..40730e8 100644
--- a/core/java/android/view/ViewRootInsetsControllerHost.java
+++ b/core/java/android/view/ViewRootInsetsControllerHost.java
@@ -279,6 +279,13 @@
         return null;
     }
 
+    @Override
+    public void notifyAnimationRunningStateChanged(boolean running) {
+        if (mViewRoot != null) {
+            mViewRoot.notifyInsetsAnimationRunningStateChanged(running);
+        }
+    }
+
     private boolean isVisibleToUser() {
         return mViewRoot.getHostVisibility() == View.VISIBLE;
     }
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index 4d6ed80..3887dd7 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -268,6 +268,13 @@
 
     optional SettingProto enhanced_voice_privacy_enabled = 23 [ (android.privacy).dest = DEST_AUTOMATIC ];
 
+    message EvenDimmer {
+        optional SettingProto even_dimmer_activated = 1 [ (android.privacy).dest = DEST_AUTOMATIC ];
+        optional SettingProto even_dimmer_min_nits = 2 [ (android.privacy).dest = DEST_AUTOMATIC ];
+    }
+
+    optional EvenDimmer even_dimmer = 98;
+
     optional SettingProto font_weight_adjustment = 85 [ (android.privacy).dest = DEST_AUTOMATIC ];
 
     message Gesture {
@@ -712,5 +719,5 @@
 
     // Please insert fields in alphabetical order and group them into messages
     // if possible (to avoid reaching the method limit).
-    // Next tag = 98;
+    // Next tag = 99;
 }
diff --git a/core/res/res/values/config_device_idle.xml b/core/res/res/values/config_device_idle.xml
index bc9ca3d..7a707c0 100644
--- a/core/res/res/values/config_device_idle.xml
+++ b/core/res/res/values/config_device_idle.xml
@@ -28,7 +28,7 @@
     <integer name="device_idle_flex_time_short_ms">60000</integer>
 
     <!-- Default for DeviceIdleController.Constants.LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT -->
-    <integer name="device_idle_light_after_inactive_to_ms">240000</integer>
+    <integer name="device_idle_light_after_inactive_to_ms">60000</integer>
 
     <!-- Default for DeviceIdleController.Constants.LIGHT_IDLE_TIMEOUT -->
     <integer name="device_idle_light_idle_to_ms">300000</integer>
@@ -43,7 +43,7 @@
     <item name="device_idle_light_idle_factor" format="float" type="integer">2.0</item>
 
     <!-- Default for DeviceIdleController.Constants.LIGHT_IDLE_INCREASE_LINEARLY -->
-    <bool name="device_idle_light_idle_increase_linearly">false</bool>
+    <bool name="device_idle_light_idle_increase_linearly">true</bool>
 
     <!-- Default for DeviceIdleController.Constants.LIGHT_IDLE_LINEAR_INCREASE_FACTOR_MS -->
     <integer name="device_idle_light_idle_linear_increase_factor_ms">300000</integer>
@@ -52,7 +52,7 @@
     <integer name="device_idle_light_idle_flex_linear_increase_factor_ms">60000</integer>
 
     <!-- Default for DeviceIdleController.Constants.LIGHT_MAX_IDLE_TIMEOUT -->
-    <integer name="device_idle_light_max_idle_to_ms">900000</integer>
+    <integer name="device_idle_light_max_idle_to_ms">1800000</integer>
 
     <!-- Default for DeviceIdleController.Constants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET -->
     <integer name="device_idle_light_idle_maintenance_min_budget_ms">60000</integer>
@@ -67,13 +67,13 @@
     <integer name="device_idle_min_deep_maintenance_time_ms">30000</integer>
 
     <!-- Default for DeviceIdleController.Constants.INACTIVE_TIMEOUT -->
-    <integer name="device_idle_inactive_to_ms">1800000</integer>
+    <integer name="device_idle_inactive_to_ms">60000</integer>
 
     <!-- Default for DeviceIdleController.Constants.SENSING_TIMEOUT -->
-    <integer name="device_idle_sensing_to_ms">240000</integer>
+    <integer name="device_idle_sensing_to_ms">30000</integer>
 
     <!-- Default for DeviceIdleController.Constants.LOCATING_TIMEOUT -->
-    <integer name="device_idle_locating_to_ms">30000</integer>
+    <integer name="device_idle_locating_to_ms">15000</integer>
 
     <!-- Default for DeviceIdleController.Constants.LOCATION_ACCURACY -->
     <item name="device_idle_location_accuracy" format="float" type="integer">20.0</item>
@@ -85,7 +85,7 @@
     <integer name="device_idle_motion_inactive_to_flex_ms">60000</integer>
 
     <!-- Default for DeviceIdleController.Constants.IDLE_AFTER_INACTIVE_TIMEOUT -->
-    <integer name="device_idle_idle_after_inactive_to_ms">1800000</integer>
+    <integer name="device_idle_idle_after_inactive_to_ms">60000</integer>
 
     <!-- Default for DeviceIdleController.Constants.IDLE_PENDING_TIMEOUT -->
     <integer name="device_idle_idle_pending_to_ms">300000</integer>
@@ -100,7 +100,7 @@
     <integer name="device_idle_quick_doze_delay_to_ms">60000</integer>
 
     <!-- Default for DeviceIdleController.Constants.IDLE_TIMEOUT -->
-    <integer name="device_idle_idle_to_ms">3600000</integer>
+    <integer name="device_idle_idle_to_ms">900000</integer>
 
     <!-- Default for DeviceIdleController.Constants.MAX_IDLE_TIMEOUT -->
     <integer name="device_idle_max_idle_to_ms">21600000</integer>
diff --git a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java
index 0855268..1bdb006 100644
--- a/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java
+++ b/core/tests/coretests/src/android/service/notification/NotificationRankingUpdateTest.java
@@ -472,6 +472,9 @@
         NotificationRankingUpdate nru = generateUpdate(getContext());
         Parcel parcel = Parcel.obtain();
         nru.writeToParcel(parcel, 0);
+        if (Flags.rankingUpdateAshmem()) {
+            assertTrue(nru.isFdNotNullAndClosed());
+        }
         parcel.setDataPosition(0);
         NotificationRankingUpdate nru1 = NotificationRankingUpdate.CREATOR.createFromParcel(parcel);
         // The rankingUpdate file descriptor is only non-null in the new path.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index f5b877a..a3eb429 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -412,6 +412,23 @@
         setLayoutDirection(LAYOUT_DIRECTION_LOCALE);
     }
 
+
+    /** Updates the width of the task view if it changed. */
+    void updateTaskViewContentWidth() {
+        if (mTaskView != null) {
+            int width = getContentWidth();
+            if (mTaskView.getWidth() != width) {
+                FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(width, MATCH_PARENT);
+                mTaskView.setLayoutParams(lp);
+            }
+        }
+    }
+
+    private int getContentWidth() {
+        boolean isStackOnLeft = mPositioner.isStackOnLeft(mStackView.getStackPosition());
+        return mPositioner.getTaskViewContentWidth(isStackOnLeft);
+    }
+
     /**
      * Initialize {@link BubbleController} and {@link BubbleStackView} here, this method must need
      * to be called after view inflate.
@@ -438,7 +455,12 @@
                     mController.getTaskViewTransitions(), mController.getSyncTransactionQueue());
             mTaskView = new TaskView(mContext, mTaskViewTaskController);
             mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener);
-            mExpandedViewContainer.addView(mTaskView);
+
+            // set a fixed width so it is not recalculated as part of a rotation. the width will be
+            // updated manually after the rotation.
+            FrameLayout.LayoutParams lp =
+                    new FrameLayout.LayoutParams(getContentWidth(), MATCH_PARENT);
+            mExpandedViewContainer.addView(mTaskView, lp);
             bringChildToFront(mTaskView);
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index 1efd9df..baa52a0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -375,6 +375,13 @@
         }
     }
 
+    /** Returns the width of the task view content. */
+    public int getTaskViewContentWidth(boolean onLeft) {
+        int[] paddings = getExpandedViewContainerPadding(onLeft, /* isOverflow = */ false);
+        int pointerOffset = showBubblesVertically() ? getPointerSize() : 0;
+        return mPositionRect.width() - paddings[0] - paddings[2] - pointerOffset;
+    }
+
     /** Gets the y position of the expanded view if it was top-aligned. */
     public float getExpandedViewYTopAligned() {
         final int top = getAvailableRect().top;
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 45c948b..91a8ce7 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
@@ -3288,6 +3288,7 @@
             mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY(mExpandedBubble,
                     mPositioner.showBubblesVertically() ? p.y : p.x));
             mExpandedViewContainer.setTranslationX(0f);
+            mExpandedBubble.getExpandedView().updateTaskViewContentWidth();
             mExpandedBubble.getExpandedView().updateView(
                     mExpandedViewContainer.getLocationOnScreen());
             updatePointerPosition(false /* forIme */);
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/Android.bp b/libs/WindowManager/Shell/tests/flicker/pip/Android.bp
index 386983c..b9b56c2 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/pip/Android.bp
@@ -132,5 +132,6 @@
 
 csuite_test {
     name: "csuite-1p3p-pip-flickers",
+    test_plan_include: "csuitePlan.xml",
     test_config_template: "csuiteDefaultTemplate.xml",
 }
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
index fafd37b..f5a8655 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
@@ -79,8 +79,6 @@
                 value="appops set com.android.shell android:mock_location deny"/>
     </target_preparer>
 
-    <target_preparer class="com.android.csuite.core.AppCrawlTesterHostPreparer"/>
-
     <!-- Use app crawler to log into Netflix -->
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
         <option name="run-command"
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuitePlan.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuitePlan.xml
new file mode 100644
index 0000000..a2fc6b4
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/pip/csuitePlan.xml
@@ -0,0 +1,3 @@
+<configuration description="Flicker tests C-Suite Crawler Test Plan">
+  <target_preparer class="com.android.csuite.core.AppCrawlTesterHostPreparer"/>
+</configuration>
\ No newline at end of file
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 8412cba..5c09b16 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -251,6 +251,8 @@
         Settings.Secure.STYLUS_HANDWRITING_ENABLED,
         Settings.Secure.DEFAULT_NOTE_TASK_PROFILE,
         Settings.Secure.CREDENTIAL_SERVICE,
-        Settings.Secure.CREDENTIAL_SERVICE_PRIMARY
+        Settings.Secure.CREDENTIAL_SERVICE_PRIMARY,
+        Settings.Secure.EVEN_DIMMER_ACTIVATED,
+        Settings.Secure.EVEN_DIMMER_MIN_NITS
     };
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 9197554..b0169a1 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -110,6 +110,9 @@
         VALIDATORS.put(Secure.FONT_WEIGHT_ADJUSTMENT, ANY_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.REDUCE_BRIGHT_COLORS_LEVEL, PERCENTAGE_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.REDUCE_BRIGHT_COLORS_PERSIST_ACROSS_REBOOTS, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.EVEN_DIMMER_ACTIVATED, BOOLEAN_VALIDATOR);
+        VALIDATORS.put(Secure.EVEN_DIMMER_MIN_NITS,
+                new InclusiveFloatRangeValidator(0.0f, Float.MAX_VALUE));
         VALIDATORS.put(Secure.TTS_DEFAULT_RATE, NON_NEGATIVE_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.TTS_DEFAULT_PITCH, NON_NEGATIVE_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.TTS_DEFAULT_SYNTH, PACKAGE_NAME_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index a509ba3..a978889 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -2135,6 +2135,15 @@
                 Settings.Secure.ENHANCED_VOICE_PRIVACY_ENABLED,
                 SecureSettingsProto.ENHANCED_VOICE_PRIVACY_ENABLED);
 
+        final long evenDimmerToken = p.start(SecureSettingsProto.EVEN_DIMMER);
+        dumpSetting(s, p,
+                Settings.Secure.EVEN_DIMMER_ACTIVATED,
+                SecureSettingsProto.EvenDimmer.EVEN_DIMMER_ACTIVATED);
+        dumpSetting(s, p,
+                Settings.Secure.EVEN_DIMMER_MIN_NITS,
+                SecureSettingsProto.EvenDimmer.EVEN_DIMMER_MIN_NITS);
+        p.end(evenDimmerToken);
+
         final long gestureToken = p.start(SecureSettingsProto.GESTURE);
         dumpSetting(s, p,
                 Settings.Secure.AWARE_ENABLED,
diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml
index d9385c7..bcc3c83 100644
--- a/packages/SystemUI/res/values-night/colors.xml
+++ b/packages/SystemUI/res/values-night/colors.xml
@@ -104,4 +104,7 @@
     <!-- Internet Dialog -->
     <color name="connected_network_primary_color">@color/material_dynamic_primary80</color>
     <color name="connected_network_secondary_color">@color/material_dynamic_secondary80</color>
+
+    <!-- Keyboard shortcut helper dialog -->
+    <color name="ksh_key_item_color">@*android:color/system_on_surface_variant_dark</color>
 </resources>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index e124aa0..5f6a39a 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -119,7 +119,7 @@
 
     <!-- Keyboard shortcuts colors -->
     <color name="ksh_application_group_color">#fff44336</color>
-    <color name="ksh_key_item_color">?androidprv:attr/materialColorOnSurfaceVariant</color>
+    <color name="ksh_key_item_color">@*android:color/system_on_surface_variant_light</color>
     <color name="ksh_key_item_background">?androidprv:attr/materialColorSurfaceContainerHighest</color>
 
     <color name="instant_apps_color">#ff4d5a64</color>
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
index eba1c25..3884184 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
@@ -36,11 +36,11 @@
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.qs.tileimpl.HeightOverrideable;
 import com.android.systemui.tuner.TunerService;
+import com.android.systemui.util.concurrency.DelayableExecutor;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
-import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 
@@ -64,6 +64,7 @@
 
     private static final String TAG = "QSAnimator";
 
+    private static final int ANIMATORS_UPDATE_DELAY_MS = 100;
     private static final float EXPANDED_TILE_DELAY = .86f;
     //Non first page delays
     private static final float QS_TILE_LABEL_FADE_OUT_START = 0.15f;
@@ -133,7 +134,7 @@
     private int mLastQQSTileHeight;
     private float mLastPosition;
     private final QSHost mHost;
-    private final Executor mExecutor;
+    private final DelayableExecutor mExecutor;
     private boolean mShowCollapsedOnKeyguard;
     private int mQQSTop;
 
@@ -144,7 +145,7 @@
     public QSAnimator(@RootView View rootView, QuickQSPanel quickPanel,
             QSPanelController qsPanelController,
             QuickQSPanelController quickQSPanelController, QSHost qsTileHost,
-            @Main Executor executor, TunerService tunerService,
+            @Main DelayableExecutor executor, TunerService tunerService,
             QSExpansionPathInterpolator qsExpansionPathInterpolator) {
         mQsRootView = rootView;
         mQuickQsPanel = quickPanel;
@@ -753,7 +754,10 @@
     public void onTilesChanged() {
         // Give the QS panels a moment to generate their new tiles, then create all new animators
         // hooked up to the new views.
-        mExecutor.execute(mUpdateAnimators);
+        mExecutor.executeDelayed(mUpdateAnimators, ANIMATORS_UPDATE_DELAY_MS);
+
+        // Also requests a lazy animators update in case the animation starts before the executor.
+        requestAnimatorUpdate();
     }
 
     private final TouchAnimator.Listener mNonFirstPageListener =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
index 31893b4..e90ddf9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
@@ -15,37 +15,48 @@
 
 package com.android.systemui.statusbar.notification.domain.interactor
 
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.statusbar.notification.collection.render.NotifStats
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 
 class ActiveNotificationsInteractor
 @Inject
 constructor(
     private val repository: ActiveNotificationListRepository,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
 ) {
     /** Notifications actively presented to the user in the notification stack, in order. */
     val topLevelRepresentativeNotifications: Flow<List<ActiveNotificationModel>> =
-        repository.activeNotifications.map { store ->
-            store.renderList.map { key ->
-                val entry =
-                    store[key]
-                        ?: error("Could not find notification with key $key in active notif store.")
-                when (entry) {
-                    is ActiveNotificationGroupModel -> entry.summary
-                    is ActiveNotificationModel -> entry
+        repository.activeNotifications
+            .map { store ->
+                store.renderList.map { key ->
+                    val entry =
+                        store[key]
+                            ?: error(
+                                "Could not find notification with key $key in active notif store."
+                            )
+                    when (entry) {
+                        is ActiveNotificationGroupModel -> entry.summary
+                        is ActiveNotificationModel -> entry
+                    }
                 }
             }
-        }
+            .flowOn(backgroundDispatcher)
 
     /** Are any notifications being actively presented in the notification stack? */
     val areAnyNotificationsPresent: Flow<Boolean> =
-        repository.activeNotifications.map { it.renderList.isNotEmpty() }.distinctUntilChanged()
+        repository.activeNotifications
+            .map { it.renderList.isNotEmpty() }
+            .distinctUntilChanged()
+            .flowOn(backgroundDispatcher)
 
     /**
      * The same as [areAnyNotificationsPresent], but without flows, for easy access in synchronous
@@ -59,6 +70,7 @@
         repository.notifStats
             .map { it.hasClearableAlertingNotifs || it.hasClearableSilentNotifs }
             .distinctUntilChanged()
+            .flowOn(backgroundDispatcher)
 
     fun setNotifStats(notifStats: NotifStats) {
         repository.notifStats.value = notifStats
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt
index 87b8e55..73341db 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt
@@ -15,19 +15,24 @@
  */
 package com.android.systemui.statusbar.notification.domain.interactor
 
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardViewStateRepository
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
 
 /** Domain logic pertaining to notifications on the keyguard. */
 class NotificationsKeyguardInteractor
 @Inject
 constructor(
     repository: NotificationsKeyguardViewStateRepository,
+    @Background backgroundDispatcher: CoroutineDispatcher,
 ) {
     /** Is a pulse expansion occurring? */
-    val isPulseExpanding: Flow<Boolean> = repository.isPulseExpanding
+    val isPulseExpanding: Flow<Boolean> = repository.isPulseExpanding.flowOn(backgroundDispatcher)
 
     /** Are notifications fully hidden from view? */
-    val areNotificationsFullyHidden: Flow<Boolean> = repository.areNotificationsFullyHidden
+    val areNotificationsFullyHidden: Flow<Boolean> =
+        repository.areNotificationsFullyHidden.flowOn(backgroundDispatcher)
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index 1dbb297..62c0ebe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -22,6 +22,8 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher;
+
 import android.content.res.Resources;
 import android.os.Handler;
 import android.os.Looper;
@@ -293,8 +295,10 @@
                 )
         );
 
-        mActiveNotificationsInteractor =
-                new ActiveNotificationsInteractor(new ActiveNotificationListRepository());
+        mActiveNotificationsInteractor = new ActiveNotificationsInteractor(
+                        new ActiveNotificationListRepository(),
+                        StandardTestDispatcher(/* scheduler = */ null, /* name = */ null)
+                );
 
         KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext);
         keyguardStatusView.setId(R.id.keyguard_status_view);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
index b86f841..6374d5e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt
@@ -25,14 +25,19 @@
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 
 @SmallTest
 class RenderNotificationsListInteractorTest : SysuiTestCase() {
+    private val backgroundDispatcher = StandardTestDispatcher()
+    private val testScope = TestScope(backgroundDispatcher)
 
     private val notifsRepository = ActiveNotificationListRepository()
-    private val notifsInteractor = ActiveNotificationsInteractor(notifsRepository)
+    private val notifsInteractor =
+        ActiveNotificationsInteractor(notifsRepository, backgroundDispatcher)
     private val underTest =
         RenderNotificationListInteractor(
             notifsRepository,
@@ -40,21 +45,26 @@
         )
 
     @Test
-    fun setRenderedList_preservesOrdering() = runTest {
-        val notifs by collectLastValue(notifsInteractor.topLevelRepresentativeNotifications)
-        val keys = (1..50).shuffled().map { "$it" }
-        val entries =
-            keys.map {
-                mock<ListEntry> {
-                    val mockRep = mock<NotificationEntry> {
-                        whenever(key).thenReturn(it)
-                        whenever(sbn).thenReturn(mock())
-                        whenever(icons).thenReturn(mock())
+    fun setRenderedList_preservesOrdering() =
+        testScope.runTest {
+            val notifs by collectLastValue(notifsInteractor.topLevelRepresentativeNotifications)
+            val keys = (1..50).shuffled().map { "$it" }
+            val entries =
+                keys.map {
+                    mock<ListEntry> {
+                        val mockRep =
+                            mock<NotificationEntry> {
+                                whenever(key).thenReturn(it)
+                                whenever(sbn).thenReturn(mock())
+                                whenever(icons).thenReturn(mock())
+                            }
+                        whenever(representativeEntry).thenReturn(mockRep)
                     }
-                    whenever(representativeEntry).thenReturn(mockRep)
                 }
-            }
-        underTest.setRenderedList(entries)
-        assertThat(notifs).comparingElementsUsing(byKey).containsExactlyElementsIn(keys).inOrder()
-    }
+            underTest.setRenderedList(entries)
+            assertThat(notifs)
+                .comparingElementsUsing(byKey)
+                .containsExactlyElementsIn(keys)
+                .inOrder()
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index ff5c026..7558974 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -35,6 +35,7 @@
 import static org.mockito.Mockito.when;
 
 import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
+import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher;
 
 import android.metrics.LogMaker;
 import android.testing.AndroidTestingRunner;
@@ -169,7 +170,8 @@
             new ActiveNotificationListRepository();
 
     private final ActiveNotificationsInteractor mActiveNotificationsInteractor =
-            new ActiveNotificationsInteractor(mActiveNotificationsRepository);
+            new ActiveNotificationsInteractor(mActiveNotificationsRepository,
+                    StandardTestDispatcher(/* scheduler = */ null, /* name = */ null));
 
     private final SeenNotificationsInteractor mSeenNotificationsInteractor =
             new SeenNotificationsInteractor(mActiveNotificationsRepository);
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorKosmos.kt
index 3d7fb6d..01f4535 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorKosmos.kt
@@ -17,7 +17,10 @@
 package com.android.systemui.statusbar.notification.domain.interactor
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
 
 val Kosmos.activeNotificationsInteractor by
-    Kosmos.Fixture { ActiveNotificationsInteractor(activeNotificationListRepository) }
+    Kosmos.Fixture {
+        ActiveNotificationsInteractor(activeNotificationListRepository, testDispatcher)
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt
index 61a38b8..432464e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt
@@ -18,11 +18,13 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository
 import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
 
 val Kosmos.notificationsKeyguardInteractor by Fixture {
     NotificationsKeyguardInteractor(
         repository = notificationsKeyguardViewStateRepository,
+        backgroundDispatcher = testDispatcher,
     )
 }
diff --git a/ravenwood/README.md b/ravenwood/README.md
new file mode 100644
index 0000000..9c4fda7
--- /dev/null
+++ b/ravenwood/README.md
@@ -0,0 +1,28 @@
+# Ravenwood
+
+Ravenwood is an officially-supported lightweight unit testing environment for Android platform code that runs on the host.
+
+Ravenwood’s focus on Android platform use-cases, improved maintainability, and device consistency distinguishes it from Robolectric, which remains a popular choice for app testing.
+
+## Background
+
+Executing tests on a typical Android device has substantial overhead, such as flashing the build, waiting for the boot to complete, and retrying tests that fail due to general flakiness.
+
+In contrast, defining a lightweight unit testing environment mitigates these issues by running directly from build artifacts (no flashing required), runs immediately (no booting required), and runs in an isolated environment (less flakiness).
+
+## Guiding principles
+Here’s a summary of the guiding principles for Ravenwood, aimed at addressing Robolectric design concerns and better supporting Android platform developers:
+
+* **API support for Ravenwood is opt-in.**  Teams that own APIs decide exactly what, and how, they support their API functionality being available to tests.  When an API hasn’t opted-in, the API signatures remain available for tests to compile against and/or mock, but they throw when called under a Ravenwood environment.
+    * _Contrasted with Robolectric which attempts to run API implementations as-is, causing maintenance pains as teams maintain or redesign their API internals._
+* **API support and customizations for Ravenwood appear directly inline with relevant code.** This improves maintenance of APIs by providing awareness of what code runs under Ravenwood, including the ability to replace code at a per-method level when Ravenwood-specific customization is needed.
+    * _Contrasted with Robolectric which maintains customized behavior in separate “Shadow” classes that are difficult for maintainers to be aware of._
+* **APIs supported under Ravenwood are tested to remain consistent with physical devices.**  As teams progressively opt-in supporting APIs under Ravenwood, we’re requiring they bring along “bivalent” tests (such as the relevant CTS) to validate that Ravenwood behaves just like a physical device.
+    * _Contrasted with Robolectric, which has limited (and forked) testing of their environment, increasing their risk of accidental divergence over time and misleading “passing” signals._
+* **Ravenwood aims to support more “real” code.**  As API owners progressively opt-in their code, they have the freedom to provide either a limited “fake” that is a faithful emulation of how a device behaves, or they can bring more “real” code that runs on physical devices.
+    * _Contrasted with Robolectric, where support for “real” code ends at the app process boundary, such as a call into `system_server`._
+
+## More details
+
+* [Ravenwood for Test Authors](test-authors.md)
+* [Ravenwood for API Maintainers](api-maintainers.md)
diff --git a/ravenwood/api-maintainers.md b/ravenwood/api-maintainers.md
new file mode 100644
index 0000000..30e899c
--- /dev/null
+++ b/ravenwood/api-maintainers.md
@@ -0,0 +1,73 @@
+# Ravenwood for API Maintainers
+
+By default, Android APIs aren’t opted-in to Ravenwood, and they default to throwing when called under the Ravenwood environment.
+
+To opt-in to supporting an API under Ravenwood, you can use the inline annotations documented below to customize your API behavior when running under Ravenwood.  Because these annotations are inline in the relevant platform source code, they serve as valuable reminders to future API maintainers of Ravenwood support expectations.
+
+> **Note:** to ensure that API teams are well-supported during early Ravenwood onboarding, the Ravenwood team is manually maintaining an allow-list of classes that are able to use Ravenwood annotations.  Please reach out to ravenwood@ so we can offer design advice and allow-list your APIs.
+
+These Ravenwood-specific annotations have no bearing on the status of an API being public, `@SystemApi`, `@TestApi`, `@hide`, etc.  Ravenwood annotations are an orthogonal concept that are only consumed by the internal `hoststubgen` tool during a post-processing step that generates the Ravenwood runtime environment.  Teams that own APIs can continue to refactor opted-in `@hide` implementation details, as long as the test-visible behavior continues passing.
+
+As described in our Guiding Principles, when a team opts-in an API, we’re requiring that they bring along “bivalent” tests (such as the relevant CTS) to validate that Ravenwood behaves just like a physical device.  At the moment this means adding the bivalent tests to relevant `TEST_MAPPING` files to ensure they remain consistently passing over time.  These bivalent tests are important because they progressively provide the foundation on which higher-level unit tests place their trust.
+
+## Opt-in to supporting a single method while other methods remained opt-out
+
+```
+@RavenwoodKeepPartialClass
+public class MyManager {
+    @RavenwoodKeep
+    public static String modeToString(int mode) {
+        // This method implementation runs as-is on both devices and Ravenwood
+    }
+
+    public static void doComplex() {
+        // This method implementation runs as-is on devices, but because there
+        // is no method-level annotation, and the class-level default is
+        // “keep partial”, this method is not supported under Ravenwood and
+        // will throw
+    }
+}
+```
+
+## Opt-in an entire class with opt-out of specific methods
+
+```
+@RavenwoodKeepWholeClass
+public class MyStruct {
+    public void doSimple() {
+        // This method implementation runs as-is on both devices and Ravenwood,
+        // implicitly inheriting the class-level annotation
+    }
+
+    @RavenwoodThrow
+    public void doComplex() {
+        // This method implementation runs as-is on devices, but the
+        // method-level annotation overrides the class-level annotation, so
+        // this method is not supported under Ravenwood and will throw
+    }
+}
+```
+
+## Replace a complex method when under Ravenwood
+
+```
+@RavenwoodKeepWholeClass
+public class MyStruct {
+    @RavenwoodReplace
+    public void doComplex() {
+        // This method implementation runs as-is on devices, but the
+        // implementation is replaced/substituted by the
+        // doComplex$ravenwood() method implementation under Ravenwood
+    }
+
+    public void doComplex$ravenwood() {
+        // This method implementation only runs under Ravenwood
+    }
+}
+```
+
+## General strategies for side-stepping tricky dependencies
+
+The “replace” strategy described above is quite powerful, and can be used in creative ways to sidestep tricky underlying dependencies that aren’t ready yet.
+
+For example, consider a constructor or static initializer that relies on unsupported functionality from another team.  By factoring the unsupported logic into a dedicated method, that method can then be replaced under Ravenwood to offer baseline functionality.
diff --git a/ravenwood/test-authors.md b/ravenwood/test-authors.md
new file mode 100644
index 0000000..2b5bd908
--- /dev/null
+++ b/ravenwood/test-authors.md
@@ -0,0 +1,132 @@
+# Ravenwood for Test Authors
+
+The Ravenwood testing environment runs inside a single Java process on the host side, and provides a limited yet growing set of Android API functionality.
+
+Ravenwood explicitly does not support “large” integration tests that expect a fully booted Android OS.  Instead, it’s more suited for “small” and “medium” tests where your code-under-test has been factored to remove dependencies on a fully booted device.
+
+When writing tests under Ravenwood, all Android API symbols associated with your declared `sdk_version` are available to link against using, but unsupported APIs will throw an exception.  This design choice enables mocking of unsupported APIs, and supports sharing of test code to build “bivalent” test suites that run against either Ravenwood or a traditional device.
+
+## Typical test structure
+
+Below are the typical steps needed to add a straightforward “small” unit test:
+
+* Define an `android_ravenwood_test` rule in your `Android.bp` file:
+
+```
+android_ravenwood_test {
+    name: "MyTestsRavenwood",
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.test.rules",
+    ],
+    srcs: [
+        "src/com/example/MyCode.java",
+        "tests/src/com/example/MyCodeTest.java",
+    ],
+    sdk_version: "test_current",
+    auto_gen_config: true,
+}
+```
+
+* Write your unit test just like you would for an Android device:
+
+```
+@RunWith(AndroidJUnit4.class)
+public class MyCodeTest {
+    @Test
+    public void testSimple() {
+        // ...
+    }
+}
+```
+
+* APIs available under Ravenwood are stateless by default.  If your test requires explicit states (such as defining the UID you’re running under, or requiring a main `Looper` thread), add a `RavenwoodRule` to declare that:
+
+```
+@RunWith(AndroidJUnit4.class)
+public class MyCodeTest {
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProcessApp()
+            .setProvideMainThread(true)
+            .build();
+```
+
+Once you’ve defined your test, you can use typical commands to execute it locally:
+
+```
+$ atest MyTestsRavenwood
+```
+
+> **Note:** There's a known bug where `atest` currently requires a connected device to run Ravenwood tests, but that device isn't used for testing.
+
+You can also run your new tests automatically via `TEST_MAPPING` rules like this:
+
+```
+{
+  "ravenwood-presubmit": [
+    {
+      "name": "MyTestsRavenwood",
+      "host": true
+    }
+  ]
+}
+```
+
+## Strategies for migration/bivalent tests
+
+Ravenwood aims to support tests that are written in a “bivalent” way, where the same test code can run on both a real Android device and under a Ravenwood environment.
+
+In situations where a test method depends on API functionality not yet available under Ravenwood, we provide an annotation to quietly “ignore” that test under Ravenwood, while continuing to validate that test on real devices.  Please note that your test must declare a `RavenwoodRule` for the annotation to take effect.
+
+Test authors are encouraged to provide a `blockedBy` or `reason` argument to help future maintainers understand why a test is being ignored, and under what conditions it might be supported in the future.
+
+```
+@RunWith(AndroidJUnit4.class)
+public class MyCodeTest {
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+    @Test
+    public void testSimple() {
+        // Simple test that runs on both devices and Ravenwood
+    }
+
+    @Test
+    @IgnoreUnderRavenwood(blockedBy = PackageManager.class)
+    public void testComplex() {
+        // Complex test that runs on devices, but is ignored under Ravenwood
+    }
+}
+```
+
+## Strategies for unsupported APIs
+
+As you write tests against Ravenwood, you’ll likely discover API dependencies that aren’t supported yet.  Here’s a few strategies that can help you make progress:
+
+* Your code-under-test may benefit from subtle dependency refactoring to reduce coupling.  (For example, providing a specific `File` argument instead of deriving it internally from a `Context`.)
+* Although mocking code that your team doesn’t own is a generally discouraged testing practice, it can be a valuable pressure relief valve when a dependency isn’t yet supported.
+
+## Strategies for debugging test development
+
+When writing tests you may encounter odd or hard to debug behaviors.  One good place to start is at the beginning of the logs stored by atest:
+
+```
+$ atest MyTestsRavenwood
+...
+Test Logs have saved in /tmp/atest_result/20231128_094010_0e90t8v8/log
+Run 'atest --history' to review test result history.
+```
+
+The most useful logs are in the `isolated-java-logs` text file, which can typically be tab-completed by copy-pasting the logs path mentioned in the atest output:
+
+```
+$ less /tmp/atest_result/20231128_133105_h9al__79/log/i*/i*/isolated-java-logs*
+```
+
+Here are some common known issues and recommended workarounds:
+
+* Some code may unconditionally interact with unsupported APIs, such as via static initializers.  One strategy is to shift the logic into `@Before` methods and make it conditional by testing `RavenwoodRule.isUnderRavenwood()`.
+* Some code may reference API symbols not yet present in the Ravenwood runtime, such as ART or ICU internals, or APIs from Mainline modules.  One strategy is to refactor to avoid these internal dependencies, but Ravenwood aims to better support them soon.
+    * This may also manifest as very odd behavior, such as test not being executed at all, tracked by bug #312517322
+    * This may also manifest as an obscure Mockito error claiming “Mockito can only mock non-private & non-final classes”
diff --git a/services/core/java/com/android/server/PinnerService.java b/services/core/java/com/android/server/PinnerService.java
index 23a30f9..c5c2b0b 100644
--- a/services/core/java/com/android/server/PinnerService.java
+++ b/services/core/java/com/android/server/PinnerService.java
@@ -1044,7 +1044,6 @@
         if (DEBUG) {
             Slog.d(TAG, "pin file: " + fileToPin + " use-pinlist: " + attemptPinIntrospection);
         }
-        Trace.beginSection("pinFile:" + fileToPin);
         ZipFile fileAsZip = null;
         InputStream pinRangeStream = null;
         try {
@@ -1067,7 +1066,6 @@
         } finally {
             safeClose(pinRangeStream);
             safeClose(fileAsZip);  // Also closes any streams we've opened
-            Trace.endSection();
         }
     }
 
diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java
index 60f087f..382ee6e 100644
--- a/services/core/java/com/android/server/Watchdog.java
+++ b/services/core/java/com/android/server/Watchdog.java
@@ -103,7 +103,7 @@
     // will be half the full timeout).
     //
     // The pre-watchdog event is similar to a full watchdog except it does not crash system server.
-    private static final int PRE_WATCHDOG_TIMEOUT_RATIO = 2;
+    private static final int PRE_WATCHDOG_TIMEOUT_RATIO = 3;
 
     // These are temporally ordered: larger values as lateness increases
     static final int COMPLETED = 0;
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 69e3aaf..f47482d 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -142,6 +142,7 @@
         "core_experiments_team_internal",
         "core_graphics",
         "dck_framework",
+        "devoptions_settings",
         "game",
         "haptics",
         "hardware_backed_security_mainline",
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java
index 8a884b6..42ebc40 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamper.java
@@ -25,7 +25,7 @@
 import java.io.PrintWriter;
 
 /**
- * Provides max allowed brightness
+ * Provides brightness range constraints
  */
 abstract class BrightnessClamper<T> {
 
@@ -74,6 +74,7 @@
     protected enum Type {
         THERMAL,
         POWER,
-        BEDTIME_MODE
+        BEDTIME_MODE,
+        LUX,
     }
 }
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index 765608e..01694dd 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -68,14 +68,14 @@
     private boolean mClamperApplied = false;
 
     public BrightnessClamperController(Handler handler,
-            ClamperChangeListener clamperChangeListener, DisplayDeviceData data,  Context context,
+            ClamperChangeListener clamperChangeListener, DisplayDeviceData data, Context context,
             DisplayManagerFlags flags) {
         this(new Injector(), handler, clamperChangeListener, data, context, flags);
     }
 
     @VisibleForTesting
     BrightnessClamperController(Injector injector, Handler handler,
-            ClamperChangeListener clamperChangeListener, DisplayDeviceData data,  Context context,
+            ClamperChangeListener clamperChangeListener, DisplayDeviceData data, Context context,
             DisplayManagerFlags flags) {
         mDeviceConfigParameterProvider = injector.getDeviceConfigParameterProvider();
         mHandler = handler;
@@ -147,7 +147,8 @@
      * Should be moved to DisplayBrightnessState OR derived from DisplayBrightnessState
      * TODO: b/263362199
      */
-    @BrightnessInfo.BrightnessMaxReason public int getBrightnessMaxReason() {
+    @BrightnessInfo.BrightnessMaxReason
+    public int getBrightnessMaxReason() {
         if (mClamperType == null) {
             return BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE;
         } else if (mClamperType == Type.THERMAL) {
@@ -241,12 +242,15 @@
                     new BrightnessThermalClamper(handler, clamperChangeListener, data));
             if (flags.isPowerThrottlingClamperEnabled()) {
                 clampers.add(new BrightnessPowerClamper(handler, clamperChangeListener,
-                            data));
+                        data));
             }
             if (flags.isBrightnessWearBedtimeModeClamperEnabled()) {
                 clampers.add(new BrightnessWearBedtimeModeClamper(handler, context,
                         clamperChangeListener, data));
             }
+            if (flags.isEvenDimmerEnabled()) {
+                clampers.add(new BrightnessMinClamper(handler, clamperChangeListener, context));
+            }
             return clampers;
         }
 
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessMinClamper.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessMinClamper.java
new file mode 100644
index 0000000..71efca1
--- /dev/null
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessMinClamper.java
@@ -0,0 +1,137 @@
+/*
+ * 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.display.brightness.clamper;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.display.utils.DebugUtils;
+
+import java.io.PrintWriter;
+
+/**
+ * Class used to prevent the screen brightness dipping below a certain value, based on current
+ * lux conditions.
+ */
+public class BrightnessMinClamper extends BrightnessClamper {
+
+    // To enable these logs, run:
+    // 'adb shell setprop persist.log.tag.BrightnessMinClamper DEBUG && adb reboot'
+    private static final String TAG = "BrightnessMinClamper";
+    private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
+
+    private final SettingsObserver mSettingsObserver;
+
+    ContentResolver mContentResolver;
+    private float mNitsLowerBound;
+
+    @VisibleForTesting
+    BrightnessMinClamper(Handler handler,
+            BrightnessClamperController.ClamperChangeListener listener, Context context) {
+        super(handler, listener);
+
+        mContentResolver = context.getContentResolver();
+        mSettingsObserver = new SettingsObserver(mHandler);
+        mHandler.post(() -> {
+            start();
+        });
+    }
+
+    private void recalculateLowerBound() {
+        final int userId = UserHandle.USER_CURRENT;
+        float settingNitsLowerBound = Settings.Secure.getFloatForUser(
+                mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS,
+                /* def= */ PowerManager.BRIGHTNESS_MIN, userId);
+
+        boolean isActive = Settings.Secure.getIntForUser(mContentResolver,
+                Settings.Secure.EVEN_DIMMER_ACTIVATED,
+                /* def= */ 0, userId) == 1;
+
+        // TODO: luxBasedNitsLowerBound = mMinNitsToLuxSpline(currentLux);
+        float luxBasedNitsLowerBound = PowerManager.BRIGHTNESS_MIN;
+        final float nitsLowerBound = Math.max(settingNitsLowerBound, luxBasedNitsLowerBound);
+
+        if (mNitsLowerBound != nitsLowerBound || mIsActive != isActive) {
+            mIsActive = isActive;
+            mNitsLowerBound = nitsLowerBound;
+            if (DEBUG) {
+                Slog.i(TAG, "mIsActive: " + mIsActive);
+            }
+            // TODO: mBrightnessCap = nitsToBrightnessSpline(mNitsLowerBound);
+            mChangeListener.onChanged();
+        }
+    }
+
+    void start() {
+        recalculateLowerBound();
+    }
+
+
+    @Override
+    Type getType() {
+        return Type.LUX;
+    }
+
+    @Override
+    void onDeviceConfigChanged() {
+        // TODO
+    }
+
+    @Override
+    void onDisplayChanged(Object displayData) {
+
+    }
+
+    @Override
+    void stop() {
+        mContentResolver.unregisterContentObserver(mSettingsObserver);
+    }
+
+    @Override
+    void dump(PrintWriter pw) {
+        pw.println("BrightnessMinClamper:");
+        pw.println("  mBrightnessCap=" + mBrightnessCap);
+        pw.println("  mIsActive=" + mIsActive);
+        pw.println("  mNitsLowerBound=" + mNitsLowerBound);
+        super.dump(pw);
+    }
+
+    private final class SettingsObserver extends ContentObserver {
+        SettingsObserver(Handler handler) {
+            super(handler);
+            mContentResolver.registerContentObserver(
+                    Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_MIN_NITS),
+                    false, this);
+            mContentResolver.registerContentObserver(
+                    Settings.Secure.getUriFor(Settings.Secure.EVEN_DIMMER_ACTIVATED),
+                    false, this);
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            recalculateLowerBound();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index c71f0cf2..2d5da71 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -78,6 +78,9 @@
             Flags.FLAG_ENABLE_POWER_THROTTLING_CLAMPER,
             Flags::enablePowerThrottlingClamper);
 
+    private final FlagState mEvenDimmerFlagState = new FlagState(
+            Flags.FLAG_EVEN_DIMMER,
+            Flags::evenDimmer);
     private final FlagState mSmallAreaDetectionFlagState = new FlagState(
             com.android.graphics.surfaceflinger.flags.Flags.FLAG_ENABLE_SMALL_AREA_DETECTION,
             com.android.graphics.surfaceflinger.flags.Flags::enableSmallAreaDetection);
@@ -174,6 +177,11 @@
         return mBackUpSmoothDisplayAndForcePeakRefreshRateFlagState.isEnabled();
     }
 
+    /** Returns whether brightness range is allowed to extend below traditional range. */
+    public boolean isEvenDimmerEnabled() {
+        return mEvenDimmerFlagState.isEnabled();
+    }
+
     public boolean isSmallAreaDetectionEnabled() {
         return mSmallAreaDetectionFlagState.isEnabled();
     }
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index 9dfa1ee..1b4d74c 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -106,6 +106,14 @@
 }
 
 flag {
+    name: "even_dimmer"
+    namespace: "display_manager"
+    description: "Feature flag for extending the brightness below traditional range"
+    bug: "179428400"
+    is_fixed_read_only: true
+}
+
+flag {
     name: "brightness_int_range_user_perception"
     namespace: "display_manager"
     description: "Feature flag for converting the brightness integer range to the user perception scale"
diff --git a/services/core/java/com/android/server/media/AudioAttributesUtils.java b/services/core/java/com/android/server/media/AudioAttributesUtils.java
new file mode 100644
index 0000000..8cb334d
--- /dev/null
+++ b/services/core/java/com/android/server/media/AudioAttributesUtils.java
@@ -0,0 +1,125 @@
+/*
+ * 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.media;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
+import android.media.MediaRoute2Info;
+
+import com.android.media.flags.Flags;
+
+/* package */ final class AudioAttributesUtils {
+
+    /* package */ static final AudioAttributes ATTRIBUTES_MEDIA = new AudioAttributes.Builder()
+            .setUsage(AudioAttributes.USAGE_MEDIA)
+            .build();
+
+    private AudioAttributesUtils() {
+        // no-op to prevent instantiation.
+    }
+
+    @MediaRoute2Info.Type
+    /* package */ static int mapToMediaRouteType(
+            @NonNull AudioDeviceAttributes audioDeviceAttributes) {
+        if (Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
+            switch (audioDeviceAttributes.getType()) {
+                case AudioDeviceInfo.TYPE_HDMI_ARC:
+                    return MediaRoute2Info.TYPE_HDMI_ARC;
+                case AudioDeviceInfo.TYPE_HDMI_EARC:
+                    return MediaRoute2Info.TYPE_HDMI_EARC;
+            }
+        }
+        switch (audioDeviceAttributes.getType()) {
+            case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
+            case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
+                return MediaRoute2Info.TYPE_BUILTIN_SPEAKER;
+            case AudioDeviceInfo.TYPE_WIRED_HEADSET:
+                return MediaRoute2Info.TYPE_WIRED_HEADSET;
+            case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
+                return MediaRoute2Info.TYPE_WIRED_HEADPHONES;
+            case AudioDeviceInfo.TYPE_DOCK:
+            case AudioDeviceInfo.TYPE_DOCK_ANALOG:
+                return MediaRoute2Info.TYPE_DOCK;
+            case AudioDeviceInfo.TYPE_HDMI:
+            case AudioDeviceInfo.TYPE_HDMI_ARC:
+            case AudioDeviceInfo.TYPE_HDMI_EARC:
+                return MediaRoute2Info.TYPE_HDMI;
+            case AudioDeviceInfo.TYPE_USB_DEVICE:
+                return MediaRoute2Info.TYPE_USB_DEVICE;
+            case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
+                return MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
+            case AudioDeviceInfo.TYPE_BLE_HEADSET:
+                return MediaRoute2Info.TYPE_BLE_HEADSET;
+            case AudioDeviceInfo.TYPE_HEARING_AID:
+                return MediaRoute2Info.TYPE_HEARING_AID;
+            default:
+                return MediaRoute2Info.TYPE_UNKNOWN;
+        }
+    }
+
+    /* package */ static boolean isDeviceOutputAttributes(
+            @Nullable AudioDeviceAttributes audioDeviceAttributes) {
+        if (audioDeviceAttributes == null) {
+            return false;
+        }
+
+        if (audioDeviceAttributes.getRole() != AudioDeviceAttributes.ROLE_OUTPUT) {
+            return false;
+        }
+
+        switch (audioDeviceAttributes.getType()) {
+            case AudioDeviceInfo.TYPE_BUILTIN_EARPIECE:
+            case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER:
+            case AudioDeviceInfo.TYPE_WIRED_HEADSET:
+            case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
+            case AudioDeviceInfo.TYPE_DOCK:
+            case AudioDeviceInfo.TYPE_DOCK_ANALOG:
+            case AudioDeviceInfo.TYPE_HDMI:
+            case AudioDeviceInfo.TYPE_HDMI_ARC:
+            case AudioDeviceInfo.TYPE_HDMI_EARC:
+            case AudioDeviceInfo.TYPE_USB_DEVICE:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /* package */ static boolean isBluetoothOutputAttributes(
+            @Nullable AudioDeviceAttributes audioDeviceAttributes) {
+        if (audioDeviceAttributes == null) {
+            return false;
+        }
+
+        if (audioDeviceAttributes.getRole() != AudioDeviceAttributes.ROLE_OUTPUT) {
+            return false;
+        }
+
+        switch (audioDeviceAttributes.getType()) {
+            case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
+            case AudioDeviceInfo.TYPE_BLE_HEADSET:
+            case AudioDeviceInfo.TYPE_BLE_SPEAKER:
+            case AudioDeviceInfo.TYPE_HEARING_AID:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+}
diff --git a/services/core/java/com/android/server/media/AudioPoliciesBluetoothRouteController.java b/services/core/java/com/android/server/media/AudioPoliciesBluetoothRouteController.java
index a00999d..8bc69c2 100644
--- a/services/core/java/com/android/server/media/AudioPoliciesBluetoothRouteController.java
+++ b/services/core/java/com/android/server/media/AudioPoliciesBluetoothRouteController.java
@@ -17,6 +17,7 @@
 package com.android.server.media;
 
 import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_AUDIO;
+import static android.bluetooth.BluetoothAdapter.STATE_CONNECTED;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -30,37 +31,38 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.media.AudioSystem;
 import android.media.MediaRoute2Info;
 import android.os.UserHandle;
 import android.text.TextUtils;
-import android.util.Log;
 import android.util.Slog;
 import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
 
 /**
- * Maintains a list of connected {@link BluetoothDevice bluetooth devices} and allows their
- * activation.
+ * Controls bluetooth routes and provides selected route override.
  *
- * <p>This class also serves as ground truth for assigning {@link MediaRoute2Info#getId() route ids}
- * for bluetooth routes via {@link #getRouteIdForBluetoothAddress}.
+ * <p>The controller offers similar functionality to {@link LegacyBluetoothRouteController} but does
+ * not support routes selection logic. Instead, relies on external clients to make a decision
+ * about currently selected route.
+ *
+ * <p>Selected route override should be used by {@link AudioManager} which is aware of Audio
+ * Policies.
  */
-// TODO: b/305199571 - Rename this class to remove the RouteController suffix, which causes
-// confusion with the BluetoothRouteController interface.
-/* package */ class AudioPoliciesBluetoothRouteController {
-    private static final String TAG = SystemMediaRoute2Provider.TAG;
+/* package */ class AudioPoliciesBluetoothRouteController
+        implements BluetoothRouteController {
+    private static final String TAG = "APBtRouteController";
 
     private static final String HEARING_AID_ROUTE_ID_PREFIX = "HEARING_AID_";
     private static final String LE_AUDIO_ROUTE_ID_PREFIX = "LE_AUDIO_";
@@ -73,8 +75,11 @@
     private final DeviceStateChangedReceiver mDeviceStateChangedReceiver =
             new DeviceStateChangedReceiver();
 
-    @NonNull private Map<String, BluetoothDevice> mAddressToBondedDevice = new HashMap<>();
-    @NonNull private final Map<String, BluetoothRouteInfo> mBluetoothRoutes = new HashMap<>();
+    @NonNull
+    private final Map<String, BluetoothRouteInfo> mBluetoothRoutes = new HashMap<>();
+
+    @NonNull
+    private final SparseIntArray mVolumeMap = new SparseIntArray();
 
     @NonNull
     private final Context mContext;
@@ -84,6 +89,11 @@
     private final BluetoothRouteController.BluetoothRoutesUpdatedListener mListener;
     @NonNull
     private final BluetoothProfileMonitor mBluetoothProfileMonitor;
+    @NonNull
+    private final AudioManager mAudioManager;
+
+    @Nullable
+    private BluetoothRouteInfo mSelectedBluetoothRoute;
 
     AudioPoliciesBluetoothRouteController(@NonNull Context context,
             @NonNull BluetoothAdapter bluetoothAdapter,
@@ -97,12 +107,21 @@
             @NonNull BluetoothAdapter bluetoothAdapter,
             @NonNull BluetoothProfileMonitor bluetoothProfileMonitor,
             @NonNull BluetoothRouteController.BluetoothRoutesUpdatedListener listener) {
-        mContext = Objects.requireNonNull(context);
-        mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter);
-        mBluetoothProfileMonitor = Objects.requireNonNull(bluetoothProfileMonitor);
-        mListener = Objects.requireNonNull(listener);
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(bluetoothAdapter);
+        Objects.requireNonNull(bluetoothProfileMonitor);
+        Objects.requireNonNull(listener);
+
+        mContext = context;
+        mBluetoothAdapter = bluetoothAdapter;
+        mBluetoothProfileMonitor = bluetoothProfileMonitor;
+        mAudioManager = mContext.getSystemService(AudioManager.class);
+        mListener = listener;
+
+        updateBluetoothRoutes();
     }
 
+    @Override
     public void start(UserHandle user) {
         mBluetoothProfileMonitor.start();
 
@@ -114,63 +133,122 @@
 
         IntentFilter deviceStateChangedIntentFilter = new IntentFilter();
 
+        deviceStateChangedIntentFilter.addAction(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
         deviceStateChangedIntentFilter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
         deviceStateChangedIntentFilter.addAction(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED);
         deviceStateChangedIntentFilter.addAction(
                 BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
         deviceStateChangedIntentFilter.addAction(
                 BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
+        deviceStateChangedIntentFilter.addAction(
+                BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED);
 
         mContext.registerReceiverAsUser(mDeviceStateChangedReceiver, user,
                 deviceStateChangedIntentFilter, null, null);
-        updateBluetoothRoutes();
     }
 
+    @Override
     public void stop() {
         mContext.unregisterReceiver(mAdapterStateChangedReceiver);
         mContext.unregisterReceiver(mDeviceStateChangedReceiver);
     }
 
-    @Nullable
-    public synchronized String getRouteIdForBluetoothAddress(@Nullable String address) {
-        BluetoothDevice bluetoothDevice = mAddressToBondedDevice.get(address);
-        // TODO: b/305199571 - Optimize the following statement to avoid creating the full
-        // MediaRoute2Info instance. We just need the id.
-        return bluetoothDevice != null
-                ? createBluetoothRoute(bluetoothDevice).mRoute.getId()
-                : null;
+    @Override
+    public boolean selectRoute(@Nullable String deviceAddress) {
+        synchronized (this) {
+            // Fetch all available devices in order to avoid race conditions with Bluetooth stack.
+            updateBluetoothRoutes();
+
+            if (deviceAddress == null) {
+                mSelectedBluetoothRoute = null;
+                return true;
+            }
+
+            BluetoothRouteInfo bluetoothRouteInfo = mBluetoothRoutes.get(deviceAddress);
+
+            if (bluetoothRouteInfo == null) {
+                Slog.w(TAG, "Cannot find bluetooth route for " + deviceAddress);
+                return false;
+            }
+
+            mSelectedBluetoothRoute = bluetoothRouteInfo;
+            setRouteConnectionState(mSelectedBluetoothRoute, STATE_CONNECTED);
+
+            updateConnectivityStateForDevicesInTheSameGroup();
+
+            return true;
+        }
     }
 
-    public synchronized void activateBluetoothDeviceWithAddress(String address) {
-        BluetoothRouteInfo btRouteInfo = mBluetoothRoutes.get(address);
+    /**
+     * Updates connectivity state for devices in the same devices group.
+     *
+     * <p>{@link BluetoothProfile#LE_AUDIO} and {@link BluetoothProfile#HEARING_AID} support
+     * grouping devices. Devices that belong to the same group should have the same routeId but
+     * different physical address.
+     *
+     * <p>In case one of the devices from the group is selected then other devices should also
+     * reflect this by changing their connectivity status to
+     * {@link MediaRoute2Info#CONNECTION_STATE_CONNECTED}.
+     */
+    private void updateConnectivityStateForDevicesInTheSameGroup() {
+        synchronized (this) {
+            for (BluetoothRouteInfo btRoute : mBluetoothRoutes.values()) {
+                if (TextUtils.equals(btRoute.mRoute.getId(), mSelectedBluetoothRoute.mRoute.getId())
+                        && !TextUtils.equals(btRoute.mBtDevice.getAddress(),
+                        mSelectedBluetoothRoute.mBtDevice.getAddress())) {
+                    setRouteConnectionState(btRoute, STATE_CONNECTED);
+                }
+            }
+        }
+    }
 
-        if (btRouteInfo == null) {
-            Slog.w(TAG, "activateBluetoothDeviceWithAddress: Ignoring unknown address " + address);
+    @Override
+    public void transferTo(@Nullable String routeId) {
+        if (routeId == null) {
+            mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_AUDIO);
             return;
         }
+
+        BluetoothRouteInfo btRouteInfo = findBluetoothRouteWithRouteId(routeId);
+
+        if (btRouteInfo == null) {
+            Slog.w(TAG, "transferTo: Unknown route. ID=" + routeId);
+            return;
+        }
+
         mBluetoothAdapter.setActiveDevice(btRouteInfo.mBtDevice, ACTIVE_DEVICE_AUDIO);
     }
 
+    @Nullable
+    private BluetoothRouteInfo findBluetoothRouteWithRouteId(@Nullable String routeId) {
+        if (routeId == null) {
+            return null;
+        }
+        synchronized (this) {
+            for (BluetoothRouteInfo btRouteInfo : mBluetoothRoutes.values()) {
+                if (TextUtils.equals(btRouteInfo.mRoute.getId(), routeId)) {
+                    return btRouteInfo;
+                }
+            }
+        }
+        return null;
+    }
+
     private void updateBluetoothRoutes() {
         Set<BluetoothDevice> bondedDevices = mBluetoothAdapter.getBondedDevices();
 
+        if (bondedDevices == null) {
+            return;
+        }
+
         synchronized (this) {
             mBluetoothRoutes.clear();
-            if (bondedDevices == null) {
-                // Bonded devices is null upon running into a BluetoothAdapter error.
-                Log.w(TAG, "BluetoothAdapter.getBondedDevices returned null.");
-                return;
-            }
-            // We don't clear bonded devices if we receive a null getBondedDevices result, because
-            // that probably means that the bluetooth stack ran into an issue. Not that all devices
-            // have been unpaired.
-            mAddressToBondedDevice =
-                    bondedDevices.stream()
-                            .collect(
-                                    Collectors.toMap(
-                                            BluetoothDevice::getAddress, Function.identity()));
+
+            // We need to query all available to BT stack devices in order to avoid inconsistency
+            // between external services, like, AndroidManager, and BT stack.
             for (BluetoothDevice device : bondedDevices) {
-                if (device.isConnected()) {
+                if (isDeviceConnected(device)) {
                     BluetoothRouteInfo newBtRoute = createBluetoothRoute(device);
                     if (newBtRoute.mConnectedProfiles.size() > 0) {
                         mBluetoothRoutes.put(device.getAddress(), newBtRoute);
@@ -180,51 +258,106 @@
         }
     }
 
-    @NonNull
-    public List<MediaRoute2Info> getAvailableBluetoothRoutes() {
-        List<MediaRoute2Info> routes = new ArrayList<>();
-        Set<String> routeIds = new HashSet<>();
+    @VisibleForTesting
+        /* package */ boolean isDeviceConnected(@NonNull BluetoothDevice device) {
+        return device.isConnected();
+    }
 
+    @Nullable
+    @Override
+    public MediaRoute2Info getSelectedRoute() {
         synchronized (this) {
-            for (BluetoothRouteInfo btRoute : mBluetoothRoutes.values()) {
-                // See createBluetoothRoute for info on why we do this.
-                if (routeIds.add(btRoute.mRoute.getId())) {
-                    routes.add(btRoute.mRoute);
-                }
+            if (mSelectedBluetoothRoute == null) {
+                return null;
+            }
+
+            return mSelectedBluetoothRoute.mRoute;
+        }
+    }
+
+    @NonNull
+    @Override
+    public List<MediaRoute2Info> getTransferableRoutes() {
+        List<MediaRoute2Info> routes = getAllBluetoothRoutes();
+        synchronized (this) {
+            if (mSelectedBluetoothRoute != null) {
+                routes.remove(mSelectedBluetoothRoute.mRoute);
             }
         }
         return routes;
     }
 
+    @NonNull
+    @Override
+    public List<MediaRoute2Info> getAllBluetoothRoutes() {
+        List<MediaRoute2Info> routes = new ArrayList<>();
+        List<String> routeIds = new ArrayList<>();
+
+        MediaRoute2Info selectedRoute = getSelectedRoute();
+        if (selectedRoute != null) {
+            routes.add(selectedRoute);
+            routeIds.add(selectedRoute.getId());
+        }
+
+        synchronized (this) {
+            for (BluetoothRouteInfo btRoute : mBluetoothRoutes.values()) {
+                // A pair of hearing aid devices or having the same hardware address
+                if (routeIds.contains(btRoute.mRoute.getId())) {
+                    continue;
+                }
+                routes.add(btRoute.mRoute);
+                routeIds.add(btRoute.mRoute.getId());
+            }
+        }
+        return routes;
+    }
+
+    @Override
+    public boolean updateVolumeForDevices(int devices, int volume) {
+        int routeType;
+        if ((devices & (AudioSystem.DEVICE_OUT_HEARING_AID)) != 0) {
+            routeType = MediaRoute2Info.TYPE_HEARING_AID;
+        } else if ((devices & (AudioManager.DEVICE_OUT_BLUETOOTH_A2DP
+                | AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES
+                | AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER)) != 0) {
+            routeType = MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
+        } else if ((devices & (AudioManager.DEVICE_OUT_BLE_HEADSET)) != 0) {
+            routeType = MediaRoute2Info.TYPE_BLE_HEADSET;
+        } else {
+            return false;
+        }
+
+        synchronized (this) {
+            mVolumeMap.put(routeType, volume);
+            if (mSelectedBluetoothRoute == null
+                    || mSelectedBluetoothRoute.mRoute.getType() != routeType) {
+                return false;
+            }
+
+            mSelectedBluetoothRoute.mRoute =
+                    new MediaRoute2Info.Builder(mSelectedBluetoothRoute.mRoute)
+                            .setVolume(volume)
+                            .build();
+        }
+
+        notifyBluetoothRoutesUpdated();
+        return true;
+    }
+
     private void notifyBluetoothRoutesUpdated() {
         mListener.onBluetoothRoutesUpdated();
     }
 
-    /**
-     * Creates a new {@link BluetoothRouteInfo}, including its member {@link
-     * BluetoothRouteInfo#mRoute}.
-     *
-     * <p>The most important logic in this method is around the {@link MediaRoute2Info#getId() route
-     * id} assignment. In some cases we want to group multiple {@link BluetoothDevice bluetooth
-     * devices} as a single media route. For example, the left and right hearing aids get exposed as
-     * two different BluetoothDevice instances, but we want to show them as a single route. In this
-     * case, we assign the same route id to all "group" bluetooth devices (like left and right
-     * hearing aids), so that a single route is exposed for both of them.
-     *
-     * <p>Deduplication by id happens downstream because we need to be able to refer to all
-     * bluetooth devices individually, since the audio stack refers to a bluetooth device group by
-     * any of its member devices.
-     */
     private BluetoothRouteInfo createBluetoothRoute(BluetoothDevice device) {
         BluetoothRouteInfo
                 newBtRoute = new BluetoothRouteInfo();
         newBtRoute.mBtDevice = device;
+
+        String routeId = device.getAddress();
         String deviceName = device.getName();
         if (TextUtils.isEmpty(deviceName)) {
             deviceName = mContext.getResources().getText(R.string.unknownName).toString();
         }
-
-        String routeId = device.getAddress();
         int type = MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
         newBtRoute.mConnectedProfiles = new SparseBooleanArray();
         if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.A2DP, device)) {
@@ -232,6 +365,7 @@
         }
         if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.HEARING_AID, device)) {
             newBtRoute.mConnectedProfiles.put(BluetoothProfile.HEARING_AID, true);
+            // Intentionally assign the same ID for a pair of devices to publish only one of them.
             routeId = HEARING_AID_ROUTE_ID_PREFIX
                     + mBluetoothProfileMonitor.getGroupId(BluetoothProfile.HEARING_AID, device);
             type = MediaRoute2Info.TYPE_HEARING_AID;
@@ -243,27 +377,66 @@
             type = MediaRoute2Info.TYPE_BLE_HEADSET;
         }
 
-        // Note that volume is only relevant for active bluetooth routes, and those are managed via
-        // AudioManager.
-        newBtRoute.mRoute =
-                new MediaRoute2Info.Builder(routeId, deviceName)
-                        .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO)
-                        .addFeature(MediaRoute2Info.FEATURE_LOCAL_PLAYBACK)
-                        .setConnectionState(MediaRoute2Info.CONNECTION_STATE_DISCONNECTED)
-                        .setDescription(
-                                mContext.getResources()
-                                        .getText(R.string.bluetooth_a2dp_audio_route_name)
-                                        .toString())
-                        .setType(type)
-                        .setAddress(device.getAddress())
-                        .build();
+        // Current volume will be set when connected.
+        newBtRoute.mRoute = new MediaRoute2Info.Builder(routeId, deviceName)
+                .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO)
+                .addFeature(MediaRoute2Info.FEATURE_LOCAL_PLAYBACK)
+                .setConnectionState(MediaRoute2Info.CONNECTION_STATE_DISCONNECTED)
+                .setDescription(mContext.getResources().getText(
+                        R.string.bluetooth_a2dp_audio_route_name).toString())
+                .setType(type)
+                .setVolumeHandling(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
+                .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
+                .setAddress(device.getAddress())
+                .build();
         return newBtRoute;
     }
 
+    private void setRouteConnectionState(@NonNull BluetoothRouteInfo btRoute,
+            @MediaRoute2Info.ConnectionState int state) {
+        if (btRoute == null) {
+            Slog.w(TAG, "setRouteConnectionState: route shouldn't be null");
+            return;
+        }
+        if (btRoute.mRoute.getConnectionState() == state) {
+            return;
+        }
+
+        MediaRoute2Info.Builder builder = new MediaRoute2Info.Builder(btRoute.mRoute)
+                .setConnectionState(state);
+        builder.setType(btRoute.getRouteType());
+
+
+
+        if (state == MediaRoute2Info.CONNECTION_STATE_CONNECTED) {
+            int currentVolume;
+            synchronized (this) {
+                currentVolume = mVolumeMap.get(btRoute.getRouteType(), 0);
+            }
+            builder.setVolume(currentVolume);
+        }
+
+        btRoute.mRoute = builder.build();
+    }
+
     private static class BluetoothRouteInfo {
         private BluetoothDevice mBtDevice;
         private MediaRoute2Info mRoute;
         private SparseBooleanArray mConnectedProfiles;
+
+        @MediaRoute2Info.Type
+        int getRouteType() {
+            // Let hearing aid profile have a priority.
+            if (mConnectedProfiles.get(BluetoothProfile.HEARING_AID, false)) {
+                return MediaRoute2Info.TYPE_HEARING_AID;
+            }
+
+            if (mConnectedProfiles.get(BluetoothProfile.LE_AUDIO, false)) {
+                return MediaRoute2Info.TYPE_BLE_HEADSET;
+            }
+
+            return MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
+        }
     }
 
     private class AdapterStateChangedReceiver extends BroadcastReceiver {
@@ -295,6 +468,9 @@
         @Override
         public void onReceive(Context context, Intent intent) {
             switch (intent.getAction()) {
+                case BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED:
+                case BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED:
+                case BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED:
                 case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED:
                 case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED:
                 case BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED:
diff --git a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java
index 27df00f..6bdfae2 100644
--- a/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java
+++ b/services/core/java/com/android/server/media/AudioPoliciesDeviceRouteController.java
@@ -17,590 +17,228 @@
 package com.android.server.media;
 
 import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO;
+import static android.media.MediaRoute2Info.FEATURE_LIVE_VIDEO;
 import static android.media.MediaRoute2Info.FEATURE_LOCAL_PLAYBACK;
+import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER;
+import static android.media.MediaRoute2Info.TYPE_DOCK;
+import static android.media.MediaRoute2Info.TYPE_HDMI;
+import static android.media.MediaRoute2Info.TYPE_HDMI_ARC;
+import static android.media.MediaRoute2Info.TYPE_HDMI_EARC;
+import static android.media.MediaRoute2Info.TYPE_USB_DEVICE;
+import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES;
+import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
 
-import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.RequiresPermission;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
 import android.content.Context;
-import android.media.AudioAttributes;
-import android.media.AudioDeviceAttributes;
-import android.media.AudioDeviceCallback;
-import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
+import android.media.AudioRoutesInfo;
+import android.media.IAudioRoutesObserver;
+import android.media.IAudioService;
 import android.media.MediaRoute2Info;
-import android.media.audiopolicy.AudioProductStrategy;
-import android.os.Handler;
-import android.os.HandlerExecutor;
-import android.os.Looper;
-import android.os.UserHandle;
-import android.text.TextUtils;
+import android.os.RemoteException;
 import android.util.Slog;
-import android.util.SparseArray;
 
 import com.android.internal.R;
-import com.android.server.media.BluetoothRouteController.NoOpBluetoothRouteController;
+import com.android.internal.annotations.VisibleForTesting;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 
-/**
- * Maintains a list of all available routes and supports transfers to any of them.
- *
- * <p>This implementation is intended for use in conjunction with {@link
- * NoOpBluetoothRouteController}, as it manages bluetooth devices directly.
- *
- * <p>This implementation obtains and manages all routes via {@link AudioManager}, with the
- * exception of {@link AudioManager#handleBluetoothActiveDeviceChanged inactive bluetooth} routes
- * which are managed by {@link AudioPoliciesBluetoothRouteController}, which depends on the
- * bluetooth stack (for example {@link BluetoothAdapter}.
- */
-// TODO: b/305199571 - Rename this class to avoid the AudioPolicies prefix, which has been flagged
-// by the audio team as a confusing name.
 /* package */ final class AudioPoliciesDeviceRouteController implements DeviceRouteController {
-    private static final String TAG = SystemMediaRoute2Provider.TAG;
+
+    private static final String TAG = "APDeviceRoutesController";
 
     @NonNull
-    private static final AudioAttributes MEDIA_USAGE_AUDIO_ATTRIBUTES =
-            new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
+    private final Context mContext;
+    @NonNull
+    private final AudioManager mAudioManager;
+    @NonNull
+    private final IAudioService mAudioService;
 
     @NonNull
-    private static final SparseArray<SystemRouteInfo> AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO =
-            new SparseArray<>();
+    private final OnDeviceRouteChangedListener mOnDeviceRouteChangedListener;
+    @NonNull
+    private final AudioRoutesObserver mAudioRoutesObserver = new AudioRoutesObserver();
 
-    @NonNull private final Context mContext;
-    @NonNull private final AudioManager mAudioManager;
-    @NonNull private final Handler mHandler;
-    @NonNull private final OnDeviceRouteChangedListener mOnDeviceRouteChangedListener;
-    @NonNull private final AudioPoliciesBluetoothRouteController mBluetoothRouteController;
+    private int mDeviceVolume;
 
     @NonNull
-    private final Map<String, MediaRoute2InfoHolder> mRouteIdToAvailableDeviceRoutes =
-            new HashMap<>();
+    private MediaRoute2Info mDeviceRoute;
+    @Nullable
+    private MediaRoute2Info mSelectedRoute;
 
-    @NonNull private final AudioProductStrategy mStrategyForMedia;
-
-    @NonNull private final AudioDeviceCallback mAudioDeviceCallback = new AudioDeviceCallbackImpl();
-
-    @NonNull
-    private final AudioManager.OnDevicesForAttributesChangedListener
-            mOnDevicesForAttributesChangedListener = this::onDevicesForAttributesChangedListener;
-
-    @NonNull private MediaRoute2Info mSelectedRoute;
-
-    // TODO: b/305199571 - Support nullable btAdapter and strategyForMedia which, when null, means
-    // no support for transferring to inactive bluetooth routes and transferring to any routes
-    // respectively.
-    @RequiresPermission(
-            anyOf = {
-                Manifest.permission.MODIFY_AUDIO_ROUTING,
-                Manifest.permission.QUERY_AUDIO_STATE
-            })
-    /* package */ AudioPoliciesDeviceRouteController(
-            @NonNull Context context,
+    @VisibleForTesting
+    /* package */ AudioPoliciesDeviceRouteController(@NonNull Context context,
             @NonNull AudioManager audioManager,
-            @NonNull Looper looper,
-            @NonNull AudioProductStrategy strategyForMedia,
-            @NonNull BluetoothAdapter btAdapter,
+            @NonNull IAudioService audioService,
             @NonNull OnDeviceRouteChangedListener onDeviceRouteChangedListener) {
-        mContext = Objects.requireNonNull(context);
-        mAudioManager = Objects.requireNonNull(audioManager);
-        mHandler = new Handler(Objects.requireNonNull(looper));
-        mStrategyForMedia = Objects.requireNonNull(strategyForMedia);
-        mOnDeviceRouteChangedListener = Objects.requireNonNull(onDeviceRouteChangedListener);
-        mBluetoothRouteController =
-                new AudioPoliciesBluetoothRouteController(
-                        mContext, btAdapter, this::rebuildAvailableRoutes);
-        rebuildAvailableRoutes();
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(audioManager);
+        Objects.requireNonNull(audioService);
+        Objects.requireNonNull(onDeviceRouteChangedListener);
+
+        mContext = context;
+        mOnDeviceRouteChangedListener = onDeviceRouteChangedListener;
+
+        mAudioManager = audioManager;
+        mAudioService = audioService;
+
+        AudioRoutesInfo newAudioRoutes = null;
+        try {
+            newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Cannot connect to audio service to start listen to routes", e);
+        }
+
+        mDeviceRoute = createRouteFromAudioInfo(newAudioRoutes);
     }
 
-    @RequiresPermission(
-            anyOf = {
-                Manifest.permission.MODIFY_AUDIO_ROUTING,
-                Manifest.permission.QUERY_AUDIO_STATE
-            })
     @Override
-    public void start(UserHandle mUser) {
-        mBluetoothRouteController.start(mUser);
-        mAudioManager.registerAudioDeviceCallback(mAudioDeviceCallback, mHandler);
-        mAudioManager.addOnDevicesForAttributesChangedListener(
-                AudioRoutingUtils.ATTRIBUTES_MEDIA,
-                new HandlerExecutor(mHandler),
-                mOnDevicesForAttributesChangedListener);
-    }
+    public synchronized boolean selectRoute(@Nullable Integer type) {
+        if (type == null) {
+            mSelectedRoute = null;
+            return true;
+        }
 
-    @RequiresPermission(
-            anyOf = {
-                Manifest.permission.MODIFY_AUDIO_ROUTING,
-                Manifest.permission.QUERY_AUDIO_STATE
-            })
-    @Override
-    public void stop() {
-        mAudioManager.removeOnDevicesForAttributesChangedListener(
-                mOnDevicesForAttributesChangedListener);
-        mAudioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback);
-        mBluetoothRouteController.stop();
-        mHandler.removeCallbacksAndMessages(/* token= */ null);
+        if (!isDeviceRouteType(type)) {
+            return false;
+        }
+
+        mSelectedRoute = createRouteFromAudioInfo(type);
+        return true;
     }
 
     @Override
     @NonNull
     public synchronized MediaRoute2Info getSelectedRoute() {
-        return mSelectedRoute;
+        if (mSelectedRoute != null) {
+            return mSelectedRoute;
+        }
+        return mDeviceRoute;
     }
 
     @Override
-    @NonNull
-    public synchronized List<MediaRoute2Info> getAvailableRoutes() {
-        return mRouteIdToAvailableDeviceRoutes.values().stream()
-                .map(it -> it.mMediaRoute2Info)
-                .toList();
-    }
-
-    @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
-    @Override
-    public synchronized void transferTo(@Nullable String routeId) {
-        if (routeId == null) {
-            // This should never happen: This branch should only execute when the matching bluetooth
-            // route controller is not the no-op one.
-            // TODO: b/305199571 - Make routeId non-null and remove this branch once we remove the
-            // legacy route controller implementations.
-            Slog.e(TAG, "Unexpected call to AudioPoliciesDeviceRouteController#transferTo(null)");
-            return;
-        }
-        MediaRoute2InfoHolder mediaRoute2InfoHolder = mRouteIdToAvailableDeviceRoutes.get(routeId);
-        if (mediaRoute2InfoHolder == null) {
-            Slog.w(TAG, "transferTo: Ignoring transfer request to unknown route id : " + routeId);
-            return;
-        }
-        if (mediaRoute2InfoHolder.mCorrespondsToInactiveBluetoothRoute) {
-            // By default, the last connected device is the active route so we don't need to apply a
-            // routing audio policy.
-            mBluetoothRouteController.activateBluetoothDeviceWithAddress(
-                    mediaRoute2InfoHolder.mMediaRoute2Info.getAddress());
-            mAudioManager.removePreferredDeviceForStrategy(mStrategyForMedia);
-        } else {
-            AudioDeviceAttributes attr =
-                    new AudioDeviceAttributes(
-                            AudioDeviceAttributes.ROLE_OUTPUT,
-                            mediaRoute2InfoHolder.mAudioDeviceInfoType,
-                            /* address= */ ""); // This is not a BT device, hence no address needed.
-            mAudioManager.setPreferredDeviceForStrategy(mStrategyForMedia, attr);
-        }
-    }
-
-    @RequiresPermission(
-            anyOf = {
-                Manifest.permission.MODIFY_AUDIO_ROUTING,
-                Manifest.permission.QUERY_AUDIO_STATE
-            })
-    @Override
     public synchronized boolean updateVolume(int volume) {
-        // TODO: b/305199571 - Optimize so that we only update the volume of the selected route. We
-        // don't need to rebuild all available routes.
-        rebuildAvailableRoutes();
+        if (mDeviceVolume == volume) {
+            return false;
+        }
+
+        mDeviceVolume = volume;
+
+        if (mSelectedRoute != null) {
+            mSelectedRoute = new MediaRoute2Info.Builder(mSelectedRoute)
+                    .setVolume(volume)
+                    .build();
+        }
+
+        mDeviceRoute = new MediaRoute2Info.Builder(mDeviceRoute)
+                .setVolume(volume)
+                .build();
+
         return true;
     }
 
-    @RequiresPermission(
-            anyOf = {
-                Manifest.permission.MODIFY_AUDIO_ROUTING,
-                Manifest.permission.QUERY_AUDIO_STATE
-            })
-    private void onDevicesForAttributesChangedListener(
-            AudioAttributes attributes, List<AudioDeviceAttributes> unusedAudioDeviceAttributes) {
-        if (attributes.getUsage() == AudioAttributes.USAGE_MEDIA) {
-            // We only care about the media usage. Ignore everything else.
-            rebuildAvailableRoutes();
-        }
-    }
+    @NonNull
+    private MediaRoute2Info createRouteFromAudioInfo(@Nullable AudioRoutesInfo newRoutes) {
+        int type = TYPE_BUILTIN_SPEAKER;
 
-    @RequiresPermission(
-            anyOf = {
-                Manifest.permission.MODIFY_AUDIO_ROUTING,
-                Manifest.permission.QUERY_AUDIO_STATE
-            })
-    private synchronized void rebuildAvailableRoutes() {
-        List<AudioDeviceAttributes> attributesOfSelectedOutputDevices =
-                mAudioManager.getDevicesForAttributes(MEDIA_USAGE_AUDIO_ATTRIBUTES);
-        int selectedDeviceAttributesType;
-        if (attributesOfSelectedOutputDevices.isEmpty()) {
-            Slog.e(
-                    TAG,
-                    "Unexpected empty list of output devices for media. Using built-in speakers.");
-            selectedDeviceAttributesType = AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
-        } else {
-            if (attributesOfSelectedOutputDevices.size() > 1) {
-                Slog.w(
-                        TAG,
-                        "AudioManager.getDevicesForAttributes returned more than one element. Using"
-                                + " the first one.");
-            }
-            selectedDeviceAttributesType = attributesOfSelectedOutputDevices.get(0).getType();
-        }
-
-        AudioDeviceInfo[] audioDeviceInfos =
-                mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
-        mRouteIdToAvailableDeviceRoutes.clear();
-        MediaRoute2InfoHolder newSelectedRouteHolder = null;
-        for (AudioDeviceInfo audioDeviceInfo : audioDeviceInfos) {
-            MediaRoute2Info mediaRoute2Info =
-                    createMediaRoute2InfoFromAudioDeviceInfo(audioDeviceInfo);
-            // Null means audioDeviceInfo is not a supported media output, like a phone's builtin
-            // earpiece. We ignore those.
-            if (mediaRoute2Info != null) {
-                int audioDeviceInfoType = audioDeviceInfo.getType();
-                MediaRoute2InfoHolder newHolder =
-                        MediaRoute2InfoHolder.createForAudioManagerRoute(
-                                mediaRoute2Info, audioDeviceInfoType);
-                mRouteIdToAvailableDeviceRoutes.put(mediaRoute2Info.getId(), newHolder);
-                if (selectedDeviceAttributesType == audioDeviceInfoType) {
-                    newSelectedRouteHolder = newHolder;
-                }
+        if (newRoutes != null) {
+            if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0) {
+                type = TYPE_WIRED_HEADPHONES;
+            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) {
+                type = TYPE_WIRED_HEADSET;
+            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
+                type = TYPE_DOCK;
+            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) {
+                type = TYPE_HDMI;
+            } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) {
+                type = TYPE_USB_DEVICE;
             }
         }
 
-        if (mRouteIdToAvailableDeviceRoutes.isEmpty()) {
-            // Due to an unknown reason (possibly an audio server crash), we ended up with an empty
-            // list of routes. Our entire codebase assumes at least one system route always exists,
-            // so we create a placeholder route represented as a built-in speaker for
-            // user-presentation purposes.
-            Slog.e(TAG, "Ended up with an empty list of routes. Creating a placeholder route.");
-            MediaRoute2InfoHolder placeholderRouteHolder = createPlaceholderBuiltinSpeakerRoute();
-            String placeholderRouteId = placeholderRouteHolder.mMediaRoute2Info.getId();
-            mRouteIdToAvailableDeviceRoutes.put(placeholderRouteId, placeholderRouteHolder);
-        }
-
-        if (newSelectedRouteHolder == null) {
-            Slog.e(
-                    TAG,
-                    "Could not map this selected device attribute type to an available route: "
-                            + selectedDeviceAttributesType);
-            // We know mRouteIdToAvailableDeviceRoutes is not empty.
-            newSelectedRouteHolder = mRouteIdToAvailableDeviceRoutes.values().iterator().next();
-        }
-        MediaRoute2InfoHolder selectedRouteHolderWithUpdatedVolumeInfo =
-                newSelectedRouteHolder.copyWithVolumeInfoFromAudioManager(mAudioManager);
-        mRouteIdToAvailableDeviceRoutes.put(
-                newSelectedRouteHolder.mMediaRoute2Info.getId(),
-                selectedRouteHolderWithUpdatedVolumeInfo);
-        mSelectedRoute = selectedRouteHolderWithUpdatedVolumeInfo.mMediaRoute2Info;
-
-        // We only add those BT routes that we have not already obtained from audio manager (which
-        // are active).
-        mBluetoothRouteController.getAvailableBluetoothRoutes().stream()
-                .filter(it -> !mRouteIdToAvailableDeviceRoutes.containsKey(it.getId()))
-                .map(MediaRoute2InfoHolder::createForInactiveBluetoothRoute)
-                .forEach(
-                        it -> mRouteIdToAvailableDeviceRoutes.put(it.mMediaRoute2Info.getId(), it));
-        mOnDeviceRouteChangedListener.onDeviceRouteChanged();
+        return createRouteFromAudioInfo(type);
     }
 
-    private MediaRoute2InfoHolder createPlaceholderBuiltinSpeakerRoute() {
-        int type = AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
-        return MediaRoute2InfoHolder.createForAudioManagerRoute(
-                createMediaRoute2Info(
-                        /* routeId= */ null, type, /* productName= */ null, /* address= */ null),
-                type);
-    }
+    @NonNull
+    private MediaRoute2Info createRouteFromAudioInfo(@MediaRoute2Info.Type int type) {
+        int name = R.string.default_audio_route_name;
+        switch (type) {
+            case TYPE_WIRED_HEADPHONES:
+            case TYPE_WIRED_HEADSET:
+                name = R.string.default_audio_route_name_headphones;
+                break;
+            case TYPE_DOCK:
+                name = R.string.default_audio_route_name_dock_speakers;
+                break;
+            case TYPE_HDMI:
+            case TYPE_HDMI_ARC:
+            case TYPE_HDMI_EARC:
+                name = R.string.default_audio_route_name_external_device;
+                break;
+            case TYPE_USB_DEVICE:
+                name = R.string.default_audio_route_name_usb;
+                break;
+        }
 
-    @Nullable
-    private MediaRoute2Info createMediaRoute2InfoFromAudioDeviceInfo(
-            AudioDeviceInfo audioDeviceInfo) {
-        String address = audioDeviceInfo.getAddress();
-        // Passing a null route id means we want to get the default id for the route. Generally, we
-        // only expect to pass null for non-Bluetooth routes.
-        String routeId =
-                TextUtils.isEmpty(address)
-                        ? null
-                        : mBluetoothRouteController.getRouteIdForBluetoothAddress(address);
-        return createMediaRoute2Info(
-                routeId, audioDeviceInfo.getType(), audioDeviceInfo.getProductName(), address);
+        synchronized (this) {
+            return new MediaRoute2Info.Builder(
+                            MediaRoute2Info.ROUTE_ID_DEVICE,
+                            mContext.getResources().getText(name).toString())
+                    .setVolumeHandling(
+                            mAudioManager.isVolumeFixed()
+                                    ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED
+                                    : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
+                    .setVolume(mDeviceVolume)
+                    .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
+                    .setType(type)
+                    .addFeature(FEATURE_LIVE_AUDIO)
+                    .addFeature(FEATURE_LIVE_VIDEO)
+                    .addFeature(FEATURE_LOCAL_PLAYBACK)
+                    .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED)
+                    .build();
+        }
     }
 
     /**
-     * Creates a new {@link MediaRoute2Info} using the provided information.
+     * Checks if the given type is a device route.
      *
-     * @param routeId A route id, or null to use an id pre-defined for the given {@code type}.
-     * @param audioDeviceInfoType The type as obtained from {@link AudioDeviceInfo#getType}.
-     * @param productName The product name as obtained from {@link
-     *     AudioDeviceInfo#getProductName()}, or null to use a predefined name for the given {@code
-     *     type}.
-     * @param address The type as obtained from {@link AudioDeviceInfo#getAddress()} or {@link
-     *     BluetoothDevice#getAddress()}.
-     * @return The new {@link MediaRoute2Info}.
+     * <p>Device route means a route which is either built-in or wired to the current device.
+     *
+     * @param type specifies the type of the device.
+     * @return {@code true} if the device is wired or built-in and {@code false} otherwise.
      */
-    @Nullable
-    private MediaRoute2Info createMediaRoute2Info(
-            @Nullable String routeId,
-            int audioDeviceInfoType,
-            @Nullable CharSequence productName,
-            @Nullable String address) {
-        SystemRouteInfo systemRouteInfo =
-                AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.get(audioDeviceInfoType);
-        if (systemRouteInfo == null) {
-            // Device type that's intentionally unsupported for media output, like the built-in
-            // earpiece.
-            return null;
-        }
-        CharSequence humanReadableName = productName;
-        if (TextUtils.isEmpty(humanReadableName)) {
-            humanReadableName = mContext.getResources().getText(systemRouteInfo.mNameResource);
-        }
-        if (routeId == null) {
-            // The caller hasn't provided an id, so we use a pre-defined one. This happens when we
-            // are creating a non-BT route, or we are creating a BT route but a race condition
-            // caused AudioManager to expose the BT route before BluetoothAdapter, preventing us
-            // from getting an id using BluetoothRouteController#getRouteIdForBluetoothAddress.
-            routeId = systemRouteInfo.mDefaultRouteId;
-        }
-        return new MediaRoute2Info.Builder(routeId, humanReadableName)
-                .setType(systemRouteInfo.mMediaRoute2InfoType)
-                .setAddress(address)
-                .setSystemRoute(true)
-                .addFeature(FEATURE_LIVE_AUDIO)
-                .addFeature(FEATURE_LOCAL_PLAYBACK)
-                .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED)
-                .build();
-    }
-
-    /**
-     * Holds a {@link MediaRoute2Info} and associated information that we don't want to put in the
-     * {@link MediaRoute2Info} class because it's solely necessary for the implementation of this
-     * class.
-     */
-    private static class MediaRoute2InfoHolder {
-
-        public final MediaRoute2Info mMediaRoute2Info;
-        public final int mAudioDeviceInfoType;
-        public final boolean mCorrespondsToInactiveBluetoothRoute;
-
-        public static MediaRoute2InfoHolder createForAudioManagerRoute(
-                MediaRoute2Info mediaRoute2Info, int audioDeviceInfoType) {
-            return new MediaRoute2InfoHolder(
-                    mediaRoute2Info,
-                    audioDeviceInfoType,
-                    /* correspondsToInactiveBluetoothRoute= */ false);
-        }
-
-        public static MediaRoute2InfoHolder createForInactiveBluetoothRoute(
-                MediaRoute2Info mediaRoute2Info) {
-            // There's no corresponding audio device info, hence the audio device info type is
-            // unknown.
-            return new MediaRoute2InfoHolder(
-                    mediaRoute2Info,
-                    /* audioDeviceInfoType= */ AudioDeviceInfo.TYPE_UNKNOWN,
-                    /* correspondsToInactiveBluetoothRoute= */ true);
-        }
-
-        private MediaRoute2InfoHolder(
-                MediaRoute2Info mediaRoute2Info,
-                int audioDeviceInfoType,
-                boolean correspondsToInactiveBluetoothRoute) {
-            mMediaRoute2Info = mediaRoute2Info;
-            mAudioDeviceInfoType = audioDeviceInfoType;
-            mCorrespondsToInactiveBluetoothRoute = correspondsToInactiveBluetoothRoute;
-        }
-
-        public MediaRoute2InfoHolder copyWithVolumeInfoFromAudioManager(
-                AudioManager mAudioManager) {
-            MediaRoute2Info routeInfoWithVolumeInfo =
-                    new MediaRoute2Info.Builder(mMediaRoute2Info)
-                            .setVolumeHandling(
-                                    mAudioManager.isVolumeFixed()
-                                            ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED
-                                            : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE)
-                            .setVolume(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC))
-                            .setVolumeMax(
-                                    mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
-                            .build();
-            return new MediaRoute2InfoHolder(
-                    routeInfoWithVolumeInfo,
-                    mAudioDeviceInfoType,
-                    mCorrespondsToInactiveBluetoothRoute);
+    private boolean isDeviceRouteType(@MediaRoute2Info.Type int type) {
+        switch (type) {
+            case TYPE_BUILTIN_SPEAKER:
+            case TYPE_WIRED_HEADPHONES:
+            case TYPE_WIRED_HEADSET:
+            case TYPE_DOCK:
+            case TYPE_HDMI:
+            case TYPE_HDMI_ARC:
+            case TYPE_HDMI_EARC:
+            case TYPE_USB_DEVICE:
+                return true;
+            default:
+                return false;
         }
     }
 
-    /**
-     * Holds route information about an {@link AudioDeviceInfo#getType() audio device info type}.
-     */
-    private static class SystemRouteInfo {
-        /** The type to use for {@link MediaRoute2Info#getType()}. */
-        public final int mMediaRoute2InfoType;
+    private class AudioRoutesObserver extends IAudioRoutesObserver.Stub {
 
-        /**
-         * Holds the route id to use if no other id is provided.
-         *
-         * <p>We only expect this id to be used for non-bluetooth routes. For bluetooth routes, in a
-         * normal scenario, the id is generated from the device information (like address, or
-         * hiSyncId), and this value is ignored. A non-normal scenario may occur when there's race
-         * condition between {@link BluetoothAdapter} and {@link AudioManager}, who are not
-         * synchronized.
-         */
-        public final String mDefaultRouteId;
-
-        /**
-         * The name to use for {@link MediaRoute2Info#getName()}.
-         *
-         * <p>Usually replaced by the UI layer with a localized string.
-         */
-        public final int mNameResource;
-
-        private SystemRouteInfo(int mediaRoute2InfoType, String defaultRouteId, int nameResource) {
-            mMediaRoute2InfoType = mediaRoute2InfoType;
-            mDefaultRouteId = defaultRouteId;
-            mNameResource = nameResource;
-        }
-    }
-
-    private class AudioDeviceCallbackImpl extends AudioDeviceCallback {
-        @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
         @Override
-        public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
-            for (AudioDeviceInfo deviceInfo : addedDevices) {
-                if (AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.contains(deviceInfo.getType())) {
-                    // When a new valid media output is connected, we clear any routing policies so
-                    // that the default routing logic from the audio framework kicks in. As a result
-                    // of this, when the user connects a bluetooth device or a wired headset, the
-                    // new device becomes the active route, which is the traditional behavior.
-                    mAudioManager.removePreferredDeviceForStrategy(mStrategyForMedia);
-                    rebuildAvailableRoutes();
-                    break;
-                }
+        public void dispatchAudioRoutesChanged(AudioRoutesInfo newAudioRoutes) {
+            boolean isDeviceRouteChanged;
+            MediaRoute2Info deviceRoute = createRouteFromAudioInfo(newAudioRoutes);
+
+            synchronized (AudioPoliciesDeviceRouteController.this) {
+                mDeviceRoute = deviceRoute;
+                isDeviceRouteChanged = mSelectedRoute == null;
             }
-        }
 
-        @RequiresPermission(
-                anyOf = {
-                    Manifest.permission.MODIFY_AUDIO_ROUTING,
-                    Manifest.permission.QUERY_AUDIO_STATE
-                })
-        @Override
-        public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
-            for (AudioDeviceInfo deviceInfo : removedDevices) {
-                if (AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.contains(deviceInfo.getType())) {
-                    rebuildAvailableRoutes();
-                    break;
-                }
+            if (isDeviceRouteChanged) {
+                mOnDeviceRouteChangedListener.onDeviceRouteChanged();
             }
         }
     }
 
-    static {
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_BUILTIN_SPEAKER,
-                        /* defaultRouteId= */ "ROUTE_ID_BUILTIN_SPEAKER",
-                        /* nameResource= */ R.string.default_audio_route_name));
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_WIRED_HEADSET,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_WIRED_HEADSET,
-                        /* defaultRouteId= */ "ROUTE_ID_WIRED_HEADSET",
-                        /* nameResource= */ R.string.default_audio_route_name_headphones));
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_WIRED_HEADPHONES,
-                        /* defaultRouteId= */ "ROUTE_ID_WIRED_HEADPHONES",
-                        /* nameResource= */ R.string.default_audio_route_name_headphones));
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_BLUETOOTH_A2DP,
-                        /* defaultRouteId= */ "ROUTE_ID_BLUETOOTH_A2DP",
-                        /* nameResource= */ R.string.bluetooth_a2dp_audio_route_name));
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_HDMI,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_HDMI,
-                        /* defaultRouteId= */ "ROUTE_ID_HDMI",
-                        /* nameResource= */ R.string.default_audio_route_name_external_device));
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_DOCK,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_DOCK,
-                        /* defaultRouteId= */ "ROUTE_ID_DOCK",
-                        /* nameResource= */ R.string.default_audio_route_name_dock_speakers));
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_USB_DEVICE,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_USB_DEVICE,
-                        /* defaultRouteId= */ "ROUTE_ID_USB_DEVICE",
-                        /* nameResource= */ R.string.default_audio_route_name_usb));
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_USB_HEADSET,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_USB_HEADSET,
-                        /* defaultRouteId= */ "ROUTE_ID_USB_HEADSET",
-                        /* nameResource= */ R.string.default_audio_route_name_usb));
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_HDMI_ARC,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_HDMI_ARC,
-                        /* defaultRouteId= */ "ROUTE_ID_HDMI_ARC",
-                        /* nameResource= */ R.string.default_audio_route_name_external_device));
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_HDMI_EARC,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_HDMI_EARC,
-                        /* defaultRouteId= */ "ROUTE_ID_HDMI_EARC",
-                        /* nameResource= */ R.string.default_audio_route_name_external_device));
-        // TODO: b/305199571 - Add a proper type constants and human readable names for AUX_LINE,
-        // LINE_ANALOG, LINE_DIGITAL, BLE_BROADCAST, BLE_SPEAKER, BLE_HEADSET, and HEARING_AID.
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_HEARING_AID,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_HEARING_AID,
-                        /* defaultRouteId= */ "ROUTE_ID_HEARING_AID",
-                        /* nameResource= */ R.string.bluetooth_a2dp_audio_route_name));
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_BLE_HEADSET,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_BLE_HEADSET,
-                        /* defaultRouteId= */ "ROUTE_ID_BLE_HEADSET",
-                        /* nameResource= */ R.string.bluetooth_a2dp_audio_route_name));
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_BLE_SPEAKER,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_BLE_HEADSET, // TODO: b/305199571 - Make a new type.
-                        /* defaultRouteId= */ "ROUTE_ID_BLE_SPEAKER",
-                        /* nameResource= */ R.string.bluetooth_a2dp_audio_route_name));
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_BLE_BROADCAST,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_BLE_HEADSET,
-                        /* defaultRouteId= */ "ROUTE_ID_BLE_BROADCAST",
-                        /* nameResource= */ R.string.bluetooth_a2dp_audio_route_name));
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_LINE_DIGITAL,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_UNKNOWN,
-                        /* defaultRouteId= */ "ROUTE_ID_LINE_DIGITAL",
-                        /* nameResource= */ R.string.default_audio_route_name_external_device));
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_LINE_ANALOG,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_UNKNOWN,
-                        /* defaultRouteId= */ "ROUTE_ID_LINE_ANALOG",
-                        /* nameResource= */ R.string.default_audio_route_name_external_device));
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_AUX_LINE,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_UNKNOWN,
-                        /* defaultRouteId= */ "ROUTE_ID_AUX_LINE",
-                        /* nameResource= */ R.string.default_audio_route_name_external_device));
-        AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put(
-                AudioDeviceInfo.TYPE_DOCK_ANALOG,
-                new SystemRouteInfo(
-                        MediaRoute2Info.TYPE_DOCK,
-                        /* defaultRouteId= */ "ROUTE_ID_DOCK_ANALOG",
-                        /* nameResource= */ R.string.default_audio_route_name_dock_speakers));
-    }
 }
diff --git a/services/core/java/com/android/server/media/AudioRoutingUtils.java b/services/core/java/com/android/server/media/AudioRoutingUtils.java
deleted file mode 100644
index 13f11eb..0000000
--- a/services/core/java/com/android/server/media/AudioRoutingUtils.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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.media;
-
-import android.Manifest;
-import android.annotation.Nullable;
-import android.annotation.RequiresPermission;
-import android.media.AudioAttributes;
-import android.media.AudioManager;
-import android.media.audiopolicy.AudioProductStrategy;
-
-/** Holds utils related to routing in the audio framework. */
-/* package */ final class AudioRoutingUtils {
-
-    /* package */ static final AudioAttributes ATTRIBUTES_MEDIA =
-            new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
-
-    @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
-    @Nullable
-    /* package */ static AudioProductStrategy getMediaAudioProductStrategy() {
-        for (AudioProductStrategy strategy : AudioManager.getAudioProductStrategies()) {
-            if (strategy.supportsAudioAttributes(AudioRoutingUtils.ATTRIBUTES_MEDIA)) {
-                return strategy;
-            }
-        }
-        return null;
-    }
-
-    private AudioRoutingUtils() {
-        // no-op to prevent instantiation.
-    }
-}
diff --git a/services/core/java/com/android/server/media/BluetoothRouteController.java b/services/core/java/com/android/server/media/BluetoothRouteController.java
index 74fdf6e..2b01001 100644
--- a/services/core/java/com/android/server/media/BluetoothRouteController.java
+++ b/services/core/java/com/android/server/media/BluetoothRouteController.java
@@ -44,11 +44,19 @@
     @NonNull
     static BluetoothRouteController createInstance(@NonNull Context context,
             @NonNull BluetoothRouteController.BluetoothRoutesUpdatedListener listener) {
+        Objects.requireNonNull(context);
         Objects.requireNonNull(listener);
-        BluetoothAdapter btAdapter = context.getSystemService(BluetoothManager.class).getAdapter();
 
-        if (btAdapter == null || Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
+        BluetoothManager bluetoothManager = (BluetoothManager)
+                context.getSystemService(Context.BLUETOOTH_SERVICE);
+        BluetoothAdapter btAdapter = bluetoothManager.getAdapter();
+
+        if (btAdapter == null) {
             return new NoOpBluetoothRouteController();
+        }
+
+        if (Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
+            return new AudioPoliciesBluetoothRouteController(context, btAdapter, listener);
         } else {
             return new LegacyBluetoothRouteController(context, btAdapter, listener);
         }
@@ -66,6 +74,17 @@
      */
     void stop();
 
+
+    /**
+     * Selects the route with the given {@code deviceAddress}.
+     *
+     * @param deviceAddress The physical address of the device to select. May be null to unselect
+     *                      the currently selected device.
+     * @return Whether the selection succeeds. If the selection fails, the state of the instance
+     * remains unaltered.
+     */
+    boolean selectRoute(@Nullable String deviceAddress);
+
     /**
      * Transfers Bluetooth output to the given route.
      *
@@ -139,6 +158,12 @@
         }
 
         @Override
+        public boolean selectRoute(String deviceAddress) {
+            // no op
+            return false;
+        }
+
+        @Override
         public void transferTo(String routeId) {
             // no op
         }
diff --git a/services/core/java/com/android/server/media/DeviceRouteController.java b/services/core/java/com/android/server/media/DeviceRouteController.java
index 9f175a9..0fdaaa7 100644
--- a/services/core/java/com/android/server/media/DeviceRouteController.java
+++ b/services/core/java/com/android/server/media/DeviceRouteController.java
@@ -16,25 +16,17 @@
 
 package com.android.server.media;
 
-import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.RequiresPermission;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothManager;
 import android.content.Context;
 import android.media.AudioManager;
+import android.media.IAudioRoutesObserver;
 import android.media.IAudioService;
 import android.media.MediaRoute2Info;
-import android.media.audiopolicy.AudioProductStrategy;
-import android.os.Looper;
 import android.os.ServiceManager;
-import android.os.UserHandle;
 
 import com.android.media.flags.Flags;
 
-import java.util.List;
-
 /**
  * Controls device routes.
  *
@@ -45,67 +37,46 @@
  */
 /* package */ interface DeviceRouteController {
 
-    /** Returns a new instance of {@link DeviceRouteController}. */
-    @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
-    /* package */ static DeviceRouteController createInstance(
-            @NonNull Context context,
-            @NonNull Looper looper,
+    /**
+     * Returns a new instance of {@link DeviceRouteController}.
+     */
+    /* package */ static DeviceRouteController createInstance(@NonNull Context context,
             @NonNull OnDeviceRouteChangedListener onDeviceRouteChangedListener) {
         AudioManager audioManager = context.getSystemService(AudioManager.class);
-        AudioProductStrategy strategyForMedia = AudioRoutingUtils.getMediaAudioProductStrategy();
+        IAudioService audioService = IAudioService.Stub.asInterface(
+                ServiceManager.getService(Context.AUDIO_SERVICE));
 
-        BluetoothManager bluetoothManager = context.getSystemService(BluetoothManager.class);
-        BluetoothAdapter btAdapter =
-                bluetoothManager != null ? bluetoothManager.getAdapter() : null;
-
-        // TODO: b/305199571 - Make the audio policies implementation work without the need for a
-        // bluetooth adapter or a strategy for media. If no strategy for media is available we can
-        // disallow media router transfers, and without a bluetooth adapter we can remove support
-        // for transfers to inactive bluetooth routes.
-        if (strategyForMedia != null
-                && btAdapter != null
-                && Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
-            return new AudioPoliciesDeviceRouteController(
-                    context,
+        if (Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
+            return new AudioPoliciesDeviceRouteController(context,
                     audioManager,
-                    looper,
-                    strategyForMedia,
-                    btAdapter,
+                    audioService,
                     onDeviceRouteChangedListener);
         } else {
-            IAudioService audioService =
-                    IAudioService.Stub.asInterface(
-                            ServiceManager.getService(Context.AUDIO_SERVICE));
-            return new LegacyDeviceRouteController(
-                    context, audioManager, audioService, onDeviceRouteChangedListener);
+            return new LegacyDeviceRouteController(context,
+                    audioManager,
+                    audioService,
+                    onDeviceRouteChangedListener);
         }
     }
 
+    /**
+     * Select the route with the given built-in or wired {@link MediaRoute2Info.Type}.
+     *
+     * <p>If the type is {@code null} then unselects the route and falls back to the default device
+     * route observed from
+     * {@link com.android.server.audio.AudioService#startWatchingRoutes(IAudioRoutesObserver)}.
+     *
+     * @param type device type. May be {@code null} to unselect currently selected route.
+     * @return whether the selection succeeds. If the selection fails the state of the controller
+     * remains intact.
+     */
+    boolean selectRoute(@Nullable @MediaRoute2Info.Type Integer type);
+
     /** Returns the currently selected device (built-in or wired) route. */
     @NonNull
     MediaRoute2Info getSelectedRoute();
 
     /**
-     * Returns all available routes.
-     *
-     * <p>Note that this method returns available routes including the selected route because (a)
-     * this interface doesn't guarantee that the internal state of the controller won't change
-     * between calls to {@link #getSelectedRoute()} and this method and (b) {@link
-     * #getSelectedRoute()} may be treated as a transferable route (not a selected route) if the
-     * selected route is from {@link BluetoothRouteController}.
-     */
-    List<MediaRoute2Info> getAvailableRoutes();
-
-    /**
-     * Transfers device output to the given route.
-     *
-     * <p>If the route is {@code null} then active route will be deactivated.
-     *
-     * @param routeId to switch to or {@code null} to unset the active device.
-     */
-    void transferTo(@Nullable String routeId);
-
-    /**
      * Updates device route volume.
      *
      * @param volume specifies a volume for the device route or 0 for unknown.
@@ -114,18 +85,6 @@
     boolean updateVolume(int volume);
 
     /**
-     * Starts listening for changes in the system to keep an up to date view of available and
-     * selected devices.
-     */
-    void start(UserHandle mUser);
-
-    /**
-     * Stops keeping the internal state up to date with the system, releasing any resources acquired
-     * in {@link #start}
-     */
-    void stop();
-
-    /**
      * Interface for receiving events when device route has changed.
      */
     interface OnDeviceRouteChangedListener {
diff --git a/services/core/java/com/android/server/media/LegacyBluetoothRouteController.java b/services/core/java/com/android/server/media/LegacyBluetoothRouteController.java
index 041fceaf..ba3cecf 100644
--- a/services/core/java/com/android/server/media/LegacyBluetoothRouteController.java
+++ b/services/core/java/com/android/server/media/LegacyBluetoothRouteController.java
@@ -132,6 +132,12 @@
         mContext.unregisterReceiver(mDeviceStateChangedReceiver);
     }
 
+    @Override
+    public boolean selectRoute(String deviceAddress) {
+        // No-op as the class decides if a route is selected based on Bluetooth events.
+        return false;
+    }
+
     /**
      * Transfers to a given bluetooth route.
      * The dedicated BT device with the route would be activated.
diff --git a/services/core/java/com/android/server/media/LegacyDeviceRouteController.java b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java
index c0f2834..65874e2 100644
--- a/services/core/java/com/android/server/media/LegacyDeviceRouteController.java
+++ b/services/core/java/com/android/server/media/LegacyDeviceRouteController.java
@@ -35,13 +35,11 @@
 import android.media.IAudioService;
 import android.media.MediaRoute2Info;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.util.Slog;
 
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 
-import java.util.Collections;
-import java.util.List;
 import java.util.Objects;
 
 /**
@@ -75,6 +73,7 @@
     private int mDeviceVolume;
     private MediaRoute2Info mDeviceRoute;
 
+    @VisibleForTesting
     /* package */ LegacyDeviceRouteController(@NonNull Context context,
             @NonNull AudioManager audioManager,
             @NonNull IAudioService audioService,
@@ -101,13 +100,9 @@
     }
 
     @Override
-    public void start(UserHandle mUser) {
-        // Nothing to do.
-    }
-
-    @Override
-    public void stop() {
-        // Nothing to do.
+    public boolean selectRoute(@Nullable Integer type) {
+        // No-op as the controller does not support selection from the outside of the class.
+        return false;
     }
 
     @Override
@@ -117,17 +112,6 @@
     }
 
     @Override
-    public synchronized List<MediaRoute2Info> getAvailableRoutes() {
-        return Collections.emptyList();
-    }
-
-    @Override
-    public synchronized void transferTo(@Nullable String routeId) {
-        // Unsupported. This implementation doesn't support transferable routes (always exposes a
-        // single non-bluetooth route).
-    }
-
-    @Override
     public synchronized boolean updateVolume(int volume) {
         if (mDeviceVolume == volume) {
             return false;
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index 86d7833..c8dba80 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -16,12 +16,15 @@
 
 package com.android.server.media;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
 import android.media.AudioManager;
 import android.media.MediaRoute2Info;
 import android.media.MediaRoute2ProviderInfo;
@@ -48,8 +51,7 @@
  */
 // TODO: check thread safety. We may need to use lock to protect variables.
 class SystemMediaRoute2Provider extends MediaRoute2Provider {
-    // Package-visible to use this tag for all system routing logic (done across multiple classes).
-    /* package */ static final String TAG = "MR2SystemProvider";
+    private static final String TAG = "MR2SystemProvider";
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private static final ComponentName COMPONENT_NAME = new ComponentName(
@@ -75,6 +77,26 @@
     private final AudioManagerBroadcastReceiver mAudioReceiver =
             new AudioManagerBroadcastReceiver();
 
+    private final AudioManager.OnDevicesForAttributesChangedListener
+            mOnDevicesForAttributesChangedListener =
+            new AudioManager.OnDevicesForAttributesChangedListener() {
+                @Override
+                public void onDevicesForAttributesChanged(@NonNull AudioAttributes attributes,
+                        @NonNull List<AudioDeviceAttributes> devices) {
+                    if (attributes.getUsage() != AudioAttributes.USAGE_MEDIA) {
+                        return;
+                    }
+
+                    mHandler.post(() -> {
+                        updateSelectedAudioDevice(devices);
+                        notifyProviderState();
+                        if (updateSessionInfosIfNeeded()) {
+                            notifySessionInfoUpdated();
+                        }
+                    });
+                }
+            };
+
     private final Object mRequestLock = new Object();
     @GuardedBy("mRequestLock")
     private volatile SessionCreationRequest mPendingSessionCreationRequest;
@@ -84,8 +106,7 @@
         mIsSystemRouteProvider = true;
         mContext = context;
         mUser = user;
-        Looper looper = Looper.getMainLooper();
-        mHandler = new Handler(looper);
+        mHandler = new Handler(Looper.getMainLooper());
 
         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
 
@@ -102,15 +123,25 @@
         mDeviceRouteController =
                 DeviceRouteController.createInstance(
                         context,
-                        looper,
-                        () ->
-                                mHandler.post(
-                                        () -> {
-                                            publishProviderState();
-                                            if (updateSessionInfosIfNeeded()) {
-                                                notifySessionInfoUpdated();
-                                            }
-                                        }));
+                        () -> {
+                            mHandler.post(
+                                    () -> {
+                                        publishProviderState();
+                                        if (updateSessionInfosIfNeeded()) {
+                                            notifySessionInfoUpdated();
+                                        }
+                                    });
+                        });
+
+        mAudioManager.addOnDevicesForAttributesChangedListener(
+                AudioAttributesUtils.ATTRIBUTES_MEDIA, mContext.getMainExecutor(),
+                mOnDevicesForAttributesChangedListener);
+
+        // These methods below should be called after all fields are initialized, as they
+        // access the fields inside.
+        List<AudioDeviceAttributes> devices =
+                mAudioManager.getDevicesForAttributes(AudioAttributesUtils.ATTRIBUTES_MEDIA);
+        updateSelectedAudioDevice(devices);
         updateProviderState();
         updateSessionInfosIfNeeded();
     }
@@ -120,21 +151,20 @@
         intentFilter.addAction(AudioManager.STREAM_DEVICES_CHANGED_ACTION);
         mContext.registerReceiverAsUser(mAudioReceiver, mUser,
                 intentFilter, null, null);
-        mHandler.post(
-                () -> {
-                    mDeviceRouteController.start(mUser);
-                    mBluetoothRouteController.start(mUser);
-                });
+
+        mHandler.post(() -> {
+            mBluetoothRouteController.start(mUser);
+            notifyProviderState();
+        });
+        updateVolume();
     }
 
     public void stop() {
         mContext.unregisterReceiver(mAudioReceiver);
-        mHandler.post(
-                () -> {
-                    mBluetoothRouteController.stop();
-                    mDeviceRouteController.stop();
-                    notifyProviderState();
-                });
+        mHandler.post(() -> {
+            mBluetoothRouteController.stop();
+            notifyProviderState();
+        });
     }
 
     @Override
@@ -195,26 +225,13 @@
     public void transferToRoute(long requestId, String sessionId, String routeId) {
         if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) {
             // The currently selected route is the default route.
-            Log.w(TAG, "Ignoring transfer to " + MediaRoute2Info.ROUTE_ID_DEFAULT);
             return;
         }
-        MediaRoute2Info selectedDeviceRoute = mDeviceRouteController.getSelectedRoute();
-        boolean isAvailableDeviceRoute =
-                mDeviceRouteController.getAvailableRoutes().stream()
-                        .anyMatch(it -> it.getId().equals(routeId));
-        boolean isSelectedDeviceRoute = TextUtils.equals(routeId, selectedDeviceRoute.getId());
 
-        if (isSelectedDeviceRoute || isAvailableDeviceRoute) {
-            // The requested route is managed by the device route controller. Note that the selected
-            // device route doesn't necessarily match mSelectedRouteId (which is the selected route
-            // of the routing session). If the selected device route is transferred to, we need to
-            // make the bluetooth routes inactive so that the device route becomes the selected
-            // route of the routing session.
-            mDeviceRouteController.transferTo(routeId);
+        MediaRoute2Info selectedDeviceRoute = mDeviceRouteController.getSelectedRoute();
+        if (TextUtils.equals(routeId, selectedDeviceRoute.getId())) {
             mBluetoothRouteController.transferTo(null);
         } else {
-            // The requested route is managed by the bluetooth route controller.
-            mDeviceRouteController.transferTo(null);
             mBluetoothRouteController.transferTo(routeId);
         }
     }
@@ -263,38 +280,41 @@
 
             MediaRoute2Info selectedDeviceRoute = mDeviceRouteController.getSelectedRoute();
 
-            RoutingSessionInfo.Builder builder =
-                    new RoutingSessionInfo.Builder(SYSTEM_SESSION_ID, packageName)
-                            .setSystemSession(true);
+            RoutingSessionInfo.Builder builder = new RoutingSessionInfo.Builder(
+                    SYSTEM_SESSION_ID, packageName).setSystemSession(true);
             builder.addSelectedRoute(selectedDeviceRoute.getId());
             for (MediaRoute2Info route : mBluetoothRouteController.getAllBluetoothRoutes()) {
                 builder.addTransferableRoute(route.getId());
             }
-
-            if (Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
-                for (MediaRoute2Info route : mDeviceRouteController.getAvailableRoutes()) {
-                    if (!TextUtils.equals(selectedDeviceRoute.getId(), route.getId())) {
-                        builder.addTransferableRoute(route.getId());
-                    }
-                }
-            }
             return builder.setProviderId(mUniqueId).build();
         }
     }
 
+    private void updateSelectedAudioDevice(@NonNull List<AudioDeviceAttributes> devices) {
+        if (devices.isEmpty()) {
+            Slog.w(TAG, "The list of preferred devices was empty.");
+            return;
+        }
+
+        AudioDeviceAttributes audioDeviceAttributes = devices.get(0);
+
+        if (AudioAttributesUtils.isDeviceOutputAttributes(audioDeviceAttributes)) {
+            mDeviceRouteController.selectRoute(
+                    AudioAttributesUtils.mapToMediaRouteType(audioDeviceAttributes));
+            mBluetoothRouteController.selectRoute(null);
+        } else if (AudioAttributesUtils.isBluetoothOutputAttributes(audioDeviceAttributes)) {
+            mDeviceRouteController.selectRoute(null);
+            mBluetoothRouteController.selectRoute(audioDeviceAttributes.getAddress());
+        } else {
+            Slog.w(TAG, "Unknown audio attributes: " + audioDeviceAttributes);
+        }
+    }
+
     private void updateProviderState() {
         MediaRoute2ProviderInfo.Builder builder = new MediaRoute2ProviderInfo.Builder();
 
         // We must have a device route in the provider info.
-        if (Flags.enableAudioPoliciesDeviceAndBluetoothController()) {
-            List<MediaRoute2Info> deviceRoutes = mDeviceRouteController.getAvailableRoutes();
-            for (MediaRoute2Info route : deviceRoutes) {
-                builder.addRoute(route);
-            }
-            setProviderState(builder.build());
-        } else {
-            builder.addRoute(mDeviceRouteController.getSelectedRoute());
-        }
+        builder.addRoute(mDeviceRouteController.getSelectedRoute());
 
         for (MediaRoute2Info route : mBluetoothRouteController.getAllBluetoothRoutes()) {
             builder.addRoute(route);
@@ -332,12 +352,7 @@
                             .setProviderId(mUniqueId)
                             .build();
             builder.addSelectedRoute(mSelectedRouteId);
-            for (MediaRoute2Info route : mDeviceRouteController.getAvailableRoutes()) {
-                String routeId = route.getId();
-                if (!mSelectedRouteId.equals(routeId)) {
-                    builder.addTransferableRoute(routeId);
-                }
-            }
+
             for (MediaRoute2Info route : mBluetoothRouteController.getTransferableRoutes()) {
                 builder.addTransferableRoute(route.getId());
             }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index aa0b9b8..3c6887c1 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -26,6 +26,7 @@
 import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
 import static android.app.Notification.FLAG_FSI_REQUESTED_BUT_DENIED;
 import static android.app.Notification.FLAG_INSISTENT;
+import static android.app.Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
 import static android.app.Notification.FLAG_NO_CLEAR;
 import static android.app.Notification.FLAG_NO_DISMISS;
 import static android.app.Notification.FLAG_ONGOING_EVENT;
@@ -58,6 +59,7 @@
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_ON;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
+import static android.app.Flags.lifetimeExtensionRefactor;
 import static android.content.Context.BIND_ALLOW_WHITELIST_MANAGEMENT;
 import static android.content.Context.BIND_AUTO_CREATE;
 import static android.content.Context.BIND_FOREGROUND_SERVICE;
@@ -1224,7 +1226,7 @@
         public void onClearAll(int callingUid, int callingPid, int userId) {
             synchronized (mNotificationLock) {
                 cancelAllLocked(callingUid, callingPid, userId, REASON_CANCEL_ALL, null,
-                        /*includeCurrentProfiles*/ true);
+                        /*includeCurrentProfiles*/ true, FLAG_ONGOING_EVENT | FLAG_NO_CLEAR);
             }
         }
 
@@ -1498,6 +1500,7 @@
             synchronized (mNotificationLock) {
                 NotificationRecord r = mNotificationsByKey.get(key);
                 if (r != null) {
+                    r.recordSmartReplied();
                     LogMaker logMaker = r.getLogMaker()
                             .setCategory(MetricsEvent.SMART_REPLY_ACTION)
                             .setSubtype(replyIndex)
@@ -1804,11 +1807,22 @@
                     record = findNotificationByKeyLocked(intent.getStringExtra(EXTRA_KEY));
                 }
                 if (record != null) {
-                    cancelNotification(record.getSbn().getUid(), record.getSbn().getInitialPid(),
-                            record.getSbn().getPackageName(), record.getSbn().getTag(),
-                            record.getSbn().getId(), 0,
-                            FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB,
-                            true, record.getUserId(), REASON_TIMEOUT, null);
+                    if (lifetimeExtensionRefactor()) {
+                        cancelNotification(record.getSbn().getUid(),
+                                record.getSbn().getInitialPid(),
+                                record.getSbn().getPackageName(), record.getSbn().getTag(),
+                                record.getSbn().getId(), 0,
+                                FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB
+                                        | FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY,
+                                true, record.getUserId(), REASON_TIMEOUT, null);
+                    } else {
+                        cancelNotification(record.getSbn().getUid(),
+                                record.getSbn().getInitialPid(),
+                                record.getSbn().getPackageName(), record.getSbn().getTag(),
+                                record.getSbn().getId(), 0,
+                                FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB,
+                                true, record.getUserId(), REASON_TIMEOUT, null);
+                    }
                 }
             }
         }
@@ -3728,8 +3742,16 @@
         @Override
         public void cancelNotificationWithTag(String pkg, String opPkg, String tag, int id,
                 int userId) {
+            // Don't allow client applications to cancel foreground service notifs, user-initiated
+            // job notifs, autobundled summaries, or notifs that have been replied to.
+            int mustNotHaveFlags = isCallingUidSystem() ? 0 :
+                    (FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB | FLAG_AUTOGROUP_SUMMARY);
+            if (lifetimeExtensionRefactor()) {
+                mustNotHaveFlags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+            }
+
             cancelNotificationInternal(pkg, opPkg, Binder.getCallingUid(), Binder.getCallingPid(),
-                    tag, id, userId);
+                    tag, id, userId, mustNotHaveFlags);
         }
 
         @Override
@@ -3740,9 +3762,16 @@
                     Binder.getCallingUid(), userId, true, false, "cancelAllNotifications", pkg);
 
             // Don't allow the app to cancel active FGS or UIJ notifications
-            cancelAllNotificationsInt(Binder.getCallingUid(), Binder.getCallingPid(),
-                    pkg, null, 0, FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB,
-                    userId, REASON_APP_CANCEL_ALL);
+            if (lifetimeExtensionRefactor()) {
+                cancelAllNotificationsInt(Binder.getCallingUid(), Binder.getCallingPid(),
+                        pkg, null, 0, FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB
+                                | FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY,
+                        userId, REASON_APP_CANCEL_ALL);
+            } else {
+                cancelAllNotificationsInt(Binder.getCallingUid(), Binder.getCallingPid(),
+                        pkg, null, 0, FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB,
+                        userId, REASON_APP_CANCEL_ALL);
+            }
         }
 
         @Override
@@ -4808,8 +4837,16 @@
                                     r.getSbn().getId(), userId, reason);
                         }
                     } else {
-                        cancelAllLocked(callingUid, callingPid, info.userid,
-                                REASON_LISTENER_CANCEL_ALL, info, info.supportsProfiles());
+                        if (lifetimeExtensionRefactor()) {
+                            cancelAllLocked(callingUid, callingPid, info.userid,
+                                    REASON_LISTENER_CANCEL_ALL, info, info.supportsProfiles(),
+                                    FLAG_ONGOING_EVENT | FLAG_NO_CLEAR
+                                            | FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
+                        } else {
+                            cancelAllLocked(callingUid, callingPid, info.userid,
+                                    REASON_LISTENER_CANCEL_ALL, info, info.supportsProfiles(),
+                                    FLAG_ONGOING_EVENT | FLAG_NO_CLEAR);
+                        }
                     }
                 }
             } finally {
@@ -4923,6 +4960,9 @@
                 int callingUid, int callingPid, String pkg, String tag, int id, int userId,
                 int reason) {
             int mustNotHaveFlags = FLAG_ONGOING_EVENT;
+            if (lifetimeExtensionRefactor()) {
+                mustNotHaveFlags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+            }
             cancelNotification(callingUid, callingPid, pkg, tag, id, 0 /* mustHaveFlags */,
                     mustNotHaveFlags,
                     true,
@@ -6712,7 +6752,12 @@
         @Override
         public void cancelNotification(String pkg, String opPkg, int callingUid, int callingPid,
                 String tag, int id, int userId) {
-            cancelNotificationInternal(pkg, opPkg, callingUid, callingPid, tag, id, userId);
+            // Don't allow client applications to cancel foreground service notifs,
+            // user-initiated job notifs or autobundled summaries.
+            final int mustNotHaveFlags = isCallingUidSystem() ? 0 :
+                    (FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB | FLAG_AUTOGROUP_SUMMARY);
+            cancelNotificationInternal(pkg, opPkg, callingUid, callingPid, tag, id, userId,
+                    mustNotHaveFlags);
         }
 
         @Override
@@ -6907,7 +6952,7 @@
     }
 
     void cancelNotificationInternal(String pkg, String opPkg, int callingUid, int callingPid,
-            String tag, int id, int userId) {
+            String tag, int id, int userId, int mustNotHaveFlags) {
         userId = ActivityManager.handleIncomingUser(callingPid,
                 callingUid, userId, true, false, "cancelNotificationWithTag", pkg);
 
@@ -6935,10 +6980,6 @@
             }
         }
 
-        // Don't allow client applications to cancel foreground service notifs, user-initiated job
-        // notifs or autobundled summaries.
-        final int mustNotHaveFlags = isCallingUidSystem() ? 0 :
-                (FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB | FLAG_AUTOGROUP_SUMMARY);
         cancelNotification(uid, callingPid, pkg, tag, id, 0,
                 mustNotHaveFlags, false, userId, REASON_APP_CANCEL, null);
     }
@@ -7294,6 +7335,11 @@
 
         notification.flags &= ~FLAG_FSI_REQUESTED_BUT_DENIED;
 
+        // Apps should not create notifications that are lifetime extended.
+        if (lifetimeExtensionRefactor()) {
+            notification.flags &= ~FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+        }
+
         if (notification.fullScreenIntent != null) {
             final AttributionSource attributionSource =
                     new AttributionSource.Builder(notificationUid).setPackageName(pkg).build();
@@ -10088,7 +10134,7 @@
 
     @GuardedBy("mNotificationLock")
     void cancelAllLocked(int callingUid, int callingPid, int userId, int reason,
-            ManagedServiceInfo listener, boolean includeCurrentProfiles) {
+            ManagedServiceInfo listener, boolean includeCurrentProfiles, int mustNotHaveFlags) {
         final long cancellationElapsedTimeMs = SystemClock.elapsedRealtime();
         mHandler.post(new Runnable() {
             @Override
@@ -10100,7 +10146,7 @@
                             null, userId, 0, 0, reason, listenerName);
 
                     FlagChecker flagChecker = (int flags) -> {
-                        int flagsToCheck = FLAG_ONGOING_EVENT | FLAG_NO_CLEAR;
+                        int flagsToCheck = mustNotHaveFlags;
                         if (REASON_LISTENER_CANCEL_ALL == reason
                                 || REASON_CANCEL_ALL == reason) {
                             flagsToCheck |= FLAG_BUBBLE;
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 100c638..64d3a20 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -24,7 +24,9 @@
 import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL;
 import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE;
 
+import android.annotation.FlaggedApi;
 import android.annotation.Nullable;
+import android.app.Flags;
 import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.NotificationChannel;
@@ -1257,10 +1259,27 @@
         mStats.setExpanded();
     }
 
+    /** Run when the notification is direct replied. */
     public void recordDirectReplied() {
+        if (Flags.lifetimeExtensionRefactor()) {
+            // Mark the NotificationRecord as lifetime extended.
+            Notification notification = getSbn().getNotification();
+            notification.flags |= Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+        }
+
         mStats.setDirectReplied();
     }
 
+
+    /** Run when the notification is smart replied. */
+    @FlaggedApi(Flags.FLAG_LIFETIME_EXTENSION_REFACTOR)
+    public void recordSmartReplied() {
+        Notification notification = getSbn().getNotification();
+        notification.flags |= Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+
+        mStats.setSmartReplied();
+    }
+
     public void recordDismissalSurface(@NotificationStats.DismissalSurface int surface) {
         mStats.setDismissalSurface(surface);
     }
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index f90bf4b..b89b4a2 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1566,6 +1566,7 @@
                 : now - userData.info.creationTime);
         DevicePolicyEventLogger
                 .createEvent(DevicePolicyEnums.REQUEST_QUIET_MODE_ENABLED)
+                .setInt(UserJourneyLogger.getUserTypeForStatsd(userData.info.userType))
                 .setStrings(callingPackage)
                 .setBoolean(enableQuietMode)
                 .setTimePeriod(period)
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index e6bbd52..c089d10 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -740,6 +740,8 @@
         private final Handler mHandler;
         private final String mName;
 
+        private boolean mInsetsAnimationRunning;
+
         Host(Handler handler, String name) {
             mHandler = handler;
             mName = name;
@@ -841,5 +843,10 @@
         public IBinder getWindowToken() {
             return null;
         }
+
+        @Override
+        public void notifyAnimationRunningStateChanged(boolean running) {
+            mInsetsAnimationRunning = running;
+        }
     }
 }
diff --git a/services/robotests/src/com/android/server/media/AudioPoliciesBluetoothRouteControllerTest.java b/services/robotests/src/com/android/server/media/AudioPoliciesBluetoothRouteControllerTest.java
new file mode 100644
index 0000000..0ad4184
--- /dev/null
+++ b/services/robotests/src/com/android/server/media/AudioPoliciesBluetoothRouteControllerTest.java
@@ -0,0 +1,293 @@
+/*
+ * 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.media;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.when;
+
+import android.app.Application;
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.media.MediaRoute2Info;
+import android.os.UserHandle;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.Shadows;
+import org.robolectric.shadows.ShadowBluetoothAdapter;
+import org.robolectric.shadows.ShadowBluetoothDevice;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(RobolectricTestRunner.class)
+public class AudioPoliciesBluetoothRouteControllerTest {
+
+    private static final String DEVICE_ADDRESS_UNKNOWN = ":unknown:ip:address:";
+    private static final String DEVICE_ADDRESS_SAMPLE_1 = "30:59:8B:E4:C6:35";
+    private static final String DEVICE_ADDRESS_SAMPLE_2 = "0D:0D:A6:FF:8D:B6";
+    private static final String DEVICE_ADDRESS_SAMPLE_3 = "2D:9B:0C:C2:6F:78";
+    private static final String DEVICE_ADDRESS_SAMPLE_4 = "66:88:F9:2D:A8:1E";
+
+    private Context mContext;
+
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+
+    @Mock
+    private BluetoothRouteController.BluetoothRoutesUpdatedListener mListener;
+
+    @Mock
+    private BluetoothProfileMonitor mBluetoothProfileMonitor;
+
+    private AudioPoliciesBluetoothRouteController mAudioPoliciesBluetoothRouteController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        Application application = ApplicationProvider.getApplicationContext();
+        mContext = application;
+
+        BluetoothManager bluetoothManager = (BluetoothManager)
+                mContext.getSystemService(Context.BLUETOOTH_SERVICE);
+
+        BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
+        mShadowBluetoothAdapter = Shadows.shadowOf(bluetoothAdapter);
+
+        mAudioPoliciesBluetoothRouteController =
+                new AudioPoliciesBluetoothRouteController(mContext, bluetoothAdapter,
+                        mBluetoothProfileMonitor, mListener) {
+                    @Override
+                    boolean isDeviceConnected(BluetoothDevice device) {
+                        return true;
+                    }
+                };
+
+        // Enable A2DP profile.
+        when(mBluetoothProfileMonitor.isProfileSupported(eq(BluetoothProfile.A2DP), any()))
+                .thenReturn(true);
+        mShadowBluetoothAdapter.setProfileConnectionState(BluetoothProfile.A2DP,
+                BluetoothProfile.STATE_CONNECTED);
+
+        mAudioPoliciesBluetoothRouteController.start(UserHandle.of(0));
+    }
+
+    @Test
+    public void getSelectedRoute_noBluetoothRoutesAvailable_returnsNull() {
+        assertThat(mAudioPoliciesBluetoothRouteController.getSelectedRoute()).isNull();
+    }
+
+    @Test
+    public void selectRoute_noBluetoothRoutesAvailable_returnsFalse() {
+        assertThat(mAudioPoliciesBluetoothRouteController
+                .selectRoute(DEVICE_ADDRESS_UNKNOWN)).isFalse();
+    }
+
+    @Test
+    public void selectRoute_noDeviceWithGivenAddress_returnsFalse() {
+        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(
+                DEVICE_ADDRESS_SAMPLE_1, DEVICE_ADDRESS_SAMPLE_3);
+
+        mShadowBluetoothAdapter.setBondedDevices(devices);
+
+        assertThat(mAudioPoliciesBluetoothRouteController
+                .selectRoute(DEVICE_ADDRESS_SAMPLE_2)).isFalse();
+    }
+
+    @Test
+    public void selectRoute_deviceIsInDevicesSet_returnsTrue() {
+        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(
+                DEVICE_ADDRESS_SAMPLE_1, DEVICE_ADDRESS_SAMPLE_2);
+
+        mShadowBluetoothAdapter.setBondedDevices(devices);
+
+        assertThat(mAudioPoliciesBluetoothRouteController
+                .selectRoute(DEVICE_ADDRESS_SAMPLE_1)).isTrue();
+    }
+
+    @Test
+    public void selectRoute_resetSelectedDevice_returnsTrue() {
+        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(
+                DEVICE_ADDRESS_SAMPLE_1, DEVICE_ADDRESS_SAMPLE_2);
+
+        mShadowBluetoothAdapter.setBondedDevices(devices);
+
+        mAudioPoliciesBluetoothRouteController.selectRoute(DEVICE_ADDRESS_SAMPLE_1);
+        assertThat(mAudioPoliciesBluetoothRouteController.selectRoute(null)).isTrue();
+    }
+
+    @Test
+    public void selectRoute_noSelectedDevice_returnsTrue() {
+        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(
+                DEVICE_ADDRESS_SAMPLE_1, DEVICE_ADDRESS_SAMPLE_2);
+
+        mShadowBluetoothAdapter.setBondedDevices(devices);
+
+        assertThat(mAudioPoliciesBluetoothRouteController.selectRoute(null)).isTrue();
+    }
+
+    @Test
+    public void getSelectedRoute_updateRouteFailed_returnsNull() {
+        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(
+                DEVICE_ADDRESS_SAMPLE_1, DEVICE_ADDRESS_SAMPLE_2);
+
+        mShadowBluetoothAdapter.setBondedDevices(devices);
+        mAudioPoliciesBluetoothRouteController
+                .selectRoute(DEVICE_ADDRESS_SAMPLE_3);
+
+        assertThat(mAudioPoliciesBluetoothRouteController.getSelectedRoute()).isNull();
+    }
+
+    @Test
+    public void getSelectedRoute_updateRouteSuccessful_returnsUpdateDevice() {
+        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(
+                DEVICE_ADDRESS_SAMPLE_1, DEVICE_ADDRESS_SAMPLE_2, DEVICE_ADDRESS_SAMPLE_4);
+
+        assertThat(mAudioPoliciesBluetoothRouteController.getSelectedRoute()).isNull();
+
+        mShadowBluetoothAdapter.setBondedDevices(devices);
+
+        assertThat(mAudioPoliciesBluetoothRouteController
+                .selectRoute(DEVICE_ADDRESS_SAMPLE_4)).isTrue();
+
+        MediaRoute2Info selectedRoute = mAudioPoliciesBluetoothRouteController.getSelectedRoute();
+        assertThat(selectedRoute.getAddress()).isEqualTo(DEVICE_ADDRESS_SAMPLE_4);
+    }
+
+    @Test
+    public void getSelectedRoute_resetSelectedRoute_returnsNull() {
+        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(
+                DEVICE_ADDRESS_SAMPLE_1, DEVICE_ADDRESS_SAMPLE_2, DEVICE_ADDRESS_SAMPLE_4);
+
+        mShadowBluetoothAdapter.setBondedDevices(devices);
+
+        // Device is not null now.
+        mAudioPoliciesBluetoothRouteController.selectRoute(DEVICE_ADDRESS_SAMPLE_4);
+        // Rest the device.
+        mAudioPoliciesBluetoothRouteController.selectRoute(null);
+
+        assertThat(mAudioPoliciesBluetoothRouteController.getSelectedRoute())
+                .isNull();
+    }
+
+    @Test
+    public void getTransferableRoutes_noSelectedRoute_returnsAllBluetoothDevices() {
+        String[] addresses = new String[] { DEVICE_ADDRESS_SAMPLE_1,
+                DEVICE_ADDRESS_SAMPLE_2, DEVICE_ADDRESS_SAMPLE_4 };
+        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(addresses);
+        mShadowBluetoothAdapter.setBondedDevices(devices);
+
+        // Force route controller to update bluetooth devices list.
+        sendBluetoothDevicesChangedBroadcast();
+
+        Set<String> transferableDevices = extractAddressesListFrom(
+                mAudioPoliciesBluetoothRouteController.getTransferableRoutes());
+        assertThat(transferableDevices).containsExactlyElementsIn(addresses);
+    }
+
+    @Test
+    public void getTransferableRoutes_hasSelectedRoute_returnsRoutesWithoutSelectedDevice() {
+        String[] addresses = new String[] { DEVICE_ADDRESS_SAMPLE_1,
+                DEVICE_ADDRESS_SAMPLE_2, DEVICE_ADDRESS_SAMPLE_4 };
+        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(addresses);
+        mShadowBluetoothAdapter.setBondedDevices(devices);
+
+        // Force route controller to update bluetooth devices list.
+        sendBluetoothDevicesChangedBroadcast();
+        mAudioPoliciesBluetoothRouteController.selectRoute(DEVICE_ADDRESS_SAMPLE_4);
+
+        Set<String> transferableDevices = extractAddressesListFrom(
+                mAudioPoliciesBluetoothRouteController.getTransferableRoutes());
+        assertThat(transferableDevices).containsExactly(DEVICE_ADDRESS_SAMPLE_1,
+                DEVICE_ADDRESS_SAMPLE_2);
+    }
+
+    @Test
+    public void getAllBluetoothRoutes_hasSelectedRoute_returnsAllRoutes() {
+        String[] addresses = new String[] { DEVICE_ADDRESS_SAMPLE_1,
+                DEVICE_ADDRESS_SAMPLE_2, DEVICE_ADDRESS_SAMPLE_4 };
+        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(addresses);
+        mShadowBluetoothAdapter.setBondedDevices(devices);
+
+        // Force route controller to update bluetooth devices list.
+        sendBluetoothDevicesChangedBroadcast();
+        mAudioPoliciesBluetoothRouteController.selectRoute(DEVICE_ADDRESS_SAMPLE_4);
+
+        Set<String> bluetoothDevices = extractAddressesListFrom(
+                mAudioPoliciesBluetoothRouteController.getAllBluetoothRoutes());
+        assertThat(bluetoothDevices).containsExactlyElementsIn(addresses);
+    }
+
+    @Test
+    public void updateVolumeForDevice_setVolumeForA2DPTo25_selectedRouteVolumeIsUpdated() {
+        String[] addresses = new String[] { DEVICE_ADDRESS_SAMPLE_1,
+                DEVICE_ADDRESS_SAMPLE_2, DEVICE_ADDRESS_SAMPLE_4 };
+        Set<BluetoothDevice> devices = generateFakeBluetoothDevicesSet(addresses);
+        mShadowBluetoothAdapter.setBondedDevices(devices);
+
+        // Force route controller to update bluetooth devices list.
+        sendBluetoothDevicesChangedBroadcast();
+        mAudioPoliciesBluetoothRouteController.selectRoute(DEVICE_ADDRESS_SAMPLE_4);
+
+        mAudioPoliciesBluetoothRouteController.updateVolumeForDevices(
+                AudioManager.DEVICE_OUT_BLUETOOTH_A2DP, 25);
+
+        MediaRoute2Info selectedRoute = mAudioPoliciesBluetoothRouteController.getSelectedRoute();
+        assertThat(selectedRoute.getVolume()).isEqualTo(25);
+    }
+
+    private void sendBluetoothDevicesChangedBroadcast() {
+        Intent intent = new Intent(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
+        mContext.sendBroadcast(intent);
+    }
+
+    private static Set<String> extractAddressesListFrom(Collection<MediaRoute2Info> routes) {
+        Set<String> addresses = new HashSet<>();
+
+        for (MediaRoute2Info route: routes) {
+            addresses.add(route.getAddress());
+        }
+
+        return addresses;
+    }
+
+    private static Set<BluetoothDevice> generateFakeBluetoothDevicesSet(String... addresses) {
+        Set<BluetoothDevice> devices = new HashSet<>();
+
+        for (String address: addresses) {
+            devices.add(ShadowBluetoothDevice.newInstance(address));
+        }
+
+        return devices;
+    }
+}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
index 9aa6136..6ba7368 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java
@@ -262,8 +262,7 @@
                 Handler handler,
                 BrightnessClamperController.ClamperChangeListener clamperChangeListener,
                 BrightnessClamperController.DisplayDeviceData data,
-                DisplayManagerFlags flags,
-                Context context) {
+                DisplayManagerFlags flags, Context context) {
             mCapturedChangeListener = clamperChangeListener;
             return mClampers;
         }
diff --git a/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java
index 1a3a6a3..cbc8538 100644
--- a/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/DeviceIdleControllerTest.java
@@ -2170,9 +2170,8 @@
     public void testStationaryDetection_QuickDozeOff() {
         setQuickDozeEnabled(false);
         enterDeepState(STATE_IDLE);
-        // Regular progression through states, so time should have increased appropriately.
-        mInjector.nowElapsed += mConstants.IDLE_AFTER_INACTIVE_TIMEOUT + mConstants.SENSING_TIMEOUT
-                + mConstants.LOCATING_TIMEOUT;
+        // Indicate that enough time has passed for the device to be considered stationary.
+        mInjector.nowElapsed += mConstants.MOTION_INACTIVE_TIMEOUT;
 
         StationaryListenerForTest stationaryListener = new StationaryListenerForTest();
 
diff --git a/services/tests/servicestests/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java b/services/tests/servicestests/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java
new file mode 100644
index 0000000..5aef7a3
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java
@@ -0,0 +1,247 @@
+/*
+ * 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.media;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.media.AudioManager;
+import android.media.AudioRoutesInfo;
+import android.media.IAudioRoutesObserver;
+import android.media.MediaRoute2Info;
+import android.os.RemoteException;
+
+import com.android.internal.R;
+import com.android.server.audio.AudioService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(JUnit4.class)
+public class AudioPoliciesDeviceRouteControllerTest {
+
+    private static final String ROUTE_NAME_DEFAULT = "default";
+    private static final String ROUTE_NAME_DOCK = "dock";
+    private static final String ROUTE_NAME_HEADPHONES = "headphones";
+
+    private static final int VOLUME_SAMPLE_1 = 25;
+
+    @Mock
+    private Context mContext;
+    @Mock
+    private Resources mResources;
+    @Mock
+    private AudioManager mAudioManager;
+    @Mock
+    private AudioService mAudioService;
+    @Mock
+    private DeviceRouteController.OnDeviceRouteChangedListener mOnDeviceRouteChangedListener;
+
+    @Captor
+    private ArgumentCaptor<IAudioRoutesObserver.Stub> mAudioRoutesObserverCaptor;
+
+    private AudioPoliciesDeviceRouteController mController;
+
+    private IAudioRoutesObserver.Stub mAudioRoutesObserver;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mResources.getText(anyInt())).thenReturn(ROUTE_NAME_DEFAULT);
+
+        // Setting built-in speaker as default speaker.
+        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
+        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_SPEAKER;
+        when(mAudioService.startWatchingRoutes(mAudioRoutesObserverCaptor.capture()))
+                .thenReturn(audioRoutesInfo);
+
+        mController = new AudioPoliciesDeviceRouteController(
+                mContext, mAudioManager, mAudioService, mOnDeviceRouteChangedListener);
+
+        mAudioRoutesObserver = mAudioRoutesObserverCaptor.getValue();
+    }
+
+    @Test
+    public void getDeviceRoute_noSelectedRoutes_returnsDefaultDevice() {
+        MediaRoute2Info route2Info = mController.getSelectedRoute();
+
+        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_DEFAULT);
+        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+    }
+
+    @Test
+    public void getDeviceRoute_audioRouteHasChanged_returnsRouteFromAudioService() {
+        when(mResources.getText(R.string.default_audio_route_name_headphones))
+                .thenReturn(ROUTE_NAME_HEADPHONES);
+
+        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
+        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
+        callAudioRoutesObserver(audioRoutesInfo);
+
+        MediaRoute2Info route2Info = mController.getSelectedRoute();
+        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_HEADPHONES);
+        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
+    }
+
+    @Test
+    public void getDeviceRoute_selectDevice_returnsSelectedRoute() {
+        when(mResources.getText(R.string.default_audio_route_name_dock_speakers))
+                .thenReturn(ROUTE_NAME_DOCK);
+
+        mController.selectRoute(MediaRoute2Info.TYPE_DOCK);
+
+        MediaRoute2Info route2Info = mController.getSelectedRoute();
+        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_DOCK);
+        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_DOCK);
+    }
+
+    @Test
+    public void getDeviceRoute_hasSelectedAndAudioServiceRoutes_returnsSelectedRoute() {
+        when(mResources.getText(R.string.default_audio_route_name_headphones))
+                .thenReturn(ROUTE_NAME_HEADPHONES);
+        when(mResources.getText(R.string.default_audio_route_name_dock_speakers))
+                .thenReturn(ROUTE_NAME_DOCK);
+
+        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
+        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
+        callAudioRoutesObserver(audioRoutesInfo);
+
+        mController.selectRoute(MediaRoute2Info.TYPE_DOCK);
+
+        MediaRoute2Info route2Info = mController.getSelectedRoute();
+        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_DOCK);
+        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_DOCK);
+    }
+
+    @Test
+    public void getDeviceRoute_unselectRoute_returnsAudioServiceRoute() {
+        when(mResources.getText(R.string.default_audio_route_name_headphones))
+                .thenReturn(ROUTE_NAME_HEADPHONES);
+        when(mResources.getText(R.string.default_audio_route_name_dock_speakers))
+                .thenReturn(ROUTE_NAME_DOCK);
+
+        mController.selectRoute(MediaRoute2Info.TYPE_DOCK);
+
+        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
+        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
+        callAudioRoutesObserver(audioRoutesInfo);
+
+        mController.selectRoute(null);
+
+        MediaRoute2Info route2Info = mController.getSelectedRoute();
+        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_HEADPHONES);
+        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
+    }
+
+    @Test
+    public void getDeviceRoute_selectRouteFails_returnsAudioServiceRoute() {
+        when(mResources.getText(R.string.default_audio_route_name_headphones))
+                .thenReturn(ROUTE_NAME_HEADPHONES);
+
+        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
+        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
+        callAudioRoutesObserver(audioRoutesInfo);
+
+        mController.selectRoute(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
+
+        MediaRoute2Info route2Info = mController.getSelectedRoute();
+        assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_HEADPHONES);
+        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
+    }
+
+    @Test
+    public void selectRoute_selectWiredRoute_returnsTrue() {
+        assertThat(mController.selectRoute(MediaRoute2Info.TYPE_HDMI)).isTrue();
+    }
+
+    @Test
+    public void selectRoute_selectBluetoothRoute_returnsFalse() {
+        assertThat(mController.selectRoute(MediaRoute2Info.TYPE_BLUETOOTH_A2DP)).isFalse();
+    }
+
+    @Test
+    public void selectRoute_unselectRoute_returnsTrue() {
+        assertThat(mController.selectRoute(null)).isTrue();
+    }
+
+    @Test
+    public void updateVolume_noSelectedRoute_deviceRouteVolumeChanged() {
+        when(mResources.getText(R.string.default_audio_route_name_headphones))
+                .thenReturn(ROUTE_NAME_HEADPHONES);
+
+        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
+        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
+        callAudioRoutesObserver(audioRoutesInfo);
+
+        mController.updateVolume(VOLUME_SAMPLE_1);
+
+        MediaRoute2Info route2Info = mController.getSelectedRoute();
+        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
+        assertThat(route2Info.getVolume()).isEqualTo(VOLUME_SAMPLE_1);
+    }
+
+    @Test
+    public void updateVolume_connectSelectedRouteLater_selectedRouteVolumeChanged() {
+        when(mResources.getText(R.string.default_audio_route_name_headphones))
+                .thenReturn(ROUTE_NAME_HEADPHONES);
+        when(mResources.getText(R.string.default_audio_route_name_dock_speakers))
+                .thenReturn(ROUTE_NAME_DOCK);
+
+        AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
+        audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
+        callAudioRoutesObserver(audioRoutesInfo);
+
+        mController.updateVolume(VOLUME_SAMPLE_1);
+
+        mController.selectRoute(MediaRoute2Info.TYPE_DOCK);
+
+        MediaRoute2Info route2Info = mController.getSelectedRoute();
+        assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_DOCK);
+        assertThat(route2Info.getVolume()).isEqualTo(VOLUME_SAMPLE_1);
+    }
+
+    /**
+     * Simulates {@link IAudioRoutesObserver.Stub#dispatchAudioRoutesChanged(AudioRoutesInfo)}
+     * from {@link AudioService}. This happens when there is a wired route change,
+     * like a wired headset being connected.
+     *
+     * @param audioRoutesInfo updated state of connected wired device
+     */
+    private void callAudioRoutesObserver(AudioRoutesInfo audioRoutesInfo) {
+        try {
+            // this is a captured observer implementation
+            // from WiredRoutesController's AudioService#startWatchingRoutes call
+            mAudioRoutesObserver.dispatchAudioRoutesChanged(audioRoutesInfo);
+        } catch (RemoteException exception) {
+            // Should not happen since the object is mocked.
+            assertWithMessage("An unexpected RemoteException happened.").fail();
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/media/DeviceRouteControllerTest.java b/services/tests/servicestests/src/com/android/server/media/DeviceRouteControllerTest.java
index 0961b7d..14b121d 100644
--- a/services/tests/servicestests/src/com/android/server/media/DeviceRouteControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/DeviceRouteControllerTest.java
@@ -19,7 +19,6 @@
 import static com.android.media.flags.Flags.FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER;
 
 import android.content.Context;
-import android.os.Looper;
 import android.platform.test.annotations.RequiresFlagsDisabled;
 import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
@@ -57,8 +56,7 @@
     @RequiresFlagsDisabled(FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER)
     public void createInstance_audioPoliciesFlagIsDisabled_createsLegacyController() {
         DeviceRouteController deviceRouteController =
-                DeviceRouteController.createInstance(
-                        mContext, Looper.getMainLooper(), mOnDeviceRouteChangedListener);
+                DeviceRouteController.createInstance(mContext, mOnDeviceRouteChangedListener);
 
         Truth.assertThat(deviceRouteController).isInstanceOf(LegacyDeviceRouteController.class);
     }
@@ -67,8 +65,7 @@
     @RequiresFlagsEnabled(FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER)
     public void createInstance_audioPoliciesFlagIsEnabled_createsAudioPoliciesController() {
         DeviceRouteController deviceRouteController =
-                DeviceRouteController.createInstance(
-                        mContext, Looper.getMainLooper(), mOnDeviceRouteChangedListener);
+                DeviceRouteController.createInstance(mContext, mOnDeviceRouteChangedListener);
 
         Truth.assertThat(deviceRouteController)
                 .isInstanceOf(AudioPoliciesDeviceRouteController.class);
diff --git a/services/tests/servicestests/src/com/android/server/usage/IntervalStatsTests.java b/services/tests/servicestests/src/com/android/server/usage/IntervalStatsTests.java
index 2be3f1e8..517f483 100644
--- a/services/tests/servicestests/src/com/android/server/usage/IntervalStatsTests.java
+++ b/services/tests/servicestests/src/com/android/server/usage/IntervalStatsTests.java
@@ -21,8 +21,11 @@
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.Assert.fail;
 
+import android.app.usage.Flags;
 import android.app.usage.UsageEvents;
+import android.app.usage.UsageStatsManager;
 import android.content.res.Configuration;
+import android.os.PersistableBundle;
 import android.test.suitebuilder.annotation.SmallTest;
 
 import androidx.test.runner.AndroidJUnit4;
@@ -99,6 +102,17 @@
                 case UsageEvents.Event.LOCUS_ID_SET:
                     event.mLocusId = "locus" + (i % 7); //"random" locus
                     break;
+                case UsageEvents.Event.USER_INTERACTION:
+                    if (Flags.userInteractionTypeApi()) {
+                        // "random" user interaction extras.
+                        PersistableBundle extras = new PersistableBundle();
+                        extras.putString(UsageStatsManager.EXTRA_EVENT_CATEGORY,
+                                "fake.namespace.category" + (i % 13));
+                        extras.putString(UsageStatsManager.EXTRA_EVENT_ACTION,
+                                "fakeaction" + (i % 13));
+                        event.mExtras = extras;
+                    }
+                    break;
             }
 
             intervalStats.addEvent(event);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 09ffe71..ee08fd2 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -28,6 +28,7 @@
 import static android.app.Notification.FLAG_BUBBLE;
 import static android.app.Notification.FLAG_CAN_COLORIZE;
 import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
+import static android.app.Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
 import static android.app.Notification.FLAG_NO_CLEAR;
 import static android.app.Notification.FLAG_ONGOING_EVENT;
 import static android.app.Notification.FLAG_ONLY_ALERT_ONCE;
@@ -320,6 +321,11 @@
     private static final int SECONDARY_DISPLAY_ID = 42;
     private static final int TEST_PROFILE_USERHANDLE = 12;
 
+    private static final String ACTION_NOTIFICATION_TIMEOUT =
+            NotificationManagerService.class.getSimpleName() + ".TIMEOUT";
+    private static final String EXTRA_KEY = "key";
+    private static final String SCHEME_TIMEOUT = "timeout";
+
     private final int mUid = Binder.getCallingUid();
     private final @UserIdInt int mUserId = UserHandle.getUserId(mUid);
 
@@ -442,6 +448,7 @@
     MultiRateLimiter mToastRateLimiter;
     BroadcastReceiver mPackageIntentReceiver;
     BroadcastReceiver mUserSwitchIntentReceiver;
+    BroadcastReceiver mNotificationTimeoutReceiver;
     NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake();
     TestableNotificationManagerService.StrongAuthTrackerFake mStrongAuthTracker;
 
@@ -677,6 +684,8 @@
         verify(mContext, atLeastOnce()).registerReceiverAsUser(broadcastReceiverCaptor.capture(),
                 any(), intentFilterCaptor.capture(), any(), any());
         verify(mContext, atLeastOnce()).registerReceiver(broadcastReceiverCaptor.capture(),
+                intentFilterCaptor.capture(), anyInt());
+        verify(mContext, atLeastOnce()).registerReceiver(broadcastReceiverCaptor.capture(),
                 intentFilterCaptor.capture());
         List<BroadcastReceiver> broadcastReceivers = broadcastReceiverCaptor.getAllValues();
         List<IntentFilter> intentFilters = intentFilterCaptor.getAllValues();
@@ -695,9 +704,14 @@
                     mUserSwitchIntentReceiver = broadcastReceivers.get(i);
                 }
             }
+            if (filter.hasAction(ACTION_NOTIFICATION_TIMEOUT)
+                    && filter.hasDataScheme(SCHEME_TIMEOUT)) {
+                mNotificationTimeoutReceiver = broadcastReceivers.get(i);
+            }
         }
         assertNotNull("package intent receiver should exist", mPackageIntentReceiver);
         assertNotNull("User-switch receiver should exist", mUserSwitchIntentReceiver);
+        assertNotNull("Notification timeout receiver should exist", mNotificationTimeoutReceiver);
 
         // Pretend the shortcut exists
         List<ShortcutInfo> shortcutInfos = new ArrayList<>();
@@ -2430,6 +2444,59 @@
     }
 
     @Test
+    public void testCancelWithTagDoesNotCancelLifetimeExtended() throws Exception {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
+        final NotificationRecord notif = generateNotificationRecord(null);
+        notif.getSbn().getNotification().flags =
+                Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+        mService.addNotification(notif);
+        final StatusBarNotification sbn = notif.getSbn();
+
+        assertThat(mBinderService.getActiveNotifications(sbn.getPackageName()).length).isEqualTo(1);
+        assertThat(mService.getNotificationRecordCount()).isEqualTo(1);
+
+        mBinderService.cancelNotificationWithTag(PKG, PKG, sbn.getTag(), sbn.getId(),
+                sbn.getUserId());
+        waitForIdle();
+
+        assertThat(mBinderService.getActiveNotifications(sbn.getPackageName()).length).isEqualTo(1);
+        assertThat(mService.getNotificationRecordCount()).isEqualTo(1);
+
+        mSetFlagsRule.disableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
+        mBinderService.cancelNotificationWithTag(PKG, PKG, sbn.getTag(), sbn.getId(),
+                sbn.getUserId());
+        waitForIdle();
+
+        assertThat(mBinderService.getActiveNotifications(sbn.getPackageName()).length).isEqualTo(0);
+        assertThat(mService.getNotificationRecordCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void testCancelAllDoesNotCancelLifetimeExtended() throws Exception {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
+        // Adds a lifetime extended notification.
+        final NotificationRecord notif = generateNotificationRecord(mTestNotificationChannel, 1,
+                null, false);
+        notif.getSbn().getNotification().flags =
+                Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+        mService.addNotification(notif);
+        // Adds a second, non-lifetime extended notification.
+        final NotificationRecord notifCancelable = generateNotificationRecord(
+                mTestNotificationChannel, 2, null, false);
+        mService.addNotification(notifCancelable);
+        // Verify that both notifications have been posted and are active.
+        assertThat(mBinderService.getActiveNotifications(PKG).length).isEqualTo(2);
+
+        mBinderService.cancelAllNotifications(PKG, notif.getSbn().getUserId());
+        waitForIdle();
+
+        // The non-lifetime extended notification, with id = 2, has been cancelled.
+        StatusBarNotification[] notifs = mBinderService.getActiveNotifications(PKG);
+        assertThat(notifs.length).isEqualTo(1);
+        assertThat(notifs[0].getId()).isEqualTo(1);
+    }
+
+    @Test
     public void testCancelNotificationWithTag_fromApp_cannotCancelFgsChild()
             throws Exception {
         when(mAmi.applyForegroundServiceNotification(
@@ -2832,6 +2899,24 @@
     }
 
     @Test
+    public void testCancelNotificationsFromListener_clearAll_NoClearLifetimeExt()
+            throws Exception {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
+
+        final NotificationRecord notif = generateNotificationRecord(
+                mTestNotificationChannel, 1, null, false);
+        notif.getNotification().flags = FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+        mService.addNotification(notif);
+
+        mService.getBinderService().cancelNotificationsFromListener(null, null);
+        waitForIdle();
+
+        StatusBarNotification[] notifs =
+                mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
+        assertThat(notifs.length).isEqualTo(1);
+    }
+
+    @Test
     public void testCancelNotificationsFromListener_byKey_GroupWithOngoingParent()
             throws Exception {
         final NotificationRecord parent = generateNotificationRecord(
@@ -3036,6 +3121,22 @@
     }
 
     @Test
+    public void testCancelNotificationsFromListener_byKey_NoClearLifetimeExt()
+            throws Exception {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
+        final NotificationRecord notif = generateNotificationRecord(
+                mTestNotificationChannel, 3, null, false);
+        notif.getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+        mService.addNotification(notif);
+        String[] keys = {notif.getSbn().getKey()};
+        mService.getBinderService().cancelNotificationsFromListener(null, keys);
+        waitForIdle();
+        StatusBarNotification[] notifs =
+                mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
+        assertEquals(1, notifs.length);
+    }
+
+    @Test
     public void testGroupInstanceIds() throws Exception {
         final NotificationRecord group1 = generateNotificationRecord(
                 mTestNotificationChannel, 1, "group1", true);
@@ -5298,6 +5399,79 @@
                 anyInt());
     }
 
+    private void simulateNotificationTimeoutBroadcast(String notificationKey) {
+        final Bundle extras = new Bundle();
+        extras.putString(EXTRA_KEY, notificationKey);
+        final Intent intent = new Intent(ACTION_NOTIFICATION_TIMEOUT);
+        intent.putExtras(extras);
+        mNotificationTimeoutReceiver.onReceive(getContext(), intent);
+    }
+
+    @Test
+    public void testTimeout_CancelsNotification() throws Exception {
+        final NotificationRecord notif = generateNotificationRecord(
+                mTestNotificationChannel, 1, null, false);
+        mService.addNotification(notif);
+
+        simulateNotificationTimeoutBroadcast(notif.getKey());
+        waitForIdle();
+
+        // Check that the notification was cancelled.
+        StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG);
+        assertThat(notifsAfter.length).isEqualTo(0);
+        assertThat(mService.getNotificationRecord(notif.getKey())).isNull();
+    }
+
+    @Test
+    public void testTimeout_NoCancelForegroundServiceNotification() throws Exception {
+        // Creates a notification with FLAG_FOREGROUND_SERVICE
+        final NotificationRecord notif = generateNotificationRecord(null);
+        notif.getSbn().getNotification().flags = Notification.FLAG_FOREGROUND_SERVICE;
+        mService.addNotification(notif);
+
+        simulateNotificationTimeoutBroadcast(notif.getKey());
+        waitForIdle();
+
+        // Check that the notification was not cancelled.
+        StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG);
+        assertThat(notifsAfter.length).isEqualTo(1);
+        assertThat(mService.getNotificationRecord(notif.getKey())).isEqualTo(notif);
+    }
+
+    @Test
+    public void testTimeout_NoCancelUserInitJobNotification() throws Exception {
+        // Create a notification with FLAG_USER_INITIATED_JOB
+        final NotificationRecord notif = generateNotificationRecord(null);
+        notif.getSbn().getNotification().flags = Notification.FLAG_USER_INITIATED_JOB;
+        mService.addNotification(notif);
+
+        simulateNotificationTimeoutBroadcast(notif.getKey());
+        waitForIdle();
+
+        // Check that the notification was not cancelled.
+        StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG);
+        assertThat(notifsAfter.length).isEqualTo(1);
+        assertThat(mService.getNotificationRecord(notif.getKey())).isEqualTo(notif);
+    }
+
+    @Test
+    public void testTimeout_NoCancelLifetimeExtensionNotification() throws Exception {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
+        // Create a notification with FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY
+        final NotificationRecord notif = generateNotificationRecord(null);
+        notif.getSbn().getNotification().flags =
+                Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+        mService.addNotification(notif);
+
+        simulateNotificationTimeoutBroadcast(notif.getKey());
+        waitForIdle();
+
+        // Check that the notification was not cancelled.
+        StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG);
+        assertThat(notifsAfter.length).isEqualTo(1);
+        assertThat(mService.getNotificationRecord(notif.getKey())).isEqualTo(notif);
+    }
+
     @Test
     public void testBumpFGImportance_channelChangePreOApp() throws Exception {
         String preOPkg = PKG_N_MR1;
@@ -7913,6 +8087,7 @@
 
     @Test
     public void testOnNotificationSmartReplySent() {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
         final int replyIndex = 2;
         final String reply = "Hello";
         final boolean modifiedBeforeSending = true;
@@ -7930,6 +8105,10 @@
         assertEquals(1, mNotificationRecordLogger.numCalls());
         assertEquals(NotificationRecordLogger.NotificationEvent.NOTIFICATION_SMART_REPLIED,
                 mNotificationRecordLogger.event(0));
+        // Check that r.recordSmartReplied was called.
+        assertThat(r.getSbn().getNotification().flags & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY)
+                .isGreaterThan(0);
+        assertThat(r.getStats().hasSmartReplied()).isTrue();
     }
 
     @Test
@@ -13116,6 +13295,20 @@
                 eq("package"), anyString(), anyInt(), anyBoolean());
     }
 
+    @Test
+    public void testFixNotification_clearsLifetimeExtendedFlag() throws Exception {
+        mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
+        Notification n = new Notification.Builder(mContext, "test")
+                .setFlag(FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true)
+                .build();
+
+        assertThat(n.flags & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isGreaterThan(0);
+
+        mService.fixNotification(n, PKG, "tag", 9, 0, mUid, NOT_FOREGROUND_SERVICE, true);
+
+        assertThat(n.flags & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(0);
+    }
+
     private NotificationRecord createAndPostNotification(Notification.Builder nb, String testName)
             throws RemoteException {
         StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, testName, mUid, 0,
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
index f83a1df..670d097 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
@@ -29,6 +29,8 @@
 import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL;
 import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
@@ -44,6 +46,7 @@
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
+import android.app.Flags;
 import android.app.Notification;
 import android.app.Notification.Builder;
 import android.app.NotificationChannel;
@@ -64,6 +67,7 @@
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.os.Vibrator;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.service.notification.Adjustment;
 import android.service.notification.StatusBarNotification;
@@ -80,6 +84,7 @@
 import com.android.server.uri.UriGrantsManagerInternal;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -122,6 +127,9 @@
     private static final NotificationRecord.Light CUSTOM_LIGHT =
             new NotificationRecord.Light(1, 2, 3);
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -651,6 +659,7 @@
 
     @Test
     public void testNotificationStats() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
         StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */,
                 true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */,
                 false /* lights */, false /* defaultLights */, groupId /* group */);
@@ -690,6 +699,37 @@
 
         record.recordDirectReplied();
         assertTrue(record.getStats().hasDirectReplied());
+
+        record.recordSmartReplied();
+        assertThat(record.getStats().hasSmartReplied()).isTrue();
+    }
+
+    @Test
+    public void testDirectRepliedAddsLifetimeExtensionFlag() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
+
+        StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */,
+                true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */,
+                false /* lights */, false /* defaultLights */, groupId /* group */);
+        NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+
+        record.recordDirectReplied();
+        assertThat(record.getSbn().getNotification().flags
+                & Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isGreaterThan(0);
+    }
+
+    @Test
+    public void testSmartRepliedAddsLifetimeExtensionFlag() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
+
+        StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */,
+                true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */,
+                false /* lights */, false /* defaultLights */, groupId /* group */);
+        NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+
+        record.recordSmartReplied();
+        assertThat(record.getSbn().getNotification().flags
+                & Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isGreaterThan(0);
     }
 
     @Test
diff --git a/telephony/java/android/telephony/CellularIdentifierDisclosure.java b/telephony/java/android/telephony/CellularIdentifierDisclosure.java
new file mode 100644
index 0000000..7b2db6d
--- /dev/null
+++ b/telephony/java/android/telephony/CellularIdentifierDisclosure.java
@@ -0,0 +1,161 @@
+/*
+ * 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 android.telephony;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * A single occurrence of a cellular identifier being disclosed in the clear before a security
+ * context is established.
+ *
+ * @hide
+ */
+public final class CellularIdentifierDisclosure implements Parcelable {
+    private static final String TAG = "CellularIdentifierDisclosure";
+
+    private @NasProtocolMessage int mNasProtocolMessage;
+    private @CellularIdentifier int mCellularIdentifier;
+    private String mPlmn;
+    private boolean mIsEmergency;
+
+    public CellularIdentifierDisclosure(@NasProtocolMessage int nasProtocolMessage,
+            @CellularIdentifier int cellularIdentifier, String plmn, boolean isEmergency) {
+        mNasProtocolMessage = nasProtocolMessage;
+        mCellularIdentifier = cellularIdentifier;
+        mPlmn = plmn;
+        mIsEmergency = isEmergency;
+    }
+
+    private CellularIdentifierDisclosure(Parcel in) {
+        readFromParcel(in);
+    }
+
+    public @NasProtocolMessage int getNasProtocolMessage() {
+        return mNasProtocolMessage;
+    }
+
+    public @CellularIdentifier int getCellularIdentifier() {
+        return mCellularIdentifier;
+    }
+
+    public String getPlmn() {
+        return mPlmn;
+    }
+
+    public boolean isEmergency() {
+        return mIsEmergency;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(mNasProtocolMessage);
+        out.writeInt(mCellularIdentifier);
+        out.writeBoolean(mIsEmergency);
+        out.writeString8(mPlmn);
+    }
+
+    public static final Parcelable.Creator<CellularIdentifierDisclosure> CREATOR =
+            new Parcelable.Creator<CellularIdentifierDisclosure>() {
+                public CellularIdentifierDisclosure createFromParcel(Parcel in) {
+                    return new CellularIdentifierDisclosure(in);
+                }
+
+                public CellularIdentifierDisclosure[] newArray(int size) {
+                    return new CellularIdentifierDisclosure[size];
+                }
+            };
+
+    @Override
+    public String toString() {
+        return TAG + ":{ mNasProtocolMessage = " + mNasProtocolMessage
+                + " mCellularIdentifier = " + mCellularIdentifier + " mIsEmergency = "
+                + mIsEmergency + " mPlmn = " + mPlmn;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof CellularIdentifierDisclosure)) return false;
+        CellularIdentifierDisclosure that = (CellularIdentifierDisclosure) o;
+        return mNasProtocolMessage == that.mNasProtocolMessage
+                && mCellularIdentifier == that.mCellularIdentifier
+                && mIsEmergency == that.mIsEmergency && mPlmn.equals(that.mPlmn);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mNasProtocolMessage, mCellularIdentifier, mIsEmergency,
+                mPlmn);
+    }
+
+    private void readFromParcel(@NonNull Parcel in) {
+        mNasProtocolMessage = in.readInt();
+        mCellularIdentifier = in.readInt();
+        mIsEmergency = in.readBoolean();
+        mPlmn = in.readString8();
+    }
+
+    public static final int NAS_PROTOCOL_MESSAGE_UNKNOWN = 0;
+    public static final int NAS_PROTOCOL_MESSAGE_ATTACH_REQUEST = 1;
+    public static final int NAS_PROTOCOL_MESSAGE_IDENTITY_RESPONSE = 2;
+    public static final int NAS_PROTOCOL_MESSAGE_DETACH_REQUEST = 3;
+    public static final int NAS_PROTOCOL_MESSAGE_TRACKING_AREA_UPDATE_REQUEST = 4;
+    public static final int NAS_PROTOCOL_MESSAGE_LOCATION_UPDATE_REQUEST = 5;
+    public static final int NAS_PROTOCOL_MESSAGE_AUTHENTICATION_AND_CIPHERING_RESPONSE = 6;
+    public static final int NAS_PROTOCOL_MESSAGE_REGISTRATION_REQUEST = 7;
+    public static final int NAS_PROTOCOL_MESSAGE_DEREGISTRATION_REQUEST = 8;
+    public static final int NAS_PROTOCOL_MESSAGE_CM_REESTABLISHMENT_REQUEST = 9;
+    public static final int NAS_PROTOCOL_MESSAGE_CM_SERVICE_REQUEST = 10;
+    public static final int NAS_PROTOCOL_MESSAGE_IMSI_DETACH_INDICATION = 11;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"NAS_PROTOCOL_MESSAGE_"}, value = {NAS_PROTOCOL_MESSAGE_UNKNOWN,
+            NAS_PROTOCOL_MESSAGE_ATTACH_REQUEST, NAS_PROTOCOL_MESSAGE_IDENTITY_RESPONSE,
+            NAS_PROTOCOL_MESSAGE_DETACH_REQUEST, NAS_PROTOCOL_MESSAGE_TRACKING_AREA_UPDATE_REQUEST,
+            NAS_PROTOCOL_MESSAGE_LOCATION_UPDATE_REQUEST,
+            NAS_PROTOCOL_MESSAGE_AUTHENTICATION_AND_CIPHERING_RESPONSE,
+            NAS_PROTOCOL_MESSAGE_REGISTRATION_REQUEST, NAS_PROTOCOL_MESSAGE_DEREGISTRATION_REQUEST,
+            NAS_PROTOCOL_MESSAGE_CM_REESTABLISHMENT_REQUEST,
+            NAS_PROTOCOL_MESSAGE_CM_SERVICE_REQUEST, NAS_PROTOCOL_MESSAGE_IMSI_DETACH_INDICATION})
+    public @interface NasProtocolMessage {
+    }
+
+    public static final int CELLULAR_IDENTIFIER_UNKNOWN = 0;
+    public static final int CELLULAR_IDENTIFIER_IMSI = 1;
+    public static final int CELLULAR_IDENTIFIER_IMEI = 2;
+    public static final int CELLULAR_IDENTIFIER_SUCI = 3;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"CELLULAR_IDENTIFIER_"}, value = {CELLULAR_IDENTIFIER_UNKNOWN,
+            CELLULAR_IDENTIFIER_IMSI, CELLULAR_IDENTIFIER_IMEI, CELLULAR_IDENTIFIER_SUCI})
+    public @interface CellularIdentifier {
+    }
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/DelegatingFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/DelegatingFilter.kt
index 45f61c5..cdd24e8 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/DelegatingFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/DelegatingFilter.kt
@@ -19,7 +19,7 @@
  * Base class for an [OutputFilter] that uses another filter as a fallback.
  */
 abstract class DelegatingFilter(
-        // fallback shouldn't be used by subclasses, so make it private.
+        // fallback shouldn't be used by subclasses directly, so make it private.
         // They should instead be calling into `super` or `outermostFilter`.
         private val fallback: OutputFilter
 ) : OutputFilter() {
@@ -27,11 +27,21 @@
         fallback.outermostFilter = this
     }
 
+    /**
+     * Returns the outermost filter in a filter chain.
+     *
+     * When methods in a subclass needs to refer to a policy on an item (class, fields, methods)
+     * that are not the "subject" item -- e.g.
+     * in [ClassWidePolicyPropagatingFilter.getPolicyForField], when it checks the
+     * class policy -- [outermostFilter] must be used, rather than the super's method.
+     * The former will always return the correct policy, but the later won't consult outer
+     * filters than the current filter.
+     */
     override var outermostFilter: OutputFilter = this
         get() = field
         set(value) {
             field = value
-            // Propagate the inner filters.
+            // Propagate to the inner filters.
             fallback.outermostFilter = value
         }
 
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
index 84856ac..ea7d1d0 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
@@ -149,7 +149,7 @@
         val fallback = super.getPolicyForField(className, fieldName)
 
         val cn = classes.getClass(className)
-        val classPolicy = super.getPolicyForClass(className)
+        val classPolicy = outermostFilter.getPolicyForClass(className)
 
         log.d("Class ${cn.name} Class policy: $classPolicy")
         if (classPolicy.policy.needsInImpl) {