Merge "Handle failures from Before/AfterClass in the runner" into main
diff --git a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java
index f20b170..3577fcd 100644
--- a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java
+++ b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ClientSocketPerfTest.java
@@ -194,7 +194,7 @@
     /**
      * Simple benchmark for the amount of time to send a given number of messages
      */
-    @Test
+    // @Test Temporarily disabled
     @Parameters(method = "getParams")
     public void time(Config config) throws Exception {
         reset();
diff --git a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java
index af3c405..ac57100 100644
--- a/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java
+++ b/apct-tests/perftests/core/src/android/conscrypt/conscrypt/ServerSocketPerfTest.java
@@ -198,7 +198,7 @@
         executor.awaitTermination(5, TimeUnit.SECONDS);
     }
 
-    @Test
+    // @Test Temporarily disabled
     @Parameters(method = "getParams")
     public void throughput(Config config) throws Exception {
         setup(config);
diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig
index 80db264..5f55075 100644
--- a/apex/jobscheduler/framework/aconfig/job.aconfig
+++ b/apex/jobscheduler/framework/aconfig/job.aconfig
@@ -23,3 +23,10 @@
     description: "Introduce a new RUN_BACKUP_JOBS permission and exemption logic allowing for longer running jobs for apps whose primary purpose is to backup or sync content."
     bug: "318731461"
 }
+
+flag {
+   name: "cleanup_empty_jobs"
+   namespace: "backstage_power"
+   description: "Enables automatic cancellation of jobs due to leaked JobParameters, reducing unnecessary battery drain and improving system efficiency. This includes logging and traces for better issue diagnosis."
+   bug: "349688611"
+}
diff --git a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
index 96494ec..11d17ca 100644
--- a/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
+++ b/apex/jobscheduler/framework/java/android/app/job/IJobCallback.aidl
@@ -85,6 +85,14 @@
      */
     @UnsupportedAppUsage
     void jobFinished(int jobId, boolean reschedule);
+
+    /*
+     * Inform JobScheduler to force finish this job because the client has lost
+     * the job handle. jobFinished can no longer be called from the client.
+     * @param jobId Unique integer used to identify this job
+     */
+    void forceJobFinished(int jobId);
+
     /*
      * Inform JobScheduler of a change in the estimated transfer payload.
      *
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
index e833bb9..52a761f 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobParameters.java
@@ -34,15 +34,21 @@
 import android.os.Parcelable;
 import android.os.PersistableBundle;
 import android.os.RemoteException;
+import android.system.SystemCleaner;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.Cleaner;
 
 /**
  * Contains the parameters used to configure/identify your job. You do not create this object
  * yourself, instead it is handed in to your application by the System.
  */
 public class JobParameters implements Parcelable {
+    private static final String TAG = "JobParameters";
 
     /** @hide */
     public static final int INTERNAL_STOP_REASON_UNKNOWN = -1;
@@ -306,6 +312,10 @@
     private int mStopReason = STOP_REASON_UNDEFINED;
     private int mInternalStopReason = INTERNAL_STOP_REASON_UNKNOWN;
     private String debugStopReason; // Human readable stop reason for debugging.
+    @Nullable
+    private JobCleanupCallback mJobCleanupCallback;
+    @Nullable
+    private Cleaner.Cleanable mCleanable;
 
     /** @hide */
     public JobParameters(IBinder callback, String namespace, int jobId, PersistableBundle extras,
@@ -326,6 +336,8 @@
         this.mTriggeredContentAuthorities = triggeredContentAuthorities;
         this.mNetwork = network;
         this.mJobNamespace = namespace;
+        this.mJobCleanupCallback = null;
+        this.mCleanable = null;
     }
 
     /**
@@ -597,6 +609,8 @@
         mStopReason = in.readInt();
         mInternalStopReason = in.readInt();
         debugStopReason = in.readString();
+        mJobCleanupCallback = null;
+        mCleanable = null;
     }
 
     /** @hide */
@@ -612,6 +626,54 @@
         this.debugStopReason = debugStopReason;
     }
 
+    /** @hide */
+    public void initCleaner(JobCleanupCallback jobCleanupCallback) {
+        mJobCleanupCallback = jobCleanupCallback;
+        mCleanable = SystemCleaner.cleaner().register(this, mJobCleanupCallback);
+    }
+
+    /**
+     * Lazy initialize the cleaner and enable it
+     *
+     * @hide
+     */
+    public void enableCleaner() {
+        if (mJobCleanupCallback == null) {
+            initCleaner(new JobCleanupCallback(IJobCallback.Stub.asInterface(callback), jobId));
+        }
+        mJobCleanupCallback.enableCleaner();
+    }
+
+    /**
+     * Disable the cleaner from running and unregister it
+     *
+     * @hide
+     */
+    public void disableCleaner() {
+        if (mJobCleanupCallback != null) {
+            mJobCleanupCallback.disableCleaner();
+            if (mCleanable != null) {
+                mCleanable.clean();
+                mCleanable = null;
+            }
+            mJobCleanupCallback = null;
+        }
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    @Nullable
+    public Cleaner.Cleanable getCleanable() {
+        return mCleanable;
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    @Nullable
+    public JobCleanupCallback getJobCleanupCallback() {
+        return mJobCleanupCallback;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -647,6 +709,67 @@
         dest.writeString(debugStopReason);
     }
 
+    /**
+     * JobCleanupCallback is used track JobParameters leak. If the job is started
+     * and jobFinish is not called at the time of garbage collection of JobParameters
+     * instance, it is considered a job leak. Force finish the job.
+     *
+     * @hide
+     */
+    public static class JobCleanupCallback implements Runnable {
+        private final IJobCallback mCallback;
+        private final int mJobId;
+        private boolean mIsCleanerEnabled;
+
+        public JobCleanupCallback(
+                IJobCallback callback,
+                int jobId) {
+            mCallback = callback;
+            mJobId = jobId;
+            mIsCleanerEnabled = false;
+        }
+
+        /**
+         * Check if the cleaner is enabled
+         *
+         * @hide
+         */
+        public boolean isCleanerEnabled() {
+            return mIsCleanerEnabled;
+        }
+
+        /**
+         * Enable the cleaner to detect JobParameter leak
+         *
+         * @hide
+         */
+        public void enableCleaner() {
+            mIsCleanerEnabled = true;
+        }
+
+        /**
+         * Disable the cleaner from running.
+         *
+         * @hide
+         */
+        public void disableCleaner() {
+            mIsCleanerEnabled = false;
+        }
+
+        /** @hide */
+        @Override
+        public void run() {
+            if (!isCleanerEnabled()) {
+                return;
+            }
+            try {
+                mCallback.forceJobFinished(mJobId);
+            } catch (Exception e) {
+                Log.wtf(TAG, "Could not destroy running job", e);
+            }
+        }
+    }
+
     public static final @android.annotation.NonNull Creator<JobParameters> CREATOR = new Creator<JobParameters>() {
         @Override
         public JobParameters createFromParcel(Parcel in) {
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
index 79d87ed..5f80c52 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobServiceEngine.java
@@ -165,7 +165,13 @@
                 case MSG_EXECUTE_JOB: {
                     final JobParameters params = (JobParameters) msg.obj;
                     try {
+                        if (Flags.cleanupEmptyJobs()) {
+                            params.enableCleaner();
+                        }
                         boolean workOngoing = JobServiceEngine.this.onStartJob(params);
+                        if (Flags.cleanupEmptyJobs() && !workOngoing) {
+                            params.disableCleaner();
+                        }
                         ackStartMessage(params, workOngoing);
                     } catch (Exception e) {
                         Log.e(TAG, "Error while executing job: " + params.getJobId());
@@ -190,6 +196,9 @@
                     IJobCallback callback = params.getCallback();
                     if (callback != null) {
                         try {
+                            if (Flags.cleanupEmptyJobs()) {
+                                params.disableCleaner();
+                            }
                             callback.jobFinished(params.getJobId(), needsReschedule);
                         } catch (RemoteException e) {
                             Log.e(TAG, "Error reporting job finish to system: binder has gone" +
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index be8e304..ee246d8 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -129,6 +129,8 @@
     private static final String[] VERB_STRINGS = {
             "VERB_BINDING", "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING", "VERB_FINISHED"
     };
+    private static final String TRACE_JOB_FORCE_FINISHED_PREFIX = "forceJobFinished:";
+    private static final String TRACE_JOB_FORCE_FINISHED_DELIMITER = "#";
 
     // States that a job occupies while interacting with the client.
     static final int VERB_BINDING = 0;
@@ -292,6 +294,11 @@
         }
 
         @Override
+        public void forceJobFinished(int jobId) {
+            doForceJobFinished(this, jobId);
+        }
+
+        @Override
         public void updateEstimatedNetworkBytes(int jobId, JobWorkItem item,
                 long downloadBytes, long uploadBytes) {
             doUpdateEstimatedNetworkBytes(this, jobId, item, downloadBytes, uploadBytes);
@@ -762,6 +769,35 @@
         }
     }
 
+    /**
+     * This method just adds traces to evaluate jobs that leak jobparameters at the client.
+     * It does not stop the job.
+     */
+    void doForceJobFinished(JobCallback cb, int jobId) {
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            final JobStatus executing;
+            synchronized (mLock) {
+                // not the current job, presumably it has finished in some way already
+                if (!verifyCallerLocked(cb)) {
+                    return;
+                }
+
+                executing = getRunningJobLocked();
+            }
+            if (executing != null && jobId == executing.getJobId()) {
+                final StringBuilder stateSuffix = new StringBuilder();
+                stateSuffix.append(TRACE_JOB_FORCE_FINISHED_PREFIX);
+                stateSuffix.append(executing.getBatteryName());
+                stateSuffix.append(TRACE_JOB_FORCE_FINISHED_DELIMITER);
+                stateSuffix.append(executing.getJobId());
+                Trace.instant(Trace.TRACE_TAG_POWER, stateSuffix.toString());
+            }
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
     private void doAcknowledgeGetTransferredDownloadBytesMessage(JobCallback cb, int jobId,
             int workId, @BytesLong long transferredBytes) {
         // TODO(255393346): Make sure apps call this appropriately and monitor for abuse
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index b83be6b..b4fb480 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -88,6 +88,7 @@
 import android.view.WindowInsetsController.Appearance;
 import android.window.TaskSnapshot;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.LocalePicker;
 import com.android.internal.app.procstats.ProcessStats;
 import com.android.internal.os.RoSystemProperties;
@@ -238,6 +239,14 @@
     private static final RateLimitingCache<List<ProcessErrorStateInfo>> mErrorProcessesCache =
             new RateLimitingCache<>(10, 2);
 
+    /** Rate-Limiting cache that allows no more than 100 calls to the service per second. */
+    @GuardedBy("mMemoryInfoCache")
+    private static final RateLimitingCache<MemoryInfo> mMemoryInfoCache =
+            new RateLimitingCache<>(10);
+    /** Used to store cached results for rate-limited calls to getMemoryInfo(). */
+    @GuardedBy("mMemoryInfoCache")
+    private static final MemoryInfo mRateLimitedMemInfo = new MemoryInfo();
+
     /**
      * Query handler for mGetCurrentUserIdCache - returns a cached value of the current foreground
      * user id if the backstage_power/android.app.cache_get_current_user_id flag is enabled.
@@ -3510,6 +3519,19 @@
             foregroundAppThreshold = source.readLong();
         }
 
+        /** @hide */
+        public void copyTo(MemoryInfo other) {
+            other.advertisedMem = advertisedMem;
+            other.availMem = availMem;
+            other.totalMem = totalMem;
+            other.threshold = threshold;
+            other.lowMemory = lowMemory;
+            other.hiddenAppThreshold = hiddenAppThreshold;
+            other.secondaryServerThreshold = secondaryServerThreshold;
+            other.visibleAppThreshold = visibleAppThreshold;
+            other.foregroundAppThreshold = foregroundAppThreshold;
+        }
+
         public static final @android.annotation.NonNull Creator<MemoryInfo> CREATOR
                 = new Creator<MemoryInfo>() {
             public MemoryInfo createFromParcel(Parcel source) {
@@ -3536,6 +3558,20 @@
      * manage its memory.
      */
     public void getMemoryInfo(MemoryInfo outInfo) {
+        if (Flags.rateLimitGetMemoryInfo()) {
+            synchronized (mMemoryInfoCache) {
+                mMemoryInfoCache.get(() -> {
+                    getMemoryInfoInternal(mRateLimitedMemInfo);
+                    return mRateLimitedMemInfo;
+                });
+                mRateLimitedMemInfo.copyTo(outInfo);
+            }
+        } else {
+            getMemoryInfoInternal(outInfo);
+        }
+    }
+
+    private void getMemoryInfoInternal(MemoryInfo outInfo) {
         try {
             getService().getMemoryInfo(outInfo);
         } catch (RemoteException e) {
diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index 4d61f41..c0c81df 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -125,3 +125,14 @@
          purpose: PURPOSE_BUGFIX
      }
 }
+
+flag {
+     namespace: "backstage_power"
+     name: "rate_limit_get_memory_info"
+     description: "Rate limit calls to getMemoryInfo using a cache"
+     is_fixed_read_only: true
+     bug: "364312431"
+     metadata {
+         purpose: PURPOSE_BUGFIX
+     }
+}
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index da3cc1b..031380d 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -86,6 +86,7 @@
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.util.XmlUtils;
+import com.android.modules.expresslog.Counter;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -12805,6 +12806,8 @@
                             new ClipData.Item(text, htmlText, null, stream));
                     setClipData(clipData);
                     if (stream != null) {
+                        logCounterIfFlagsMissing(FLAG_GRANT_READ_URI_PERMISSION,
+                                "intents.value_explicit_uri_grant_for_send_action");
                         addFlags(FLAG_GRANT_READ_URI_PERMISSION);
                     }
                     return true;
@@ -12846,6 +12849,8 @@
 
                     setClipData(clipData);
                     if (streams != null) {
+                        logCounterIfFlagsMissing(FLAG_GRANT_READ_URI_PERMISSION,
+                                "intents.value_explicit_uri_grant_for_send_multiple_action");
                         addFlags(FLAG_GRANT_READ_URI_PERMISSION);
                     }
                     return true;
@@ -12865,6 +12870,10 @@
                 putExtra(MediaStore.EXTRA_OUTPUT, output);
 
                 setClipData(ClipData.newRawUri("", output));
+
+                logCounterIfFlagsMissing(
+                        FLAG_GRANT_WRITE_URI_PERMISSION | FLAG_GRANT_READ_URI_PERMISSION,
+                        "intents.value_explicit_uri_grant_for_image_capture_action");
                 addFlags(FLAG_GRANT_WRITE_URI_PERMISSION|FLAG_GRANT_READ_URI_PERMISSION);
                 return true;
             }
@@ -12873,6 +12882,12 @@
         return false;
     }
 
+    private void logCounterIfFlagsMissing(int requiredFlags, String metricId) {
+        if ((getFlags() & requiredFlags) != requiredFlags) {
+            Counter.logIncrement(metricId);
+        }
+    }
+
     @android.ravenwood.annotation.RavenwoodThrow
     private Uri maybeConvertFileToContentUri(Context context, Uri uri) {
         if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())
diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java
index 6514872..ef59e0a 100644
--- a/core/java/android/database/CursorWindow.java
+++ b/core/java/android/database/CursorWindow.java
@@ -26,6 +26,10 @@
 import android.database.sqlite.SQLiteException;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
+import android.ravenwood.annotation.RavenwoodThrow;
 
 import dalvik.annotation.optimization.FastNative;
 import dalvik.system.CloseGuard;
@@ -40,9 +44,8 @@
  * consumer for reading.
  * </p>
  */
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
-        "com.android.platform.test.ravenwood.nativesubstitution.CursorWindow_host")
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("CursorWindow_host")
 public class CursorWindow extends SQLiteClosable implements Parcelable {
     private static final String STATS_TAG = "CursorWindowStats";
 
@@ -63,48 +66,69 @@
     private final CloseGuard mCloseGuard;
 
     // May throw CursorWindowAllocationException
+    @RavenwoodRedirect
     private static native long nativeCreate(String name, int cursorWindowSize);
 
     // May throw CursorWindowAllocationException
+    @RavenwoodRedirect
     private static native long nativeCreateFromParcel(Parcel parcel);
+    @RavenwoodRedirect
     private static native void nativeDispose(long windowPtr);
+    @RavenwoodRedirect
     private static native void nativeWriteToParcel(long windowPtr, Parcel parcel);
 
+    @RavenwoodRedirect
     private static native String nativeGetName(long windowPtr);
+    @RavenwoodRedirect
     private static native byte[] nativeGetBlob(long windowPtr, int row, int column);
+    @RavenwoodRedirect
     private static native String nativeGetString(long windowPtr, int row, int column);
+    @RavenwoodThrow
     private static native void nativeCopyStringToBuffer(long windowPtr, int row, int column,
             CharArrayBuffer buffer);
+    @RavenwoodRedirect
     private static native boolean nativePutBlob(long windowPtr, byte[] value, int row, int column);
+    @RavenwoodRedirect
     private static native boolean nativePutString(long windowPtr, String value,
             int row, int column);
 
     // Below native methods don't do unconstrained work, so are FastNative for performance
 
     @FastNative
+    @RavenwoodThrow
     private static native void nativeClear(long windowPtr);
 
     @FastNative
+    @RavenwoodRedirect
     private static native int nativeGetNumRows(long windowPtr);
     @FastNative
+    @RavenwoodRedirect
     private static native boolean nativeSetNumColumns(long windowPtr, int columnNum);
     @FastNative
+    @RavenwoodRedirect
     private static native boolean nativeAllocRow(long windowPtr);
     @FastNative
+    @RavenwoodThrow
     private static native void nativeFreeLastRow(long windowPtr);
 
     @FastNative
+    @RavenwoodRedirect
     private static native int nativeGetType(long windowPtr, int row, int column);
     @FastNative
+    @RavenwoodRedirect
     private static native long nativeGetLong(long windowPtr, int row, int column);
     @FastNative
+    @RavenwoodRedirect
     private static native double nativeGetDouble(long windowPtr, int row, int column);
 
     @FastNative
+    @RavenwoodRedirect
     private static native boolean nativePutLong(long windowPtr, long value, int row, int column);
     @FastNative
+    @RavenwoodRedirect
     private static native boolean nativePutDouble(long windowPtr, double value, int row, int column);
     @FastNative
+    @RavenwoodThrow
     private static native boolean nativePutNull(long windowPtr, int row, int column);
 
 
diff --git a/core/java/android/os/AppZygote.java b/core/java/android/os/AppZygote.java
index 07fbe4a..0541a96 100644
--- a/core/java/android/os/AppZygote.java
+++ b/core/java/android/os/AppZygote.java
@@ -111,12 +111,15 @@
         try {
             int runtimeFlags = Zygote.getMemorySafetyRuntimeFlagsForSecondaryZygote(
                     mAppInfo, mProcessInfo);
+
+            final int[] sharedAppGid = {
+                    UserHandle.getSharedAppGid(UserHandle.getAppId(mAppInfo.uid)) };
             mZygote = Process.ZYGOTE_PROCESS.startChildZygote(
                     "com.android.internal.os.AppZygoteInit",
                     mAppInfo.processName + "_zygote",
                     mZygoteUid,
                     mZygoteUid,
-                    null,  // gids
+                    sharedAppGid,  // Zygote gets access to shared app GID for profiles
                     runtimeFlags,
                     "app_zygote",  // seInfo
                     abi,  // abi
diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
index da2eec9..b2d9260 100644
--- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java
@@ -19,9 +19,9 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.TestApi;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Trace;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
 import android.util.Log;
 import android.util.Printer;
 import android.util.SparseArray;
@@ -51,9 +51,8 @@
  * <p>You can retrieve the MessageQueue for the current thread with
  * {@link Looper#myQueue() Looper.myQueue()}.
  */
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
-        "com.android.platform.test.ravenwood.nativesubstitution.MessageQueue_host")
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("MessageQueue_host")
 public final class MessageQueue {
     private static final String TAG = "ConcurrentMessageQueue";
     private static final boolean DEBUG = false;
@@ -345,11 +344,17 @@
     // Barriers are indicated by messages with a null target whose arg1 field carries the token.
     private final AtomicInteger mNextBarrierToken = new AtomicInteger(1);
 
+    @RavenwoodRedirect
     private static native long nativeInit();
+    @RavenwoodRedirect
     private static native void nativeDestroy(long ptr);
+    @RavenwoodRedirect
     private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
+    @RavenwoodRedirect
     private static native void nativeWake(long ptr);
+    @RavenwoodRedirect
     private static native boolean nativeIsPolling(long ptr);
+    @RavenwoodRedirect
     private static native void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
 
     MessageQueue(boolean quitAllowed) {
diff --git a/core/java/android/os/LegacyMessageQueue/MessageQueue.java b/core/java/android/os/LegacyMessageQueue/MessageQueue.java
index 6b9b349..4474e7e 100644
--- a/core/java/android/os/LegacyMessageQueue/MessageQueue.java
+++ b/core/java/android/os/LegacyMessageQueue/MessageQueue.java
@@ -20,9 +20,9 @@
 import android.annotation.NonNull;
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
-import android.os.Handler;
-import android.os.Process;
-import android.os.Trace;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
 import android.util.Log;
 import android.util.Printer;
 import android.util.SparseArray;
@@ -42,9 +42,8 @@
  * <p>You can retrieve the MessageQueue for the current thread with
  * {@link Looper#myQueue() Looper.myQueue()}.
  */
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
-        "com.android.platform.test.ravenwood.nativesubstitution.MessageQueue_host")
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("MessageQueue_host")
 public final class MessageQueue {
     private static final String TAG = "MessageQueue";
     private static final boolean DEBUG = false;
@@ -79,12 +78,18 @@
     @UnsupportedAppUsage
     private int mNextBarrierToken;
 
+    @RavenwoodRedirect
     private native static long nativeInit();
+    @RavenwoodRedirect
     private native static void nativeDestroy(long ptr);
     @UnsupportedAppUsage
+    @RavenwoodRedirect
     private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
+    @RavenwoodRedirect
     private native static void nativeWake(long ptr);
+    @RavenwoodRedirect
     private native static boolean nativeIsPolling(long ptr);
+    @RavenwoodRedirect
     private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
 
     MessageQueue(boolean quitAllowed) {
diff --git a/core/java/android/os/LockedMessageQueue/MessageQueue.java b/core/java/android/os/LockedMessageQueue/MessageQueue.java
index b24e14b..f1affce 100644
--- a/core/java/android/os/LockedMessageQueue/MessageQueue.java
+++ b/core/java/android/os/LockedMessageQueue/MessageQueue.java
@@ -20,8 +20,9 @@
 import android.annotation.NonNull;
 import android.annotation.TestApi;
 import android.compat.annotation.UnsupportedAppUsage;
-import android.os.Handler;
-import android.os.Trace;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
 import android.util.Log;
 import android.util.Printer;
 import android.util.SparseArray;
@@ -44,9 +45,8 @@
  * <p>You can retrieve the MessageQueue for the current thread with
  * {@link Looper#myQueue() Looper.myQueue()}.
  */
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
-        "com.android.platform.test.ravenwood.nativesubstitution.MessageQueue_host")
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("MessageQueue_host")
 public final class MessageQueue {
     private static final String TAG = "LockedMessageQueue";
     private static final boolean DEBUG = false;
@@ -389,12 +389,18 @@
     @UnsupportedAppUsage
     private int mNextBarrierToken;
 
+    @RavenwoodRedirect
     private native static long nativeInit();
+    @RavenwoodRedirect
     private native static void nativeDestroy(long ptr);
     @UnsupportedAppUsage
+    @RavenwoodRedirect
     private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
+    @RavenwoodRedirect
     private native static void nativeWake(long ptr);
+    @RavenwoodRedirect
     private native static boolean nativeIsPolling(long ptr);
+    @RavenwoodRedirect
     private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
 
     MessageQueue(boolean quitAllowed) {
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 47096db..2ac2ae9 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -28,7 +28,8 @@
 import android.app.AppOpsManager;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.ravenwood.annotation.RavenwoodKeepWholeClass;
-import android.ravenwood.annotation.RavenwoodNativeSubstitutionClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
 import android.ravenwood.annotation.RavenwoodReplace;
 import android.ravenwood.annotation.RavenwoodThrow;
 import android.text.TextUtils;
@@ -233,8 +234,7 @@
  * {@link #readSparseArray(ClassLoader, Class)}.
  */
 @RavenwoodKeepWholeClass
-@RavenwoodNativeSubstitutionClass(
-        "com.android.platform.test.ravenwood.nativesubstitution.Parcel_host")
+@RavenwoodRedirectionClass("Parcel_host")
 public final class Parcel {
 
     private static final boolean DEBUG_RECYCLE = false;
@@ -387,6 +387,7 @@
     private static final int SIZE_COMPLEX_TYPE = 1;
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native void nativeMarkSensitive(long nativePtr);
     @FastNative
     @RavenwoodThrow
@@ -395,86 +396,126 @@
     @RavenwoodThrow
     private static native boolean nativeIsForRpc(long nativePtr);
     @CriticalNative
+    @RavenwoodRedirect
     private static native int nativeDataSize(long nativePtr);
     @CriticalNative
+    @RavenwoodRedirect
     private static native int nativeDataAvail(long nativePtr);
     @CriticalNative
+    @RavenwoodRedirect
     private static native int nativeDataPosition(long nativePtr);
     @CriticalNative
+    @RavenwoodRedirect
     private static native int nativeDataCapacity(long nativePtr);
     @FastNative
+    @RavenwoodRedirect
     private static native void nativeSetDataSize(long nativePtr, int size);
     @CriticalNative
+    @RavenwoodRedirect
     private static native void nativeSetDataPosition(long nativePtr, int pos);
     @FastNative
+    @RavenwoodRedirect
     private static native void nativeSetDataCapacity(long nativePtr, int size);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native boolean nativePushAllowFds(long nativePtr, boolean allowFds);
     @CriticalNative
+    @RavenwoodRedirect
     private static native void nativeRestoreAllowFds(long nativePtr, boolean lastValue);
 
+    @RavenwoodRedirect
     private static native void nativeWriteByteArray(long nativePtr, byte[] b, int offset, int len);
+    @RavenwoodRedirect
     private static native void nativeWriteBlob(long nativePtr, byte[] b, int offset, int len);
     @CriticalNative
+    @RavenwoodRedirect
     private static native int nativeWriteInt(long nativePtr, int val);
     @CriticalNative
+    @RavenwoodRedirect
     private static native int nativeWriteLong(long nativePtr, long val);
     @CriticalNative
+    @RavenwoodRedirect
     private static native int nativeWriteFloat(long nativePtr, float val);
     @CriticalNative
+    @RavenwoodRedirect
     private static native int nativeWriteDouble(long nativePtr, double val);
     @RavenwoodThrow
     private static native void nativeSignalExceptionForError(int error);
     @FastNative
+    @RavenwoodRedirect
     private static native void nativeWriteString8(long nativePtr, String val);
     @FastNative
+    @RavenwoodRedirect
     private static native void nativeWriteString16(long nativePtr, String val);
     @FastNative
     @RavenwoodThrow
     private static native void nativeWriteStrongBinder(long nativePtr, IBinder val);
     @FastNative
+    @RavenwoodRedirect
     private static native void nativeWriteFileDescriptor(long nativePtr, FileDescriptor val);
 
+    @RavenwoodRedirect
     private static native byte[] nativeCreateByteArray(long nativePtr);
+    @RavenwoodRedirect
     private static native boolean nativeReadByteArray(long nativePtr, byte[] dest, int destLen);
+    @RavenwoodRedirect
     private static native byte[] nativeReadBlob(long nativePtr);
     @CriticalNative
+    @RavenwoodRedirect
     private static native int nativeReadInt(long nativePtr);
     @CriticalNative
+    @RavenwoodRedirect
     private static native long nativeReadLong(long nativePtr);
     @CriticalNative
+    @RavenwoodRedirect
     private static native float nativeReadFloat(long nativePtr);
     @CriticalNative
+    @RavenwoodRedirect
     private static native double nativeReadDouble(long nativePtr);
     @FastNative
+    @RavenwoodRedirect
     private static native String nativeReadString8(long nativePtr);
     @FastNative
+    @RavenwoodRedirect
     private static native String nativeReadString16(long nativePtr);
     @FastNative
     @RavenwoodThrow
     private static native IBinder nativeReadStrongBinder(long nativePtr);
     @FastNative
+    @RavenwoodRedirect
     private static native FileDescriptor nativeReadFileDescriptor(long nativePtr);
 
+    @RavenwoodRedirect
     private static native long nativeCreate();
+    @RavenwoodRedirect
     private static native void nativeFreeBuffer(long nativePtr);
+    @RavenwoodRedirect
     private static native void nativeDestroy(long nativePtr);
 
+    @RavenwoodRedirect
     private static native byte[] nativeMarshall(long nativePtr);
+    @RavenwoodRedirect
     private static native void nativeUnmarshall(
             long nativePtr, byte[] data, int offset, int length);
+    @RavenwoodRedirect
     private static native int nativeCompareData(long thisNativePtr, long otherNativePtr);
+    @RavenwoodRedirect
     private static native boolean nativeCompareDataInRange(
             long ptrA, int offsetA, long ptrB, int offsetB, int length);
+    @RavenwoodRedirect
     private static native void nativeAppendFrom(
             long thisNativePtr, long otherNativePtr, int offset, int length);
     @CriticalNative
+    @RavenwoodRedirect
     private static native boolean nativeHasFileDescriptors(long nativePtr);
+    @RavenwoodRedirect
     private static native boolean nativeHasFileDescriptorsInRange(
             long nativePtr, int offset, int length);
 
+    @RavenwoodRedirect
     private static native boolean nativeHasBinders(long nativePtr);
+    @RavenwoodRedirect
     private static native boolean nativeHasBindersInRange(
             long nativePtr, int offset, int length);
     @RavenwoodThrow
diff --git a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
index 79f229a..80c24a9 100644
--- a/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
+++ b/core/java/android/os/SemiConcurrentMessageQueue/MessageQueue.java
@@ -19,8 +19,9 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.TestApi;
-import android.os.Handler;
-import android.os.Trace;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
 import android.util.Log;
 import android.util.Printer;
 import android.util.SparseArray;
@@ -37,8 +38,6 @@
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 import java.util.PriorityQueue;
-import java.util.PriorityQueue;
-import java.util.PriorityQueue;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
 
@@ -50,9 +49,8 @@
  * <p>You can retrieve the MessageQueue for the current thread with
  * {@link Looper#myQueue() Looper.myQueue()}.
  */
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
-        "com.android.platform.test.ravenwood.nativesubstitution.MessageQueue_host")
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("MessageQueue_host")
 public final class MessageQueue {
     private static final String TAG = "SemiConcurrentMessageQueue";
     private static final boolean DEBUG = false;
@@ -338,11 +336,17 @@
     // Barriers are indicated by messages with a null target whose arg1 field carries the token.
     private final AtomicInteger mNextBarrierToken = new AtomicInteger(1);
 
+    @RavenwoodRedirect
     private static native long nativeInit();
+    @RavenwoodRedirect
     private static native void nativeDestroy(long ptr);
+    @RavenwoodRedirect
     private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
+    @RavenwoodRedirect
     private static native void nativeWake(long ptr);
+    @RavenwoodRedirect
     private static native boolean nativeIsPolling(long ptr);
+    @RavenwoodRedirect
     private static native void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
 
     MessageQueue(boolean quitAllowed) {
diff --git a/core/java/android/os/SystemProperties.java b/core/java/android/os/SystemProperties.java
index 0a38691..e53873b 100644
--- a/core/java/android/os/SystemProperties.java
+++ b/core/java/android/os/SystemProperties.java
@@ -21,11 +21,13 @@
 import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.ravenwood.annotation.RavenwoodKeepWholeClass;
-import android.ravenwood.annotation.RavenwoodNativeSubstitutionClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
 import android.util.Log;
 import android.util.MutableInt;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.ravenwood.RavenwoodEnvironment;
 
 import dalvik.annotation.optimization.CriticalNative;
 import dalvik.annotation.optimization.FastNative;
@@ -56,8 +58,7 @@
  */
 @SystemApi
 @RavenwoodKeepWholeClass
-@RavenwoodNativeSubstitutionClass(
-        "com.android.platform.test.ravenwood.nativesubstitution.SystemProperties_host")
+@RavenwoodRedirectionClass("SystemProperties_host")
 public class SystemProperties {
     private static final String TAG = "SystemProperties";
     private static final boolean TRACK_KEY_ACCESS = false;
@@ -75,7 +76,7 @@
 
     @UnsupportedAppUsage
     @GuardedBy("sChangeCallbacks")
-    private static final ArrayList<Runnable> sChangeCallbacks = new ArrayList<Runnable>();
+    static final ArrayList<Runnable> sChangeCallbacks = new ArrayList<Runnable>();
 
     @GuardedBy("sRoReads")
     private static final HashMap<String, MutableInt> sRoReads =
@@ -102,30 +103,18 @@
     }
 
     /** @hide */
+    @RavenwoodRedirect
     public static void init$ravenwood(Map<String, String> values,
             Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate) {
-        native_init$ravenwood(values, keyReadablePredicate, keyWritablePredicate,
-                SystemProperties::callChangeCallbacks);
-        synchronized (sChangeCallbacks) {
-            sChangeCallbacks.clear();
-        }
+        throw RavenwoodEnvironment.notSupportedOnDevice();
     }
 
     /** @hide */
+    @RavenwoodRedirect
     public static void reset$ravenwood() {
-        native_reset$ravenwood();
-        synchronized (sChangeCallbacks) {
-            sChangeCallbacks.clear();
-        }
+        throw RavenwoodEnvironment.notSupportedOnDevice();
     }
 
-    // These native methods are currently only implemented by Ravenwood, as it's the only
-    // mechanism we have to jump to our RavenwoodNativeSubstitutionClass
-    private static native void native_init$ravenwood(Map<String, String> values,
-            Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate,
-            Runnable changeCallback);
-    private static native void native_reset$ravenwood();
-
     // The one-argument version of native_get used to be a regular native function. Nowadays,
     // we use the two-argument form of native_get all the time, but we can't just delete the
     // one-argument overload: apps use it via reflection, as the UnsupportedAppUsage annotation
@@ -137,34 +126,46 @@
 
     @FastNative
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    @RavenwoodRedirect
     private static native String native_get(String key, String def);
     @FastNative
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    @RavenwoodRedirect
     private static native int native_get_int(String key, int def);
     @FastNative
     @UnsupportedAppUsage
+    @RavenwoodRedirect
     private static native long native_get_long(String key, long def);
     @FastNative
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    @RavenwoodRedirect
     private static native boolean native_get_boolean(String key, boolean def);
 
     @FastNative
+    @RavenwoodRedirect
     private static native long native_find(String name);
     @FastNative
+    @RavenwoodRedirect
     private static native String native_get(long handle);
     @CriticalNative
+    @RavenwoodRedirect
     private static native int native_get_int(long handle, int def);
     @CriticalNative
+    @RavenwoodRedirect
     private static native long native_get_long(long handle, long def);
     @CriticalNative
+    @RavenwoodRedirect
     private static native boolean native_get_boolean(long handle, boolean def);
 
     // _NOT_ FastNative: native_set performs IPC and can block
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    @RavenwoodRedirect
     private static native void native_set(String key, String def);
 
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    @RavenwoodRedirect
     private static native void native_add_change_callback();
+    @RavenwoodRedirect
     private static native void native_report_sysprop_change();
 
     /**
@@ -300,7 +301,7 @@
     }
 
     @SuppressWarnings("unused")  // Called from native code.
-    private static void callChangeCallbacks() {
+    static void callChangeCallbacks() {
         ArrayList<Runnable> callbacks = null;
         synchronized (sChangeCallbacks) {
             //Log.i("foo", "Calling " + sChangeCallbacks.size() + " change callbacks!");
diff --git a/core/java/android/util/EventLog.java b/core/java/android/util/EventLog.java
index 0a73fd1..00545da 100644
--- a/core/java/android/util/EventLog.java
+++ b/core/java/android/util/EventLog.java
@@ -21,6 +21,10 @@
 import android.annotation.SystemApi;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Build;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
+import android.ravenwood.annotation.RavenwoodThrow;
 
 import java.io.BufferedReader;
 import java.io.FileReader;
@@ -48,9 +52,8 @@
  * They carry a payload of one or more int, long, or String values.  The
  * event-log-tags file defines the payload contents for each type code.
  */
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
-        "com.android.platform.test.ravenwood.nativesubstitution.EventLog_host")
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("EventLog_host")
 public class EventLog {
     /** @hide */ public EventLog() {}
 
@@ -339,6 +342,7 @@
      * @param value A value to log
      * @return The number of bytes written
      */
+    @RavenwoodRedirect
     public static native int writeEvent(int tag, int value);
 
     /**
@@ -347,6 +351,7 @@
      * @param value A value to log
      * @return The number of bytes written
      */
+    @RavenwoodRedirect
     public static native int writeEvent(int tag, long value);
 
     /**
@@ -355,6 +360,7 @@
      * @param value A value to log
      * @return The number of bytes written
      */
+    @RavenwoodRedirect
     public static native int writeEvent(int tag, float value);
 
     /**
@@ -363,6 +369,7 @@
      * @param str A value to log
      * @return The number of bytes written
      */
+    @RavenwoodRedirect
     public static native int writeEvent(int tag, String str);
 
     /**
@@ -371,6 +378,7 @@
      * @param list A list of values to log
      * @return The number of bytes written
      */
+    @RavenwoodRedirect
     public static native int writeEvent(int tag, Object... list);
 
     /**
@@ -379,6 +387,7 @@
      * @param output container to add events into
      * @throws IOException if something goes wrong reading events
      */
+    @RavenwoodThrow
     public static native void readEvents(int[] tags, Collection<Event> output)
             throws IOException;
 
@@ -391,6 +400,7 @@
      * @hide
      */
     @SystemApi
+    @RavenwoodThrow
     public static native void readEventsOnWrapping(int[] tags, long timestamp,
             Collection<Event> output)
             throws IOException;
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index f021bdf..e10cc28 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -4345,6 +4345,7 @@
 
             handleSyncRequestWhenNoAsyncDraw(mActiveSurfaceSyncGroup, mHasPendingTransactions,
                     mPendingTransaction, "view not visible");
+            mHasPendingTransactions = false;
         } else if (cancelAndRedraw) {
             if (!mWasLastDrawCanceled) {
                 logAndTrace("Canceling draw."
@@ -4372,6 +4373,7 @@
             if (!performDraw(mActiveSurfaceSyncGroup)) {
                 handleSyncRequestWhenNoAsyncDraw(mActiveSurfaceSyncGroup, mHasPendingTransactions,
                         mPendingTransaction, mLastPerformDrawSkippedReason);
+                mHasPendingTransactions = false;
             }
         }
         mWasLastDrawCanceled = cancelAndRedraw;
@@ -4388,7 +4390,14 @@
             mReportNextDraw = false;
             mLastReportNextDrawReason = null;
             mActiveSurfaceSyncGroup = null;
-            mHasPendingTransactions = false;
+            if (mHasPendingTransactions) {
+                // TODO: We shouldn't ever actually hit this, it means mPendingTransaction wasn't
+                // merged with a sync group or BLASTBufferQueue before making it to this point
+                // But better a one or two frame flicker than steady-state broken from dropping
+                // whatever is in this transaction
+                mPendingTransaction.apply();
+                mHasPendingTransactions = false;
+            }
             mSyncBuffer = false;
             if (isInWMSRequestedSync()) {
                 mWmsRequestSyncGroup.markSyncReady();
@@ -5305,6 +5314,7 @@
     private void registerCallbackForPendingTransactions() {
         Transaction t = new Transaction();
         t.merge(mPendingTransaction);
+        mHasPendingTransactions = false;
 
         registerRtFrameCallback(new FrameDrawingCallback() {
             @Override
@@ -5384,6 +5394,7 @@
         if (!usingAsyncReport && mHasPendingTransactions) {
             pendingTransaction = new Transaction();
             pendingTransaction.merge(mPendingTransaction);
+            mHasPendingTransactions = false;
         } else {
             pendingTransaction = null;
         }
@@ -9942,6 +9953,7 @@
         }
         handleSyncRequestWhenNoAsyncDraw(mActiveSurfaceSyncGroup, mHasPendingTransactions,
                 mPendingTransaction, "shutting down VRI");
+        mHasPendingTransactions = false;
         WindowManagerGlobal.getInstance().doRemoveView(this);
     }
 
@@ -12601,6 +12613,7 @@
         if (mHasPendingTransactions) {
             t = new Transaction();
             t.merge(mPendingTransaction);
+            mHasPendingTransactions = false;
         } else {
             t = null;
         }
diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java
index 4cc0d8a..c3168001 100644
--- a/core/java/android/window/TaskFragmentOrganizer.java
+++ b/core/java/android/window/TaskFragmentOrganizer.java
@@ -69,6 +69,23 @@
     public static final String KEY_ERROR_CALLBACK_OP_TYPE = "operation_type";
 
     /**
+     * Key to bundle {@link TaskFragmentInfo}s from the system in
+     * {@link #registerOrganizer(boolean, Bundle)}
+     *
+     * @hide
+     */
+    public static final String KEY_RESTORE_TASK_FRAGMENTS_INFO = "key_restore_task_fragments_info";
+
+    /**
+     * Key to bundle {@link TaskFragmentParentInfo} from the system in
+     * {@link #registerOrganizer(boolean, Bundle)}
+     *
+     * @hide
+     */
+    public static final String KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO =
+            "key_restore_task_fragment_parent_info";
+
+    /**
      * No change set.
      */
     @WindowManager.TransitionType
diff --git a/core/java/com/android/internal/os/LongArrayMultiStateCounter.java b/core/java/com/android/internal/os/LongArrayMultiStateCounter.java
index 07fa679..dfb2884 100644
--- a/core/java/com/android/internal/os/LongArrayMultiStateCounter.java
+++ b/core/java/com/android/internal/os/LongArrayMultiStateCounter.java
@@ -18,6 +18,10 @@
 
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
+import android.ravenwood.annotation.RavenwoodReplace;
 
 import com.android.internal.util.Preconditions;
 
@@ -55,18 +59,15 @@
  *
  * @hide
  */
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
-        "com.android.platform.test.ravenwood.nativesubstitution.LongArrayMultiStateCounter_host")
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("LongArrayMultiStateCounter_host")
 public final class LongArrayMultiStateCounter implements Parcelable {
 
     /**
      * Container for a native equivalent of a long[].
      */
-    @android.ravenwood.annotation.RavenwoodKeepWholeClass
-    @android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
-            "com.android.platform.test.ravenwood.nativesubstitution"
-            + ".LongArrayMultiStateCounter_host$LongArrayContainer_host")
+    @RavenwoodKeepWholeClass
+    @RavenwoodRedirectionClass("LongArrayContainer_host")
     public static class LongArrayContainer {
         private static NativeAllocationRegistry sRegistry;
 
@@ -81,7 +82,7 @@
             registerNativeAllocation();
         }
 
-        @android.ravenwood.annotation.RavenwoodReplace
+        @RavenwoodReplace
         private void registerNativeAllocation() {
             if (sRegistry == null) {
                 synchronized (LongArrayMultiStateCounter.class) {
@@ -140,18 +141,23 @@
         }
 
         @CriticalNative
+        @RavenwoodRedirect
         private static native long native_init(int length);
 
         @CriticalNative
+        @RavenwoodRedirect
         private static native long native_getReleaseFunc();
 
         @FastNative
+        @RavenwoodRedirect
         private static native void native_setValues(long nativeObject, long[] array);
 
         @FastNative
+        @RavenwoodRedirect
         private static native void native_getValues(long nativeObject, long[] array);
 
         @FastNative
+        @RavenwoodRedirect
         private static native boolean native_combineValues(long nativeObject, long[] array,
                 int[] indexMap);
     }
@@ -175,7 +181,7 @@
         registerNativeAllocation();
     }
 
-    @android.ravenwood.annotation.RavenwoodReplace
+    @RavenwoodReplace
     private void registerNativeAllocation() {
         if (sRegistry == null) {
             synchronized (LongArrayMultiStateCounter.class) {
@@ -374,57 +380,73 @@
 
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native long native_init(int stateCount, int arrayLength);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native long native_getReleaseFunc();
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native void native_setEnabled(long nativeObject, boolean enabled,
             long timestampMs);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native void native_setState(long nativeObject, int state, long timestampMs);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native void native_copyStatesFrom(long nativeObjectTarget,
             long nativeObjectSource);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native void native_setValues(long nativeObject, int state,
             long longArrayContainerNativeObject);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native void native_updateValues(long nativeObject,
             long longArrayContainerNativeObject, long timestampMs);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native void native_incrementValues(long nativeObject,
             long longArrayContainerNativeObject, long timestampMs);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native void native_addCounts(long nativeObject,
             long longArrayContainerNativeObject);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native void native_reset(long nativeObject);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native void native_getCounts(long nativeObject,
             long longArrayContainerNativeObject, int state);
 
     @FastNative
+    @RavenwoodRedirect
     private static native String native_toString(long nativeObject);
 
     @FastNative
+    @RavenwoodRedirect
     private static native void native_writeToParcel(long nativeObject, Parcel dest, int flags);
 
     @FastNative
+    @RavenwoodRedirect
     private static native long native_initFromParcel(Parcel parcel);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native int native_getStateCount(long nativeObject);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native int native_getArrayLength(long nativeObject);
 }
diff --git a/core/java/com/android/internal/os/LongMultiStateCounter.java b/core/java/com/android/internal/os/LongMultiStateCounter.java
index e5662c7..c386a86 100644
--- a/core/java/com/android/internal/os/LongMultiStateCounter.java
+++ b/core/java/com/android/internal/os/LongMultiStateCounter.java
@@ -18,6 +18,10 @@
 
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
+import android.ravenwood.annotation.RavenwoodReplace;
 
 import com.android.internal.util.Preconditions;
 
@@ -55,9 +59,8 @@
  *
  * @hide
  */
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
-@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
-        "com.android.platform.test.ravenwood.nativesubstitution.LongMultiStateCounter_host")
+@RavenwoodKeepWholeClass
+@RavenwoodRedirectionClass("LongMultiStateCounter_host")
 public final class LongMultiStateCounter implements Parcelable {
 
     private static NativeAllocationRegistry sRegistry;
@@ -82,7 +85,7 @@
         mStateCount = native_getStateCount(mNativeObject);
     }
 
-    @android.ravenwood.annotation.RavenwoodReplace
+    @RavenwoodReplace
     private void registerNativeAllocation() {
         if (sRegistry == null) {
             synchronized (LongMultiStateCounter.class) {
@@ -210,43 +213,56 @@
 
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native long native_init(int stateCount);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native long native_getReleaseFunc();
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native void native_setEnabled(long nativeObject, boolean enabled,
             long timestampMs);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native void native_setState(long nativeObject, int state, long timestampMs);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native long native_updateValue(long nativeObject, long value, long timestampMs);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native void native_incrementValue(long nativeObject, long increment,
             long timestampMs);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native void native_addCount(long nativeObject, long count);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native void native_reset(long nativeObject);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native long native_getCount(long nativeObject, int state);
 
     @FastNative
+    @RavenwoodRedirect
     private static native String native_toString(long nativeObject);
 
     @FastNative
+    @RavenwoodRedirect
     private static native void native_writeToParcel(long nativeObject, Parcel dest, int flags);
 
     @FastNative
+    @RavenwoodRedirect
     private static native long native_initFromParcel(Parcel parcel);
 
     @CriticalNative
+    @RavenwoodRedirect
     private static native int native_getStateCount(long nativeObject);
 }
diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
index 12d3264..032ac42 100644
--- a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
+++ b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
@@ -3025,6 +3025,7 @@
     @Override
     public PackageImpl setSplitCodePaths(@Nullable String[] splitCodePaths) {
         this.splitCodePaths = splitCodePaths;
+        this.mSplits = null; // reset for paths changed
         if (splitCodePaths != null) {
             int size = splitCodePaths.length;
             for (int index = 0; index < size; index++) {
diff --git a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
index 319efe0..30b160a 100644
--- a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
+++ b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
@@ -16,15 +16,15 @@
 package com.android.internal.ravenwood;
 
 import android.ravenwood.annotation.RavenwoodKeepWholeClass;
-import android.ravenwood.annotation.RavenwoodNativeSubstitutionClass;
+import android.ravenwood.annotation.RavenwoodRedirect;
+import android.ravenwood.annotation.RavenwoodRedirectionClass;
 import android.ravenwood.annotation.RavenwoodReplace;
 
 /**
  * Class to interact with the Ravenwood environment.
  */
 @RavenwoodKeepWholeClass
-@RavenwoodNativeSubstitutionClass(
-        "com.android.platform.test.ravenwood.nativesubstitution.RavenwoodEnvironment_host")
+@RavenwoodRedirectionClass("RavenwoodEnvironment_host")
 public final class RavenwoodEnvironment {
     public static final String TAG = "RavenwoodEnvironment";
 
@@ -40,7 +40,7 @@
         ensureRavenwoodInitialized();
     }
 
-    private static RuntimeException notSupportedOnDevice() {
+    public static RuntimeException notSupportedOnDevice() {
         return new UnsupportedOperationException("This method can only be used on Ravenwood");
     }
 
@@ -56,14 +56,10 @@
      *
      * No-op if called on the device side.
      */
-    @RavenwoodReplace
+    @RavenwoodRedirect
     public static void ensureRavenwoodInitialized() {
     }
 
-    private static void ensureRavenwoodInitialized$ravenwood() {
-        nativeEnsureRavenwoodInitialized();
-    }
-
     /**
      * USE IT SPARINGLY! Returns true if it's running on Ravenwood, hostside test environment.
      *
@@ -89,15 +85,11 @@
      * Get the object back from the address obtained from
      * {@link dalvik.system.VMRuntime#addressOf(Object)}.
      */
-    @RavenwoodReplace
+    @RavenwoodRedirect
     public <T> T fromAddress(long address) {
         throw notSupportedOnDevice();
     }
 
-    private <T> T fromAddress$ravenwood(long address) {
-        return nativeFromAddress(address);
-    }
-
     /**
      * See {@link Workaround}. It's only usable on Ravenwood.
      */
@@ -113,20 +105,11 @@
     /**
      * @return the "ravenwood-runtime" directory.
      */
-    @RavenwoodReplace
+    @RavenwoodRedirect
     public String getRavenwoodRuntimePath() {
         throw notSupportedOnDevice();
     }
 
-    private String getRavenwoodRuntimePath$ravenwood() {
-        return nativeGetRavenwoodRuntimePath();
-    }
-
-    // Private native methods that are actually substituted on Ravenwood
-    private native <T> T nativeFromAddress(long address);
-    private native String nativeGetRavenwoodRuntimePath();
-    private static native void nativeEnsureRavenwoodInitialized();
-
     /**
      * A set of APIs used to work around missing features on Ravenwood. Ideally, this class should
      * be empty, and all its APIs should be able to be implemented properly.
diff --git a/core/java/com/android/internal/widget/LockPatternView.java b/core/java/com/android/internal/widget/LockPatternView.java
index 11c220b..0ec55f9 100644
--- a/core/java/com/android/internal/widget/LockPatternView.java
+++ b/core/java/com/android/internal/widget/LockPatternView.java
@@ -120,6 +120,7 @@
     private static final String TAG = "LockPatternView";
 
     private OnPatternListener mOnPatternListener;
+    private ExternalHapticsPlayer mExternalHapticsPlayer;
     @UnsupportedAppUsage
     private final ArrayList<Cell> mPattern = new ArrayList<Cell>(9);
 
@@ -317,6 +318,13 @@
         void onPatternDetected(List<Cell> pattern);
     }
 
+    /** An external haptics player for pattern updates. */
+    public interface ExternalHapticsPlayer{
+
+        /** Perform haptic feedback when a cell is added to the pattern. */
+        void performCellAddedFeedback();
+    }
+
     public LockPatternView(Context context) {
         this(context, null);
     }
@@ -461,6 +469,15 @@
     }
 
     /**
+     * Set the external haptics player for feedback on pattern detection.
+     * @param player The external player.
+     */
+    @UnsupportedAppUsage
+    public void setExternalHapticsPlayer(ExternalHapticsPlayer player) {
+        mExternalHapticsPlayer = player;
+    }
+
+    /**
      * Set the pattern explicitely (rather than waiting for the user to input
      * a pattern).
      * @param displayMode How to display the pattern.
@@ -847,6 +864,16 @@
         return null;
     }
 
+    @Override
+    public boolean performHapticFeedback(int feedbackConstant, int flags) {
+        if (mExternalHapticsPlayer != null) {
+            mExternalHapticsPlayer.performCellAddedFeedback();
+            return true;
+        } else {
+            return super.performHapticFeedback(feedbackConstant, flags);
+        }
+    }
+
     private void addCellToPattern(Cell newCell) {
         mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true;
         mPattern.add(newCell);
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java
index 4ce2942..bfccb29 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/BackupHelper.java
@@ -16,16 +16,26 @@
 
 package androidx.window.extensions.embedding;
 
+import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENTS_INFO;
+import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO;
+
 import android.os.Build;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.Looper;
 import android.os.MessageQueue;
+import android.util.ArrayMap;
 import android.util.Log;
+import android.util.SparseArray;
+import android.window.TaskFragmentInfo;
+import android.window.TaskFragmentParentInfo;
+import android.window.WindowContainerTransaction;
 
 import androidx.annotation.NonNull;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Helper class to back up and restore the TaskFragmentOrganizer state, in order to resume
@@ -40,11 +50,21 @@
     @NonNull
     private final SplitController mController;
     @NonNull
+    private final SplitPresenter mPresenter;
+    @NonNull
     private final BackupIdler mBackupIdler = new BackupIdler();
     private boolean mBackupIdlerScheduled;
 
-    BackupHelper(@NonNull SplitController splitController, @NonNull Bundle savedState) {
+    private final List<ParcelableTaskContainerData> mParcelableTaskContainerDataList =
+            new ArrayList<>();
+    private final ArrayMap<IBinder, TaskFragmentInfo> mTaskFragmentInfos = new ArrayMap<>();
+    private final SparseArray<TaskFragmentParentInfo> mTaskFragmentParentInfos =
+            new SparseArray<>();
+
+    BackupHelper(@NonNull SplitController splitController, @NonNull SplitPresenter splitPresenter,
+            @NonNull Bundle savedState) {
         mController = splitController;
+        mPresenter = splitPresenter;
 
         if (!savedState.isEmpty()) {
             restoreState(savedState);
@@ -67,13 +87,13 @@
         public boolean queueIdle() {
             synchronized (mController.mLock) {
                 mBackupIdlerScheduled = false;
-                startBackup();
+                saveState();
             }
             return false;
         }
     }
 
-    private void startBackup() {
+    private void saveState() {
         final List<TaskContainer> taskContainers = mController.getTaskContainers();
         if (taskContainers.isEmpty()) {
             Log.w(TAG, "No task-container to back up");
@@ -97,13 +117,92 @@
             return;
         }
 
-        final List<ParcelableTaskContainerData> parcelableTaskContainerDataList =
-                savedState.getParcelableArrayList(KEY_TASK_CONTAINERS,
-                        ParcelableTaskContainerData.class);
-        for (ParcelableTaskContainerData data : parcelableTaskContainerDataList) {
-            final TaskContainer taskContainer = new TaskContainer(data, mController);
-            if (DEBUG) Log.d(TAG, "Restoring task " + taskContainer.getTaskId());
-            // TODO(b/289875940): implement the TaskContainer restoration.
+        if (DEBUG) Log.d(TAG, "Start restoring saved-state");
+        mParcelableTaskContainerDataList.addAll(savedState.getParcelableArrayList(
+                KEY_TASK_CONTAINERS, ParcelableTaskContainerData.class));
+        if (DEBUG) Log.d(TAG, "Retrieved tasks : " + mParcelableTaskContainerDataList.size());
+        if (mParcelableTaskContainerDataList.isEmpty()) {
+            return;
+        }
+
+        final List<TaskFragmentInfo> infos = savedState.getParcelableArrayList(
+                KEY_RESTORE_TASK_FRAGMENTS_INFO, TaskFragmentInfo.class);
+        for (TaskFragmentInfo info : infos) {
+            if (DEBUG) Log.d(TAG, "Retrieved: " + info);
+            mTaskFragmentInfos.put(info.getFragmentToken(), info);
+            mPresenter.updateTaskFragmentInfo(info);
+        }
+
+        final List<TaskFragmentParentInfo> parentInfos = savedState.getParcelableArrayList(
+                KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO,
+                TaskFragmentParentInfo.class);
+        for (TaskFragmentParentInfo info : parentInfos) {
+            if (DEBUG) Log.d(TAG, "Retrieved: " + info);
+            mTaskFragmentParentInfos.put(info.getTaskId(), info);
         }
     }
-}
+
+    boolean hasPendingStateToRestore() {
+        return !mParcelableTaskContainerDataList.isEmpty();
+    }
+
+    /**
+     * Returns {@code true} if any of the {@link TaskContainer} is restored.
+     * Otherwise, returns {@code false}.
+     */
+    boolean rebuildTaskContainers(@NonNull WindowContainerTransaction wct,
+            @NonNull Set<EmbeddingRule> rules) {
+        if (mParcelableTaskContainerDataList.isEmpty()) {
+            return false;
+        }
+
+        if (DEBUG) Log.d(TAG, "Rebuilding TaskContainers.");
+        final ArrayMap<String, EmbeddingRule> embeddingRuleMap = new ArrayMap<>();
+        for (EmbeddingRule rule : rules) {
+            embeddingRuleMap.put(rule.getTag(), rule);
+        }
+
+        boolean restoredAny = false;
+        for (int i = mParcelableTaskContainerDataList.size() - 1; i >= 0; i--) {
+            final ParcelableTaskContainerData parcelableTaskContainerData =
+                    mParcelableTaskContainerDataList.get(i);
+            final List<String> tags = parcelableTaskContainerData.getSplitRuleTags();
+            if (!embeddingRuleMap.containsAll(tags)) {
+                // has unknown tag, unable to restore.
+                if (DEBUG) {
+                    Log.d(TAG, "Rebuilding TaskContainer abort! Unknown Tag. Task#"
+                            + parcelableTaskContainerData.mTaskId);
+                }
+                continue;
+            }
+
+            mParcelableTaskContainerDataList.remove(parcelableTaskContainerData);
+            final TaskContainer taskContainer = new TaskContainer(parcelableTaskContainerData,
+                    mController, mTaskFragmentInfos);
+            if (DEBUG) Log.d(TAG, "Created TaskContainer " + taskContainer);
+            mController.addTaskContainer(taskContainer.getTaskId(), taskContainer);
+
+            for (ParcelableSplitContainerData splitData :
+                    parcelableTaskContainerData.getParcelableSplitContainerDataList()) {
+                final SplitRule rule = (SplitRule) embeddingRuleMap.get(splitData.mSplitRuleTag);
+                assert rule != null;
+                if (mController.getContainer(splitData.getPrimaryContainerToken()) != null
+                        && mController.getContainer(splitData.getSecondaryContainerToken())
+                        != null) {
+                    taskContainer.addSplitContainer(
+                            new SplitContainer(splitData, mController, rule));
+                }
+            }
+
+            mController.onTaskFragmentParentRestored(wct, taskContainer.getTaskId(),
+                    mTaskFragmentParentInfos.get(taskContainer.getTaskId()));
+            restoredAny = true;
+        }
+
+        if (mParcelableTaskContainerDataList.isEmpty()) {
+            mTaskFragmentParentInfos.clear();
+            mTaskFragmentInfos.clear();
+        }
+        return restoredAny;
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java
index 817cfce..cb280c5 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableSplitContainerData.java
@@ -89,13 +89,13 @@
     };
 
     @NonNull
-    private IBinder getPrimaryContainerToken() {
+    IBinder getPrimaryContainerToken() {
         return mSplitContainer != null ? mSplitContainer.getPrimaryContainer().getToken()
                 : mPrimaryContainerToken;
     }
 
     @NonNull
-    private IBinder getSecondaryContainerToken() {
+    IBinder getSecondaryContainerToken() {
         return mSplitContainer != null ? mSplitContainer.getSecondaryContainer().getToken()
                 : mSecondaryContainerToken;
     }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java
index 7377d00..97aa699 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/ParcelableTaskContainerData.java
@@ -108,6 +108,15 @@
                 : mParcelableSplitContainerDataList;
     }
 
+    @NonNull
+    List<String> getSplitRuleTags() {
+        final List<String> tags = new ArrayList<>();
+        for (ParcelableSplitContainerData data : getParcelableSplitContainerDataList()) {
+            tags.add(data.mSplitRuleTag);
+        }
+        return tags;
+    }
+
     @Override
     public int describeContents() {
         return 0;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
index 6d436ec..faf73c2 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
@@ -86,6 +86,25 @@
         }
     }
 
+    /** This is only used when restoring it from a {@link ParcelableSplitContainerData}. */
+    SplitContainer(@NonNull ParcelableSplitContainerData parcelableData,
+            @NonNull SplitController splitController, @NonNull SplitRule splitRule) {
+        mParcelableData = parcelableData;
+        mPrimaryContainer = splitController.getContainer(parcelableData.getPrimaryContainerToken());
+        mSecondaryContainer = splitController.getContainer(
+                parcelableData.getSecondaryContainerToken());
+        mSplitRule = splitRule;
+        mDefaultSplitAttributes = splitRule.getDefaultSplitAttributes();
+        mCurrentSplitAttributes = mDefaultSplitAttributes;
+
+        if (shouldFinishPrimaryWithSecondary(splitRule)) {
+            mSecondaryContainer.addContainerToFinishOnExit(mPrimaryContainer);
+        }
+        if (shouldFinishSecondaryWithPrimary(splitRule)) {
+            mPrimaryContainer.addContainerToFinishOnExit(mSecondaryContainer);
+        }
+    }
+
     void setPrimaryContainer(@NonNull TaskFragmentContainer primaryContainer) {
         if (!mParcelableData.mIsPrimaryContainerMutable) {
             throw new IllegalStateException("Cannot update primary TaskFragmentContainer");
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index f2f2b7ea..db4bb0e 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -279,6 +279,26 @@
             Log.i(TAG, "Setting embedding rules. Size: " + rules.size());
             mSplitRules.clear();
             mSplitRules.addAll(rules);
+
+            if (!Flags.aeBackStackRestore() || !mPresenter.isRebuildTaskContainersNeeded()) {
+                return;
+            }
+
+            try {
+                final TransactionRecord transactionRecord =
+                        mTransactionManager.startNewTransaction();
+                final WindowContainerTransaction wct = transactionRecord.getTransaction();
+                if (mPresenter.rebuildTaskContainers(wct, rules)) {
+                    transactionRecord.apply(false /* shouldApplyIndependently */);
+                    updateCallbackIfNecessary();
+                } else {
+                    transactionRecord.abort();
+                }
+            } catch (IllegalStateException ex) {
+                Log.e(TAG, "Having an existing transaction while running restoration with"
+                        + "new rules!! It is likely too late to perform the restoration "
+                        + "already!?", ex);
+            }
         }
     }
 
@@ -903,6 +923,12 @@
     }
 
     @GuardedBy("mLock")
+    void onTaskFragmentParentRestored(@NonNull WindowContainerTransaction wct, int taskId,
+            @NonNull TaskFragmentParentInfo parentInfo) {
+        onTaskFragmentParentInfoChanged(wct, taskId, parentInfo);
+    }
+
+    @GuardedBy("mLock")
     void updateContainersInTaskIfVisible(@NonNull WindowContainerTransaction wct, int taskId) {
         final TaskContainer taskContainer = getTaskContainer(taskId);
         if (taskContainer == null) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index abc7b29..0c0ded9 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -24,6 +24,7 @@
 import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
 
 import android.annotation.AnimRes;
+import android.annotation.NonNull;
 import android.app.Activity;
 import android.app.ActivityThread;
 import android.app.WindowConfiguration;
@@ -47,7 +48,6 @@
 import android.window.WindowContainerTransaction;
 
 import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.window.extensions.core.util.function.Function;
 import androidx.window.extensions.embedding.SplitAttributes.SplitType;
@@ -67,6 +67,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Executor;
 
 /**
@@ -174,7 +175,7 @@
         } else {
             registerOrganizer();
         }
-        mBackupHelper = new BackupHelper(controller, outSavedState);
+        mBackupHelper = new BackupHelper(controller, this, outSavedState);
         if (!SplitController.ENABLE_SHELL_TRANSITIONS) {
             // TODO(b/207070762): cleanup with legacy app transition
             // Animation will be handled by WM Shell when Shell transition is enabled.
@@ -186,6 +187,15 @@
         mBackupHelper.scheduleBackup();
     }
 
+    boolean isRebuildTaskContainersNeeded() {
+        return mBackupHelper.hasPendingStateToRestore();
+    }
+
+    boolean rebuildTaskContainers(@NonNull WindowContainerTransaction wct,
+            @NonNull Set<EmbeddingRule> rules) {
+        return mBackupHelper.rebuildTaskContainers(wct, rules);
+    }
+
     /**
      * Deletes the specified container and all other associated and dependent containers in the same
      * transaction.
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 608a3be..74cce68 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -31,6 +31,7 @@
 import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.os.IBinder;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.DisplayMetrics;
 import android.util.Log;
@@ -147,14 +148,23 @@
 
     /** This is only used when restoring it from a {@link ParcelableTaskContainerData}. */
     TaskContainer(@NonNull ParcelableTaskContainerData data,
-            @NonNull SplitController splitController) {
+            @NonNull SplitController splitController,
+            @NonNull ArrayMap<IBinder, TaskFragmentInfo> taskFragmentInfoMap) {
         mParcelableTaskContainerData = new ParcelableTaskContainerData(data, this);
+        mInfo = new TaskFragmentParentInfo(new Configuration(), 0 /* displayId */, -1 /* taskId */,
+                false /* visible */, false /* hasDirectActivity */, null /* decorSurface */);
         mSplitController = splitController;
         for (ParcelableTaskFragmentContainerData tfData :
                 data.getParcelableTaskFragmentContainerDataList()) {
-            final TaskFragmentContainer container =
-                    new TaskFragmentContainer(tfData, splitController, this);
-            mContainers.add(container);
+            final TaskFragmentInfo info = taskFragmentInfoMap.get(tfData.mToken);
+            if (info != null && !info.isEmpty()) {
+                final TaskFragmentContainer container =
+                        new TaskFragmentContainer(tfData, splitController, this);
+                container.setInfo(new WindowContainerTransaction(), info);
+                mContainers.add(container);
+            } else {
+                Log.d(TAG, "Drop " + tfData + " while restoring Task " + data.mTaskId);
+            }
         }
     }
 
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
index cf39415..6c83d88 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java
@@ -29,7 +29,6 @@
 import android.graphics.drawable.Drawable;
 import android.util.TypedValue;
 import android.view.SurfaceControl;
-import android.view.SurfaceSession;
 import android.window.TaskSnapshot;
 
 /**
@@ -75,7 +74,7 @@
 
         public PipColorOverlay(Context context) {
             mContext = context;
-            mLeash = new SurfaceControl.Builder(new SurfaceSession())
+            mLeash = new SurfaceControl.Builder()
                     .setCallsite(TAG)
                     .setName(LAYER_NAME)
                     .setColorLayer()
@@ -123,7 +122,7 @@
         public PipSnapshotOverlay(TaskSnapshot snapshot, Rect sourceRectHint) {
             mSnapshot = snapshot;
             mSourceRectHint = new Rect(sourceRectHint);
-            mLeash = new SurfaceControl.Builder(new SurfaceSession())
+            mLeash = new SurfaceControl.Builder()
                     .setCallsite(TAG)
                     .setName(LAYER_NAME)
                     .build();
@@ -183,7 +182,7 @@
 
             mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888);
             prepareAppIconOverlay(appIcon);
-            mLeash = new SurfaceControl.Builder(new SurfaceSession())
+            mLeash = new SurfaceControl.Builder()
                     .setCallsite(TAG)
                     .setName(LAYER_NAME)
                     .build();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 7b3b207..1563994 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -982,7 +982,6 @@
         mShellBackAnimationRegistry.resetDefaultCrossActivity();
         cancelLatencyTracking();
         mReceivedNullNavigationInfo = false;
-        mBackTransitionHandler.mLastTrigger = triggerBack;
         if (mBackNavigationInfo != null) {
             mPreviousNavigationType = mBackNavigationInfo.getType();
             mBackNavigationInfo.onBackNavigationFinished(triggerBack);
@@ -1103,7 +1102,6 @@
                                     endLatencyTracking();
                                     if (!validateAnimationTargets(apps)) {
                                         Log.e(TAG, "Invalid animation targets!");
-                                        mBackTransitionHandler.consumeQueuedTransitionIfNeeded();
                                         return;
                                     }
                                     mBackAnimationFinishedCallback = finishedCallback;
@@ -1113,7 +1111,6 @@
                                         return;
                                     }
                                     kickStartAnimation();
-                                    mBackTransitionHandler.consumeQueuedTransitionIfNeeded();
                                 });
                     }
 
@@ -1121,7 +1118,6 @@
                     public void onAnimationCancelled() {
                         mShellExecutor.execute(
                                 () -> {
-                                    mBackTransitionHandler.consumeQueuedTransitionIfNeeded();
                                     if (!mShellBackAnimationRegistry.cancel(
                                             mBackNavigationInfo != null
                                                     ? mBackNavigationInfo.getType()
@@ -1160,8 +1156,6 @@
         boolean mCloseTransitionRequested;
         SurfaceControl.Transaction mFinishOpenTransaction;
         Transitions.TransitionFinishCallback mFinishOpenTransitionCallback;
-        QueuedTransition mQueuedTransition = null;
-        boolean mLastTrigger;
         // The Transition to make behindActivity become visible
         IBinder mPrepareOpenTransition;
         // The Transition to make behindActivity become invisible, if prepare open exist and
@@ -1178,13 +1172,6 @@
             }
         }
 
-        void consumeQueuedTransitionIfNeeded() {
-            if (mQueuedTransition != null) {
-                mQueuedTransition.consume();
-                mQueuedTransition = null;
-            }
-        }
-
         private void applyFinishOpenTransition() {
             mOpenTransitionInfo = null;
             mPrepareOpenTransition = null;
@@ -1215,7 +1202,9 @@
                 @NonNull SurfaceControl.Transaction st,
                 @NonNull SurfaceControl.Transaction ft,
                 @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            if (info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) {
+            final boolean isPrepareTransition =
+                    info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION;
+            if (isPrepareTransition) {
                 kickStartAnimation();
             }
             // Both mShellExecutor and Transitions#mMainExecutor are ShellMainThread, so we don't
@@ -1240,21 +1229,14 @@
             }
 
             if (mApps == null || mApps.length == 0) {
-                if (mBackNavigationInfo != null && mShellBackAnimationRegistry
-                        .isWaitingAnimation(mBackNavigationInfo.getType())) {
-                    // Waiting for animation? Queue update to wait for animation start.
-                    consumeQueuedTransitionIfNeeded();
-                    mQueuedTransition = new QueuedTransition(info, st, ft, finishCallback);
-                    return true;
-                } else if (mLastTrigger) {
-                    // animation was done, consume directly
+                if (mCloseTransitionRequested) {
+                    // animation never start, consume directly
                     applyAndFinish(st, ft, finishCallback);
                     return true;
-                } else {
-                    // animation was cancelled but transition haven't happen, we must handle it
-                    if (mClosePrepareTransition == null && mCurrentTracker.isFinished()) {
-                        createClosePrepareTransition();
-                    }
+                } else if (mClosePrepareTransition == null && isPrepareTransition) {
+                    // Gesture animation was cancelled before prepare transition ready, create the
+                    // the close prepare transition
+                    createClosePrepareTransition();
                 }
             }
 
@@ -1413,9 +1395,6 @@
                 if (mPrepareOpenTransition != null) {
                     applyFinishOpenTransition();
                 }
-                if (mQueuedTransition != null) {
-                    consumeQueuedTransitionIfNeeded();
-                }
                 return;
             }
             // Handle the commit transition if this handler is running the open transition.
@@ -1423,11 +1402,9 @@
             t.apply();
             if (mCloseTransitionRequested) {
                 if (mApps == null || mApps.length == 0) {
-                    if (mQueuedTransition == null) {
-                        // animation was done
-                        applyFinishOpenTransition();
-                        mCloseTransitionRequested = false;
-                    } // let queued transition finish.
+                    // animation was done
+                    applyFinishOpenTransition();
+                    mCloseTransitionRequested = false;
                 } else {
                     // we are animating, wait until animation finish
                     mOnAnimationFinishCallback = () -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java
index 4b138e4..dd17e29 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SurfaceUtils.java
@@ -17,7 +17,6 @@
 package com.android.wm.shell.common;
 
 import android.view.SurfaceControl;
-import android.view.SurfaceSession;
 
 /**
  * Helpers for handling surface.
@@ -25,16 +24,15 @@
 public class SurfaceUtils {
     /** Creates a dim layer above host surface. */
     public static SurfaceControl makeDimLayer(SurfaceControl.Transaction t, SurfaceControl host,
-            String name, SurfaceSession surfaceSession) {
-        final SurfaceControl dimLayer = makeColorLayer(host, name, surfaceSession);
+            String name) {
+        final SurfaceControl dimLayer = makeColorLayer(host, name);
         t.setLayer(dimLayer, Integer.MAX_VALUE).setColor(dimLayer, new float[]{0f, 0f, 0f});
         return dimLayer;
     }
 
     /** Creates a color layer for host surface. */
-    public static SurfaceControl makeColorLayer(SurfaceControl host, String name,
-            SurfaceSession surfaceSession) {
-        return new SurfaceControl.Builder(surfaceSession)
+    public static SurfaceControl makeColorLayer(SurfaceControl host, String name) {
+        return new SurfaceControl.Builder()
                 .setParent(host)
                 .setColorLayer()
                 .setName(name)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
index ef33b38..3dc86de 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
@@ -42,7 +42,6 @@
 import android.view.ScrollCaptureResponse;
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
-import android.view.SurfaceSession;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowManager;
@@ -311,7 +310,7 @@
         @Override
         protected SurfaceControl getParentSurface(IWindow window,
                 WindowManager.LayoutParams attrs) {
-            SurfaceControl leash = new SurfaceControl.Builder(new SurfaceSession())
+            SurfaceControl leash = new SurfaceControl.Builder()
                   .setContainerLayer()
                   .setName("SystemWindowLeash")
                   .setHidden(false)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
index 7175e36..de3152a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
@@ -43,7 +43,6 @@
 import android.view.LayoutInflater;
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
-import android.view.SurfaceSession;
 import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowlessWindowManager;
@@ -74,7 +73,6 @@
     private static final String GAP_BACKGROUND_SURFACE_NAME = "GapBackground";
 
     private final IconProvider mIconProvider;
-    private final SurfaceSession mSurfaceSession;
 
     private Drawable mIcon;
     private ImageView mVeilIconView;
@@ -103,17 +101,15 @@
     private int mOffsetY;
     private int mRunningAnimationCount = 0;
 
-    public SplitDecorManager(Configuration configuration, IconProvider iconProvider,
-            SurfaceSession surfaceSession) {
+    public SplitDecorManager(Configuration configuration, IconProvider iconProvider) {
         super(configuration, null /* rootSurface */, null /* hostInputToken */);
         mIconProvider = iconProvider;
-        mSurfaceSession = surfaceSession;
     }
 
     @Override
     protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
         // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later.
-        final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+        final SurfaceControl.Builder builder = new SurfaceControl.Builder()
                 .setContainerLayer()
                 .setName(TAG)
                 .setHidden(true)
@@ -238,7 +234,7 @@
 
         if (mBackgroundLeash == null) {
             mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
-                    RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession);
+                    RESIZING_BACKGROUND_SURFACE_NAME);
             t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask))
                     .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1);
         }
@@ -248,7 +244,7 @@
             final int left = isLandscape ? mOldMainBounds.width() : 0;
             final int top = isLandscape ? 0 : mOldMainBounds.height();
             mGapBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
-                    GAP_BACKGROUND_SURFACE_NAME, mSurfaceSession);
+                    GAP_BACKGROUND_SURFACE_NAME);
             // Fill up another side bounds area.
             t.setColor(mGapBackgroundLeash, getResizingBackgroundColor(resizingTask))
                     .setLayer(mGapBackgroundLeash, Integer.MAX_VALUE - 2)
@@ -405,7 +401,7 @@
         if (mBackgroundLeash == null) {
             // Initialize background
             mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
-                    RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession);
+                    RESIZING_BACKGROUND_SURFACE_NAME);
             t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask))
                     .setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
index 46c1a43..c5f1974 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
@@ -36,7 +36,6 @@
 import android.view.LayoutInflater;
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
-import android.view.SurfaceSession;
 import android.view.WindowManager;
 import android.view.WindowlessWindowManager;
 
@@ -98,7 +97,7 @@
     @Override
     protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
         // Can't set position for the ViewRootImpl SC directly. Create a leash to manipulate later.
-        final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+        final SurfaceControl.Builder builder = new SurfaceControl.Builder()
                 .setContainerLayer()
                 .setName(TAG)
                 .setHidden(true)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
index 0564c95..d2b4f1a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
@@ -38,7 +38,6 @@
 import android.view.IWindow;
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
-import android.view.SurfaceSession;
 import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowlessWindowManager;
@@ -173,7 +172,7 @@
     @Override
     protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
         String className = getClass().getSimpleName();
-        final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+        final SurfaceControl.Builder builder = new SurfaceControl.Builder()
                 .setContainerLayer()
                 .setName(className + "Leash")
                 .setHidden(false)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt
index 831b331..abc26cf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIComponent.kt
@@ -24,7 +24,6 @@
 import android.view.IWindow
 import android.view.SurfaceControl
 import android.view.SurfaceControlViewHost
-import android.view.SurfaceSession
 import android.view.View
 import android.view.WindowManager
 import android.view.WindowlessWindowManager
@@ -106,7 +105,7 @@
         attrs: WindowManager.LayoutParams
     ): SurfaceControl? {
         val className = javaClass.simpleName
-        val builder = SurfaceControl.Builder(SurfaceSession())
+        val builder = SurfaceControl.Builder()
                 .setContainerLayer()
                 .setName(className + "Leash")
                 .setHidden(false)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java
index 71cc8df..422656c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/BackgroundWindowManager.java
@@ -38,7 +38,6 @@
 import android.view.LayoutInflater;
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
-import android.view.SurfaceSession;
 import android.view.View;
 import android.view.WindowManager;
 import android.view.WindowlessWindowManager;
@@ -105,7 +104,7 @@
 
     @Override
     protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
-        final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+        final SurfaceControl.Builder builder = new SurfaceControl.Builder()
                 .setColorLayer()
                 .setBufferSize(mDisplayBounds.width(), mDisplayBounds.height())
                 .setFormat(PixelFormat.RGB_888)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 7e165af..793e2aa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -61,7 +61,6 @@
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
-import android.view.SurfaceSession;
 import android.view.WindowManager;
 import android.widget.Toast;
 import android.window.RemoteTransition;
@@ -897,7 +896,7 @@
 
     private SurfaceControl reparentSplitTasksForAnimation(RemoteAnimationTarget[] apps,
             SurfaceControl.Transaction t, String callsite) {
-        final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+        final SurfaceControl.Builder builder = new SurfaceControl.Builder()
                 .setContainerLayer()
                 .setName("RecentsAnimationSplitTasks")
                 .setHidden(false)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index f3113dc..dad0d4e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -104,7 +104,6 @@
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
-import android.view.SurfaceSession;
 import android.view.WindowManager;
 import android.widget.Toast;
 import android.window.DisplayAreaInfo;
@@ -171,8 +170,6 @@
 
     private static final String TAG = StageCoordinator.class.getSimpleName();
 
-    private final SurfaceSession mSurfaceSession = new SurfaceSession();
-
     private final StageTaskListener mMainStage;
     private final StageListenerImpl mMainStageListener = new StageListenerImpl();
     private final StageTaskListener mSideStage;
@@ -334,7 +331,6 @@
                 mDisplayId,
                 mMainStageListener,
                 mSyncQueue,
-                mSurfaceSession,
                 iconProvider,
                 mWindowDecorViewModel);
         mSideStage = new StageTaskListener(
@@ -343,7 +339,6 @@
                 mDisplayId,
                 mSideStageListener,
                 mSyncQueue,
-                mSurfaceSession,
                 iconProvider,
                 mWindowDecorViewModel);
         mDisplayController = displayController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index 29b5114..d64c0a2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -39,7 +39,6 @@
 import android.util.SparseArray;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
-import android.view.SurfaceSession;
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 
@@ -93,7 +92,6 @@
 
     private final Context mContext;
     private final StageListenerCallbacks mCallbacks;
-    private final SurfaceSession mSurfaceSession;
     private final SyncTransactionQueue mSyncQueue;
     private final IconProvider mIconProvider;
     private final Optional<WindowDecorViewModel> mWindowDecorViewModel;
@@ -108,12 +106,11 @@
 
     StageTaskListener(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
             StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue,
-            SurfaceSession surfaceSession, IconProvider iconProvider,
+            IconProvider iconProvider,
             Optional<WindowDecorViewModel> windowDecorViewModel) {
         mContext = context;
         mCallbacks = callbacks;
         mSyncQueue = syncQueue;
-        mSurfaceSession = surfaceSession;
         mIconProvider = iconProvider;
         mWindowDecorViewModel = windowDecorViewModel;
         taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this);
@@ -203,12 +200,11 @@
             mRootTaskInfo = taskInfo;
             mSplitDecorManager = new SplitDecorManager(
                     mRootTaskInfo.configuration,
-                    mIconProvider,
-                    mSurfaceSession);
+                    mIconProvider);
             mCallbacks.onRootTaskAppeared();
             sendStatusChanged();
             mSyncQueue.runInSync(t -> mDimLayer =
-                    SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer", mSurfaceSession));
+                    SurfaceUtils.makeDimLayer(t, mRootLeash, "Dim layer"));
         } else if (taskInfo.parentTaskId == mRootTaskInfo.taskId) {
             final int taskId = taskInfo.taskId;
             mChildrenLeashes.put(taskId, leash);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
index fac3592..2e9b53e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
@@ -33,7 +33,6 @@
 import android.util.SparseArray;
 import android.view.IWindow;
 import android.view.SurfaceControl;
-import android.view.SurfaceSession;
 import android.view.WindowManager;
 import android.view.WindowlessWindowManager;
 import android.window.SplashScreenView;
@@ -204,7 +203,7 @@
         @Override
         protected SurfaceControl getParentSurface(IWindow window,
                 WindowManager.LayoutParams attrs) {
-            final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
+            final SurfaceControl.Builder builder = new SurfaceControl.Builder()
                     .setContainerLayer()
                     .setName("Windowless window")
                     .setHidden(false)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 4fc6c44..ff4b981 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -92,7 +92,6 @@
 import android.util.ArrayMap;
 import android.view.Choreographer;
 import android.view.SurfaceControl;
-import android.view.SurfaceSession;
 import android.view.WindowManager;
 import android.view.animation.AlphaAnimation;
 import android.view.animation.Animation;
@@ -134,8 +133,6 @@
     private final TransitionAnimation mTransitionAnimation;
     private final DevicePolicyManager mDevicePolicyManager;
 
-    private final SurfaceSession mSurfaceSession = new SurfaceSession();
-
     /** Keeps track of the currently-running animations associated with each transition. */
     private final ArrayMap<IBinder, ArrayList<Animator>> mAnimations = new ArrayMap<>();
 
@@ -705,7 +702,7 @@
             TransitionInfo.Change change, TransitionInfo info, int animHint,
             ArrayList<Animator> animations, Runnable onAnimFinish) {
         final int rootIdx = TransitionUtil.rootIndexFor(change, info);
-        final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext, mSurfaceSession,
+        final ScreenRotationAnimation anim = new ScreenRotationAnimation(mContext,
                 mTransactionPool, startTransaction, change, info.getRoot(rootIdx).getLeash(),
                 animHint);
         // The rotation animation may consist of 3 animations: fade-out screenshot, fade-in real
@@ -918,7 +915,7 @@
         }
 
         final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
-        final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession,
+        final WindowThumbnail wt = WindowThumbnail.createAndAttach(
                 change.getLeash(), thumbnail, transaction);
         final Animation a =
                 mTransitionAnimation.createCrossProfileAppsThumbnailAnimationLocked(bounds);
@@ -943,7 +940,7 @@
             @NonNull Runnable finishCallback, TransitionInfo.Change change,
             TransitionInfo.AnimationOptions options, float cornerRadius) {
         final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
-        final WindowThumbnail wt = WindowThumbnail.createAndAttach(mSurfaceSession,
+        final WindowThumbnail wt = WindowThumbnail.createAndAttach(
                 change.getLeash(), options.getThumbnail(), transaction);
         final Rect bounds = change.getEndAbsBounds();
         final int orientation = mContext.getResources().getConfiguration().orientation;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
index 3d79a1c..c385f9a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
@@ -74,8 +74,12 @@
             final boolean isBackGesture = change.hasFlags(FLAG_BACK_GESTURE_ANIMATED);
             if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) {
                 if (Flags.migratePredictiveBackTransition()) {
-                    if (!isBackGesture && TransitionUtil.isOpenOrCloseMode(mode)) {
-                        notifyHomeVisibilityChanged(TransitionUtil.isOpeningType(mode));
+                    final boolean gestureToHomeTransition = isBackGesture
+                            && TransitionUtil.isClosingType(info.getType());
+                    if (gestureToHomeTransition
+                            || (!isBackGesture && TransitionUtil.isOpenOrCloseMode(mode))) {
+                        notifyHomeVisibilityChanged(gestureToHomeTransition
+                                || TransitionUtil.isOpeningType(mode));
                     }
                 } else {
                     if (TransitionUtil.isOpenOrCloseMode(mode) || isBackGesture) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
index 0bf9d36..5802e2c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
@@ -38,7 +38,6 @@
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
-import android.view.SurfaceSession;
 import android.view.animation.Animation;
 import android.view.animation.AnimationUtils;
 import android.window.ScreenCapture;
@@ -112,7 +111,7 @@
     /** Intensity of light/whiteness of the layout after rotation occurs. */
     private float mEndLuma;
 
-    ScreenRotationAnimation(Context context, SurfaceSession session, TransactionPool pool,
+    ScreenRotationAnimation(Context context, TransactionPool pool,
             Transaction t, TransitionInfo.Change change, SurfaceControl rootLeash, int animHint) {
         mContext = context;
         mTransactionPool = pool;
@@ -126,7 +125,7 @@
         mStartRotation = change.getStartRotation();
         mEndRotation = change.getEndRotation();
 
-        mAnimLeash = new SurfaceControl.Builder(session)
+        mAnimLeash = new SurfaceControl.Builder()
                 .setParent(rootLeash)
                 .setEffectLayer()
                 .setCallsite("ShellRotationAnimation")
@@ -153,7 +152,7 @@
                     return;
                 }
 
-                mScreenshotLayer = new SurfaceControl.Builder(session)
+                mScreenshotLayer = new SurfaceControl.Builder()
                         .setParent(mAnimLeash)
                         .setBLASTLayer()
                         .setSecure(screenshotBuffer.containsSecureLayers())
@@ -178,7 +177,7 @@
             t.setCrop(mSurfaceControl, new Rect(0, 0, mEndWidth, mEndHeight));
 
             if (!isCustomRotate()) {
-                mBackColorSurface = new SurfaceControl.Builder(session)
+                mBackColorSurface = new SurfaceControl.Builder()
                         .setParent(rootLeash)
                         .setColorLayer()
                         .setOpaque(true)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java
index 2c668ed..341f2bc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/WindowThumbnail.java
@@ -21,7 +21,6 @@
 import android.graphics.PixelFormat;
 import android.hardware.HardwareBuffer;
 import android.view.SurfaceControl;
-import android.view.SurfaceSession;
 
 /**
  * Represents a surface that is displayed over a transition surface.
@@ -33,10 +32,10 @@
     private WindowThumbnail() {}
 
     /** Create a thumbnail surface and attach it over a parent surface. */
-    static WindowThumbnail createAndAttach(SurfaceSession surfaceSession, SurfaceControl parent,
+    static WindowThumbnail createAndAttach(SurfaceControl parent,
             HardwareBuffer thumbnailHeader, SurfaceControl.Transaction t) {
         WindowThumbnail windowThumbnail = new WindowThumbnail();
-        windowThumbnail.mSurfaceControl = new SurfaceControl.Builder(surfaceSession)
+        windowThumbnail.mSurfaceControl = new SurfaceControl.Builder()
                 .setParent(parent)
                 .setName("WindowThumanil : " + parent.toString())
                 .setCallsite("WindowThumanil")
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
index fd6c4d8..fb81ed4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
@@ -30,7 +30,6 @@
 import android.view.LayoutInflater
 import android.view.SurfaceControl
 import android.view.SurfaceControlViewHost
-import android.view.SurfaceSession
 import android.view.WindowManager
 import android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL
 import android.view.WindowlessWindowManager
@@ -66,7 +65,6 @@
     private val lightColors = dynamicLightColorScheme(context)
     private val darkColors = dynamicDarkColorScheme(context)
 
-    private val surfaceSession = SurfaceSession()
     private lateinit var iconView: ImageView
     private var iconSize = 0
 
@@ -126,7 +124,7 @@
                 .setCallsite("ResizeVeil#setupResizeVeil")
                 .build()
         backgroundSurface = surfaceControlBuilderFactory
-                .create("Resize veil background of Task=" + taskInfo.taskId, surfaceSession)
+                .create("Resize veil background of Task=" + taskInfo.taskId)
                 .setColorLayer()
                 .setHidden(true)
                 .setParent(veilSurface)
@@ -399,10 +397,6 @@
         fun create(name: String): SurfaceControl.Builder {
             return SurfaceControl.Builder().setName(name)
         }
-
-        fun create(name: String, surfaceSession: SurfaceSession): SurfaceControl.Builder {
-            return SurfaceControl.Builder(surfaceSession).setName(name)
-        }
     }
 
     companion object {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
index dfa5ab4..9ef4b8c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
@@ -29,6 +29,7 @@
 import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
 import android.view.WindowManager
 import android.widget.ImageButton
+import com.android.internal.policy.SystemBarUtils
 import com.android.window.flags.Flags
 import com.android.wm.shell.R
 import com.android.wm.shell.shared.animation.Interpolators
@@ -74,7 +75,10 @@
     ) {
         captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo))
         this.taskInfo = taskInfo
-        if (!isCaptionVisible && hasStatusBarInputLayer()) {
+        // If handle is not in status bar region(i.e., bottom stage in vertical split),
+        // do not create an input layer
+        if (position.y >= SystemBarUtils.getStatusBarHeight(context)) return
+        if (!isCaptionVisible && hasStatusBarInputLayer() ) {
             disposeStatusBarInputLayer()
             return
         }
@@ -120,7 +124,7 @@
                 inputManager.pilferPointers(v.viewRootImpl.inputToken)
             }
             captionHandle.dispatchTouchEvent(event)
-            true
+            return@setOnTouchListener true
         }
         windowManagerWrapper.updateViewLayout(view, lp)
     }
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
index 426f40b..a54d497 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindow.kt
@@ -17,7 +17,6 @@
 package com.android.wm.shell.scenarios
 
 import android.app.Instrumentation
-import android.platform.test.annotations.Postsubmit
 import android.tools.NavBar
 import android.tools.Rotation
 import android.tools.flicker.rules.ChangeDisplayOrientationRule
@@ -33,15 +32,12 @@
 import org.junit.After
 import org.junit.Assume
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.BlockJUnit4ClassRunner
 
-@RunWith(BlockJUnit4ClassRunner::class)
-@Postsubmit
-open class MaximizeAppWindow
-@JvmOverloads
+@Ignore("Test Base Class")
+abstract class MaximizeAppWindow
 constructor(private val rotation: Rotation = Rotation.ROTATION_0, isResizable: Boolean = true) {
 
     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index 413e495..e514dc3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -49,7 +49,6 @@
 import android.os.RemoteException;
 import android.util.SparseArray;
 import android.view.SurfaceControl;
-import android.view.SurfaceSession;
 import android.window.ITaskOrganizer;
 import android.window.ITaskOrganizerController;
 import android.window.TaskAppearedInfo;
@@ -169,7 +168,7 @@
     public void testTaskLeashReleasedAfterVanished() throws RemoteException {
         assumeFalse(ENABLE_SHELL_TRANSITIONS);
         RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
-        SurfaceControl taskLeash = new SurfaceControl.Builder(new SurfaceSession())
+        SurfaceControl taskLeash = new SurfaceControl.Builder()
                 .setName("task").build();
         mOrganizer.registerOrganizer();
         mOrganizer.onTaskAppeared(taskInfo, taskLeash);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
index 751275b..66dcef6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java
@@ -24,7 +24,6 @@
 import android.graphics.Rect;
 import android.os.Handler;
 import android.view.SurfaceControl;
-import android.view.SurfaceSession;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.wm.shell.ShellTaskOrganizer;
@@ -89,7 +88,7 @@
 
             // Prepare root task for testing.
             mRootTask = new TestRunningTaskInfoBuilder().build();
-            mRootLeash = new SurfaceControl.Builder(new SurfaceSession()).setName("test").build();
+            mRootLeash = new SurfaceControl.Builder().setName("test").build();
             onTaskAppeared(mRootTask, mRootLeash);
         }
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index af288c8..ce3944a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -53,7 +53,6 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.view.SurfaceControl;
-import android.view.SurfaceSession;
 import android.window.IRemoteTransition;
 import android.window.RemoteTransition;
 import android.window.TransitionInfo;
@@ -106,7 +105,6 @@
     @Mock private DisplayInsetsController mDisplayInsetsController;
     @Mock private TransactionPool mTransactionPool;
     @Mock private Transitions mTransitions;
-    @Mock private SurfaceSession mSurfaceSession;
     @Mock private IconProvider mIconProvider;
     @Mock private WindowDecorViewModel mWindowDecorViewModel;
     @Mock private ShellExecutor mMainExecutor;
@@ -134,11 +132,11 @@
         doReturn(mock(SurfaceControl.Transaction.class)).when(mTransactionPool).acquire();
         mSplitLayout = SplitTestUtils.createMockSplitLayout();
         mMainStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
-                StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
+                StageTaskListener.StageListenerCallbacks.class), mSyncQueue,
                 mIconProvider, Optional.of(mWindowDecorViewModel)));
         mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
         mSideStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock(
-                StageTaskListener.StageListenerCallbacks.class), mSyncQueue, mSurfaceSession,
+                StageTaskListener.StageListenerCallbacks.class), mSyncQueue,
                 mIconProvider, Optional.of(mWindowDecorViewModel)));
         mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface());
         mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index ce343b8..a6c16c4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -50,7 +50,6 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.view.SurfaceControl;
-import android.view.SurfaceSession;
 import android.window.RemoteTransition;
 import android.window.WindowContainerTransaction;
 
@@ -119,7 +118,6 @@
     private final Rect mBounds2 = new Rect(5, 10, 15, 20);
     private final Rect mRootBounds = new Rect(0, 0, 45, 60);
 
-    private SurfaceSession mSurfaceSession = new SurfaceSession();
     private SurfaceControl mRootLeash;
     private SurfaceControl mDividerLeash;
     private ActivityManager.RunningTaskInfo mRootTask;
@@ -139,7 +137,7 @@
                 mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool,
                 mMainExecutor, mMainHandler, Optional.empty(), mLaunchAdjacentController,
                 Optional.empty()));
-        mDividerLeash = new SurfaceControl.Builder(mSurfaceSession).setName("fakeDivider").build();
+        mDividerLeash = new SurfaceControl.Builder().setName("fakeDivider").build();
 
         when(mSplitLayout.getBounds1()).thenReturn(mBounds1);
         when(mSplitLayout.getBounds2()).thenReturn(mBounds2);
@@ -149,7 +147,7 @@
         when(mSplitLayout.getDividerLeash()).thenReturn(mDividerLeash);
 
         mRootTask = new TestRunningTaskInfoBuilder().build();
-        mRootLeash = new SurfaceControl.Builder(mSurfaceSession).setName("test").build();
+        mRootLeash = new SurfaceControl.Builder().setName("test").build();
         mStageCoordinator.onTaskAppeared(mRootTask, mRootLeash);
 
         mSideStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
index 8b5cb97..b7b7d0d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java
@@ -32,7 +32,6 @@
 import android.app.ActivityManager;
 import android.os.SystemProperties;
 import android.view.SurfaceControl;
-import android.view.SurfaceSession;
 import android.window.WindowContainerTransaction;
 
 import androidx.test.annotation.UiThreadTest;
@@ -82,7 +81,6 @@
     private WindowContainerTransaction mWct;
     @Captor
     private ArgumentCaptor<SyncTransactionQueue.TransactionRunnable> mRunnableCaptor;
-    private SurfaceSession mSurfaceSession = new SurfaceSession();
     private SurfaceControl mSurfaceControl;
     private ActivityManager.RunningTaskInfo mRootTask;
     private StageTaskListener mStageTaskListener;
@@ -97,12 +95,11 @@
                 DEFAULT_DISPLAY,
                 mCallbacks,
                 mSyncQueue,
-                mSurfaceSession,
                 mIconProvider,
                 Optional.of(mWindowDecorViewModel));
         mRootTask = new TestRunningTaskInfoBuilder().build();
         mRootTask.parentTaskId = INVALID_TASK_ID;
-        mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession).setName("test").build();
+        mSurfaceControl = new SurfaceControl.Builder().setName("test").build();
         mStageTaskListener.onTaskAppeared(mRootTask, mSurfaceControl);
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
index 1984885..17fd95b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
@@ -49,7 +49,6 @@
 import android.testing.TestableLooper;
 import android.view.SurfaceControl;
 import android.view.SurfaceHolder;
-import android.view.SurfaceSession;
 import android.view.ViewTreeObserver;
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
@@ -95,7 +94,6 @@
     Looper mViewLooper;
     TestHandler mViewHandler;
 
-    SurfaceSession mSession;
     SurfaceControl mLeash;
 
     Context mContext;
@@ -106,7 +104,7 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mLeash = new SurfaceControl.Builder(mSession)
+        mLeash = new SurfaceControl.Builder()
                 .setName("test")
                 .build();
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
index f51a960..8f49de0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
@@ -20,6 +20,7 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
 
@@ -39,6 +40,7 @@
 import android.os.Looper;
 import android.os.RemoteException;
 import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.view.SurfaceControl;
@@ -213,6 +215,35 @@
         verify(mListener, times(1)).onHomeVisibilityChanged(true);
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_MIGRATE_PREDICTIVE_BACK_TRANSITION)
+    public void testHomeActivityWithBackGestureNotifiesHomeIsVisibleAfterClose()
+            throws RemoteException {
+        TransitionInfo info = mock(TransitionInfo.class);
+        TransitionInfo.Change change = mock(TransitionInfo.Change.class);
+        ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class);
+        when(change.getTaskInfo()).thenReturn(taskInfo);
+        when(info.getChanges()).thenReturn(new ArrayList<>(List.of(change)));
+        when(info.getType()).thenReturn(TRANSIT_PREPARE_BACK_NAVIGATION);
+
+        when(change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)).thenReturn(true);
+        setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_OPEN, true);
+
+        mHomeTransitionObserver.onTransitionReady(mock(IBinder.class),
+                info,
+                mock(SurfaceControl.Transaction.class),
+                mock(SurfaceControl.Transaction.class));
+        verify(mListener, times(0)).onHomeVisibilityChanged(anyBoolean());
+
+        when(info.getType()).thenReturn(TRANSIT_TO_BACK);
+        setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_CHANGE, true);
+        mHomeTransitionObserver.onTransitionReady(mock(IBinder.class),
+                info,
+                mock(SurfaceControl.Transaction.class),
+                mock(SurfaceControl.Transaction.class));
+        verify(mListener, times(1)).onHomeVisibilityChanged(true);
+    }
+
     /**
      * Helper class to initialize variables for the rest.
      */
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt
index a07be79..e0d16aa 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt
@@ -97,7 +97,7 @@
             .thenReturn(spyResizeVeilSurfaceBuilder)
         doReturn(mockResizeVeilSurface).whenever(spyResizeVeilSurfaceBuilder).build()
         whenever(mockSurfaceControlBuilderFactory
-            .create(eq("Resize veil background of Task=" + taskInfo.taskId), any()))
+            .create(eq("Resize veil background of Task=" + taskInfo.taskId)))
             .thenReturn(spyBackgroundSurfaceBuilder)
         doReturn(mockBackgroundSurface).whenever(spyBackgroundSurfaceBuilder).build()
         whenever(mockSurfaceControlBuilderFactory
diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
index ff09084..c4173ed 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
@@ -460,7 +460,7 @@
 
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
-        if (keyCode == KeyEvent.KEYCODE_BACK) {
+        if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
             event.startTracking();
             return true;
         }
@@ -479,7 +479,7 @@
             return true;
         }
 
-        if (keyCode == KeyEvent.KEYCODE_BACK
+        if ((keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE)
                 && event.isTracking() && !event.isCanceled()) {
             if (mPrintPreviewController != null && mPrintPreviewController.isOptionsOpened()
                     && !hasErrors()) {
diff --git a/packages/SettingsLib/Metadata/Android.bp b/packages/SettingsLib/Metadata/Android.bp
new file mode 100644
index 0000000..207637f
--- /dev/null
+++ b/packages/SettingsLib/Metadata/Android.bp
@@ -0,0 +1,23 @@
+package {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+    name: "SettingsLibMetadata-srcs",
+    srcs: ["src/**/*.kt"],
+}
+
+android_library {
+    name: "SettingsLibMetadata",
+    defaults: [
+        "SettingsLintDefaults",
+    ],
+    srcs: [":SettingsLibMetadata-srcs"],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.fragment_fragment",
+        "guava",
+        "SettingsLibDataStore",
+    ],
+    kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SettingsLib/Metadata/AndroidManifest.xml b/packages/SettingsLib/Metadata/AndroidManifest.xml
new file mode 100644
index 0000000..1c801e6
--- /dev/null
+++ b/packages/SettingsLib/Metadata/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.settingslib.metadata">
+
+    <uses-sdk android:minSdkVersion="21" />
+</manifest>
diff --git a/packages/SettingsLib/Metadata/processor/Android.bp b/packages/SettingsLib/Metadata/processor/Android.bp
new file mode 100644
index 0000000..d8acc76
--- /dev/null
+++ b/packages/SettingsLib/Metadata/processor/Android.bp
@@ -0,0 +1,11 @@
+package {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_plugin {
+    name: "SettingsLibMetadata-processor",
+    srcs: ["src/**/*.kt"],
+    processor_class: "com.android.settingslib.metadata.PreferenceScreenAnnotationProcessor",
+    java_resource_dirs: ["resources"],
+    visibility: ["//visibility:public"],
+}
diff --git a/packages/SettingsLib/Metadata/processor/resources/META-INF/services/javax.annotation.processing.Processor b/packages/SettingsLib/Metadata/processor/resources/META-INF/services/javax.annotation.processing.Processor
new file mode 100644
index 0000000..762a01a
--- /dev/null
+++ b/packages/SettingsLib/Metadata/processor/resources/META-INF/services/javax.annotation.processing.Processor
@@ -0,0 +1 @@
+com.android.settingslib.metadata.PreferenceScreenAnnotationProcessor
\ No newline at end of file
diff --git a/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt
new file mode 100644
index 0000000..620d717
--- /dev/null
+++ b/packages/SettingsLib/Metadata/processor/src/com/android/settingslib/metadata/PreferenceScreenAnnotationProcessor.kt
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import java.util.TreeMap
+import javax.annotation.processing.AbstractProcessor
+import javax.annotation.processing.ProcessingEnvironment
+import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.SourceVersion
+import javax.lang.model.element.AnnotationMirror
+import javax.lang.model.element.AnnotationValue
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.lang.model.type.TypeMirror
+import javax.tools.Diagnostic
+
+/** Processor to gather preference screens annotated with `@ProvidePreferenceScreen`. */
+class PreferenceScreenAnnotationProcessor : AbstractProcessor() {
+    private val screens = TreeMap<String, ConstructorType>()
+    private val overlays = mutableMapOf<String, String>()
+    private val contextType: TypeMirror by lazy {
+        processingEnv.elementUtils.getTypeElement("android.content.Context").asType()
+    }
+
+    private var options: Map<String, Any?>? = null
+    private lateinit var annotationElement: TypeElement
+    private lateinit var optionsElement: TypeElement
+    private lateinit var screenType: TypeMirror
+
+    override fun getSupportedAnnotationTypes() = setOf(ANNOTATION, OPTIONS)
+
+    override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()
+
+    override fun init(processingEnv: ProcessingEnvironment) {
+        super.init(processingEnv)
+        val elementUtils = processingEnv.elementUtils
+        annotationElement = elementUtils.getTypeElement(ANNOTATION)
+        optionsElement = elementUtils.getTypeElement(OPTIONS)
+        screenType = elementUtils.getTypeElement("$PACKAGE.$PREFERENCE_SCREEN_METADATA").asType()
+    }
+
+    override fun process(
+        annotations: MutableSet<out TypeElement>,
+        roundEnv: RoundEnvironment,
+    ): Boolean {
+        roundEnv.getElementsAnnotatedWith(optionsElement).singleOrNull()?.run {
+            if (options != null) error("@$OPTIONS_NAME is already specified: $options", this)
+            options =
+                annotationMirrors
+                    .single { it.isElement(optionsElement) }
+                    .elementValues
+                    .entries
+                    .associate { it.key.simpleName.toString() to it.value.value }
+        }
+        for (element in roundEnv.getElementsAnnotatedWith(annotationElement)) {
+            (element as? TypeElement)?.process()
+        }
+        if (roundEnv.processingOver()) codegen()
+        return false
+    }
+
+    private fun TypeElement.process() {
+        if (kind != ElementKind.CLASS || modifiers.contains(Modifier.ABSTRACT)) {
+            error("@$ANNOTATION_NAME must be added to non abstract class", this)
+            return
+        }
+        if (!processingEnv.typeUtils.isAssignable(asType(), screenType)) {
+            error("@$ANNOTATION_NAME must be added to $PREFERENCE_SCREEN_METADATA subclass", this)
+            return
+        }
+        val constructorType = getConstructorType()
+        if (constructorType == null) {
+            error(
+                "Class must be an object, or has single public constructor that " +
+                    "accepts no parameter or a Context parameter",
+                this,
+            )
+            return
+        }
+        val screenQualifiedName = qualifiedName.toString()
+        screens[screenQualifiedName] = constructorType
+        val annotation = annotationMirrors.single { it.isElement(annotationElement) }
+        val overlay = annotation.getOverlay()
+        if (overlay != null) {
+            overlays.put(overlay, screenQualifiedName)?.let {
+                error("$overlay has been overlaid by $it", this)
+            }
+        }
+    }
+
+    private fun codegen() {
+        val collector = (options?.get("codegenCollector") as? String) ?: DEFAULT_COLLECTOR
+        if (collector.isEmpty()) return
+        val parts = collector.split('/')
+        if (parts.size == 3) {
+            generateCode(parts[0], parts[1], parts[2])
+        } else {
+            throw IllegalArgumentException(
+                "Collector option '$collector' does not follow 'PKG/CLASS/METHOD' format"
+            )
+        }
+    }
+
+    private fun generateCode(outputPkg: String, outputClass: String, outputFun: String) {
+        for ((overlay, screen) in overlays) {
+            if (screens.remove(overlay) == null) {
+                warn("$overlay is overlaid by $screen but not annotated with @$ANNOTATION_NAME")
+            } else {
+                processingEnv.messager.printMessage(
+                    Diagnostic.Kind.NOTE,
+                    "$overlay is overlaid by $screen",
+                )
+            }
+        }
+        processingEnv.filer.createSourceFile("$outputPkg.$outputClass").openWriter().use {
+            it.write("package $outputPkg;\n\n")
+            it.write("import $PACKAGE.$PREFERENCE_SCREEN_METADATA;\n\n")
+            it.write("// Generated by annotation processor for @$ANNOTATION_NAME\n")
+            it.write("public final class $outputClass {\n")
+            it.write("  private $outputClass() {}\n\n")
+            it.write(
+                "  public static java.util.List<$PREFERENCE_SCREEN_METADATA> " +
+                    "$outputFun(android.content.Context context) {\n"
+            )
+            it.write(
+                "    java.util.ArrayList<$PREFERENCE_SCREEN_METADATA> screens = " +
+                    "new java.util.ArrayList<>(${screens.size});\n"
+            )
+            for ((screen, constructorType) in screens) {
+                when (constructorType) {
+                    ConstructorType.DEFAULT -> it.write("    screens.add(new $screen());\n")
+                    ConstructorType.CONTEXT -> it.write("    screens.add(new $screen(context));\n")
+                    ConstructorType.SINGLETON -> it.write("    screens.add($screen.INSTANCE);\n")
+                }
+            }
+            for ((overlay, screen) in overlays) {
+                it.write("    // $overlay is overlaid by $screen\n")
+            }
+            it.write("    return screens;\n")
+            it.write("  }\n")
+            it.write("}")
+        }
+    }
+
+    private fun AnnotationMirror.isElement(element: TypeElement) =
+        processingEnv.typeUtils.isSameType(annotationType.asElement().asType(), element.asType())
+
+    private fun AnnotationMirror.getOverlay(): String? {
+        for ((key, value) in elementValues) {
+            if (key.simpleName.contentEquals("overlay")) {
+                return if (value.isDefaultClassValue(key)) null else value.value.toString()
+            }
+        }
+        return null
+    }
+
+    private fun AnnotationValue.isDefaultClassValue(key: ExecutableElement) =
+        processingEnv.typeUtils.isSameType(
+            value as TypeMirror,
+            key.defaultValue.value as TypeMirror,
+        )
+
+    private fun TypeElement.getConstructorType(): ConstructorType? {
+        var constructor: ExecutableElement? = null
+        for (element in enclosedElements) {
+            if (element.isKotlinObject()) return ConstructorType.SINGLETON
+            if (element.kind != ElementKind.CONSTRUCTOR) continue
+            if (!element.modifiers.contains(Modifier.PUBLIC)) continue
+            if (constructor != null) return null
+            constructor = element as ExecutableElement
+        }
+        return constructor?.parameters?.run {
+            when {
+                isEmpty() -> ConstructorType.DEFAULT
+                size == 1 && processingEnv.typeUtils.isSameType(this[0].asType(), contextType) ->
+                    ConstructorType.CONTEXT
+                else -> null
+            }
+        }
+    }
+
+    private fun Element.isKotlinObject() =
+        kind == ElementKind.FIELD &&
+            modifiers.run { contains(Modifier.PUBLIC) && contains(Modifier.STATIC) } &&
+            simpleName.toString() == "INSTANCE"
+
+    private fun warn(msg: CharSequence) =
+        processingEnv.messager.printMessage(Diagnostic.Kind.WARNING, msg)
+
+    private fun error(msg: CharSequence, element: Element) =
+        processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, msg, element)
+
+    private enum class ConstructorType {
+        DEFAULT, // default constructor with no parameter
+        CONTEXT, // constructor with a Context parameter
+        SINGLETON, // Kotlin object class
+    }
+
+    companion object {
+        private const val PACKAGE = "com.android.settingslib.metadata"
+        private const val ANNOTATION_NAME = "ProvidePreferenceScreen"
+        private const val ANNOTATION = "$PACKAGE.$ANNOTATION_NAME"
+        private const val PREFERENCE_SCREEN_METADATA = "PreferenceScreenMetadata"
+
+        private const val OPTIONS_NAME = "ProvidePreferenceScreenOptions"
+        private const val OPTIONS = "$PACKAGE.$OPTIONS_NAME"
+        private const val DEFAULT_COLLECTOR = "$PACKAGE/PreferenceScreenCollector/get"
+    }
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt
new file mode 100644
index 0000000..ea20a74
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/Annotations.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import kotlin.reflect.KClass
+
+/**
+ * Annotation to provide preference screen.
+ *
+ * The annotated class must satisfy either condition:
+ * - the primary constructor has no parameter
+ * - the primary constructor has a single [android.content.Context] parameter
+ * - it is a Kotlin object class
+ *
+ * @param overlay if specified, current annotated screen will overlay the given screen
+ */
+@Retention(AnnotationRetention.SOURCE)
+@Target(AnnotationTarget.CLASS)
+@MustBeDocumented
+annotation class ProvidePreferenceScreen(
+    val overlay: KClass<out PreferenceScreenMetadata> = PreferenceScreenMetadata::class,
+)
+
+/**
+ * Provides options for [ProvidePreferenceScreen] annotation processor.
+ *
+ * @param codegenCollector generated collector class (format: "pkg/class/method"), an empty string
+ *   means do not generate code
+ */
+@Retention(AnnotationRetention.SOURCE)
+@Target(AnnotationTarget.CLASS)
+@MustBeDocumented
+annotation class ProvidePreferenceScreenOptions(
+    val codegenCollector: String = "com.android.settingslib.metadata/PreferenceScreenCollector/get",
+)
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
new file mode 100644
index 0000000..51a8580
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import android.content.Context
+import androidx.annotation.ArrayRes
+import androidx.annotation.IntDef
+import com.android.settingslib.datastore.KeyValueStore
+
+/** Permit of read and write request. */
+@IntDef(
+    ReadWritePermit.ALLOW,
+    ReadWritePermit.DISALLOW,
+    ReadWritePermit.REQUIRE_APP_PERMISSION,
+    ReadWritePermit.REQUIRE_USER_AGREEMENT,
+)
+@Retention(AnnotationRetention.SOURCE)
+annotation class ReadWritePermit {
+    companion object {
+        /** Allow to read/write value. */
+        const val ALLOW = 0
+        /** Disallow to read/write value (e.g. uid not allowed). */
+        const val DISALLOW = 1
+        /** Require (runtime/special) app permission from user explicitly. */
+        const val REQUIRE_APP_PERMISSION = 2
+        /** Require explicit user agreement (e.g. terms of service). */
+        const val REQUIRE_USER_AGREEMENT = 3
+    }
+}
+
+/** Preference interface that has a value persisted in datastore. */
+interface PersistentPreference<T> {
+
+    /**
+     * Returns the key-value storage of the preference.
+     *
+     * The default implementation returns the storage provided by
+     * [PreferenceScreenRegistry.getKeyValueStore].
+     */
+    fun storage(context: Context): KeyValueStore =
+        PreferenceScreenRegistry.getKeyValueStore(context, this as PreferenceMetadata)!!
+
+    /**
+     * Returns if the external application (identified by [callingUid]) has permission to read
+     * preference value.
+     *
+     * The underlying implementation does NOT need to check common states like isEnabled,
+     * isRestricted or isAvailable.
+     */
+    @ReadWritePermit
+    fun getReadPermit(context: Context, myUid: Int, callingUid: Int): Int =
+        PreferenceScreenRegistry.getReadPermit(
+            context,
+            myUid,
+            callingUid,
+            this as PreferenceMetadata,
+        )
+
+    /**
+     * Returns if the external application (identified by [callingUid]) has permission to write
+     * preference with given [value].
+     *
+     * The underlying implementation does NOT need to check common states like isEnabled,
+     * isRestricted or isAvailable.
+     */
+    @ReadWritePermit
+    fun getWritePermit(context: Context, value: T?, myUid: Int, callingUid: Int): Int =
+        PreferenceScreenRegistry.getWritePermit(
+            context,
+            value,
+            myUid,
+            callingUid,
+            this as PreferenceMetadata,
+        )
+}
+
+/** Descriptor of values. */
+sealed interface ValueDescriptor {
+
+    /** Returns if given value (represented by index) is valid. */
+    fun isValidValue(context: Context, index: Int): Boolean
+}
+
+/**
+ * A boolean type value.
+ *
+ * A zero value means `False`, otherwise it is `True`.
+ */
+interface BooleanValue : ValueDescriptor {
+    override fun isValidValue(context: Context, index: Int) = true
+}
+
+/** Value falls into a given array. */
+interface DiscreteValue<T> : ValueDescriptor {
+    @get:ArrayRes val values: Int
+
+    @get:ArrayRes val valuesDescription: Int
+
+    fun getValue(context: Context, index: Int): T
+}
+
+/**
+ * Value falls into a text array, whose element is [CharSequence] type.
+ *
+ * [values] resource is `<string-array>`.
+ */
+interface DiscreteTextValue : DiscreteValue<CharSequence> {
+    override fun isValidValue(context: Context, index: Int): Boolean {
+        if (index < 0) return false
+        return index < context.resources.getTextArray(values).size
+    }
+
+    override fun getValue(context: Context, index: Int): CharSequence =
+        context.resources.getTextArray(values)[index]
+}
+
+/**
+ * Value falls into a string array, whose element is [String] type.
+ *
+ * [values] resource is `<string-array>`.
+ */
+interface DiscreteStringValue : DiscreteValue<String> {
+    override fun isValidValue(context: Context, index: Int): Boolean {
+        if (index < 0) return false
+        return index < context.resources.getStringArray(values).size
+    }
+
+    override fun getValue(context: Context, index: Int): String =
+        context.resources.getStringArray(values)[index]
+}
+
+/**
+ * Value falls into an integer array.
+ *
+ * [values] resource is `<integer-array>`.
+ */
+interface DiscreteIntValue : DiscreteValue<Int> {
+    override fun isValidValue(context: Context, index: Int): Boolean {
+        if (index < 0) return false
+        return index < context.resources.getIntArray(values).size
+    }
+
+    override fun getValue(context: Context, index: Int): Int =
+        context.resources.getIntArray(values)[index]
+}
+
+/** Value is between a range. */
+interface RangeValue : ValueDescriptor {
+    /** The lower bound (inclusive) of the range. */
+    val minValue: Int
+
+    /** The upper bound (inclusive) of the range. */
+    val maxValue: Int
+
+    /** The increment step within the range. 0 means unset, which implies step size is 1. */
+    val incrementStep: Int
+        get() = 0
+
+    override fun isValidValue(context: Context, index: Int) = index in minValue..maxValue
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
new file mode 100644
index 0000000..4503738
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceHierarchy.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+/** A node in preference hierarchy that is associated with [PreferenceMetadata]. */
+open class PreferenceHierarchyNode internal constructor(val metadata: PreferenceMetadata)
+
+/**
+ * Preference hierarchy describes the structure of preferences recursively.
+ *
+ * A root hierarchy represents a preference screen. A sub-hierarchy represents a preference group.
+ */
+class PreferenceHierarchy internal constructor(metadata: PreferenceMetadata) :
+    PreferenceHierarchyNode(metadata) {
+
+    private val children = mutableListOf<PreferenceHierarchyNode>()
+
+    /** Adds a preference to the hierarchy. */
+    operator fun PreferenceMetadata.unaryPlus() {
+        children.add(PreferenceHierarchyNode(this))
+    }
+
+    /**
+     * Adds preference screen with given key (as a placeholder) to the hierarchy.
+     *
+     * This is mainly to support Android Settings overlays. OEMs might want to custom some of the
+     * screens. In resource-based hierarchy, it leverages the resource overlay. In terms of DSL or
+     * programmatic hierarchy, it will be a problem to specify concrete screen metadata objects.
+     * Instead, use preference screen key as a placeholder in the hierarchy and screen metadata will
+     * be looked up from [PreferenceScreenRegistry] lazily at runtime.
+     *
+     * @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry]
+     */
+    operator fun String.unaryPlus() {
+        children.add(PreferenceHierarchyNode(PreferenceScreenRegistry[this]!!))
+    }
+
+    /** Adds a preference to the hierarchy. */
+    fun add(metadata: PreferenceMetadata) {
+        children.add(PreferenceHierarchyNode(metadata))
+    }
+
+    /** Adds a preference group to the hierarchy. */
+    operator fun PreferenceGroup.unaryPlus() = PreferenceHierarchy(this).also { children.add(it) }
+
+    /** Adds a preference group and returns its preference hierarchy. */
+    fun addGroup(metadata: PreferenceGroup): PreferenceHierarchy =
+        PreferenceHierarchy(metadata).also { children.add(it) }
+
+    /**
+     * Adds preference screen with given key (as a placeholder) to the hierarchy.
+     *
+     * This is mainly to support Android Settings overlays. OEMs might want to custom some of the
+     * screens. In resource-based hierarchy, it leverages the resource overlay. In terms of DSL or
+     * programmatic hierarchy, it will be a problem to specify concrete screen metadata objects.
+     * Instead, use preference screen key as a placeholder in the hierarchy and screen metadata will
+     * be looked up from [PreferenceScreenRegistry] lazily at runtime.
+     *
+     * @throws NullPointerException if screen is not registered to [PreferenceScreenRegistry]
+     */
+    fun addPreferenceScreen(screenKey: String) {
+        children.add(PreferenceHierarchy(PreferenceScreenRegistry[screenKey]!!))
+    }
+
+    /** Extensions to add more preferences to the hierarchy. */
+    operator fun plusAssign(init: PreferenceHierarchy.() -> Unit) = init(this)
+
+    /** Traversals preference hierarchy and applies given action. */
+    fun forEach(action: (PreferenceHierarchyNode) -> Unit) {
+        for (it in children) action(it)
+    }
+
+    /** Traversals preference hierarchy and applies given action. */
+    suspend fun forEachAsync(action: suspend (PreferenceHierarchyNode) -> Unit) {
+        for (it in children) action(it)
+    }
+
+    /** Finds the [PreferenceMetadata] object associated with given key. */
+    fun find(key: String): PreferenceMetadata? {
+        if (metadata.key == key) return metadata
+        for (child in children) {
+            if (child is PreferenceHierarchy) {
+                val result = child.find(key)
+                if (result != null) return result
+            } else {
+                if (child.metadata.key == key) return child.metadata
+            }
+        }
+        return null
+    }
+
+    /** Returns all the [PreferenceMetadata]s appear in the hierarchy. */
+    fun getAllPreferences(): List<PreferenceMetadata> =
+        mutableListOf<PreferenceMetadata>().also { getAllPreferences(it) }
+
+    private fun getAllPreferences(result: MutableList<PreferenceMetadata>) {
+        result.add(metadata)
+        for (child in children) {
+            if (child is PreferenceHierarchy) {
+                child.getAllPreferences(result)
+            } else {
+                result.add(child.metadata)
+            }
+        }
+    }
+}
+
+/**
+ * Builder function to create [PreferenceHierarchy] in
+ * [DSL](https://kotlinlang.org/docs/type-safe-builders.html) manner.
+ */
+fun preferenceHierarchy(metadata: PreferenceMetadata, init: PreferenceHierarchy.() -> Unit) =
+    PreferenceHierarchy(metadata).also(init)
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
new file mode 100644
index 0000000..f39f3a0
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import androidx.annotation.AnyThread
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+
+/**
+ * Interface provides preference metadata (title, summary, icon, etc.).
+ *
+ * Besides the existing APIs, subclass could integrate with following interface to provide more
+ * information:
+ * - [PreferenceTitleProvider]: provide dynamic title content
+ * - [PreferenceSummaryProvider]: provide dynamic summary content (e.g. based on preference value)
+ * - [PreferenceAvailabilityProvider]: provide preference availability (e.g. based on flag)
+ * - [PreferenceLifecycleProvider]: provide the lifecycle callbacks and notify state change
+ *
+ * Notes:
+ * - UI framework support:
+ *     - This class does not involve any UI logic, it is the data layer.
+ *     - Subclass could integrate with datastore and UI widget to provide UI layer. For instance,
+ *       `PreferenceBinding` supports Jetpack Preference binding.
+ * - Datastore:
+ *     - Subclass should implement the [PersistentPreference] to note that current preference is
+ *       persistent in datastore.
+ *     - It is always recommended to support back up preference value changed by user. Typically,
+ *       the back up and restore happen within datastore, the [allowBackup] API is to mark if
+ *       current preference value should be backed up (backup allowed by default).
+ * - Preference indexing for search:
+ *     - Override [isIndexable] API to mark if preference is indexable (enabled by default).
+ *     - If [isIndexable] returns true, preference title and summary will be indexed with cache.
+ *       More indexing data could be provided through [keywords].
+ *     - Settings search will cache the preference title/summary/keywords for indexing. The cache is
+ *       invalidated when system locale changed, app upgraded, etc.
+ *     - Dynamic content is not suitable to be cached for indexing. Subclass that implements
+ *       [PreferenceTitleProvider] / [PreferenceSummaryProvider] will not have its title / summary
+ *       indexed.
+ */
+@AnyThread
+interface PreferenceMetadata {
+
+    /** Preference key. */
+    val key: String
+
+    /**
+     * Preference title resource id.
+     *
+     * Implement [PreferenceTitleProvider] if title is generated dynamically.
+     */
+    val title: Int
+        @StringRes get() = 0
+
+    /**
+     * Preference summary resource id.
+     *
+     * Implement [PreferenceSummaryProvider] if summary is generated dynamically (e.g. summary is
+     * provided per preference value)
+     */
+    val summary: Int
+        @StringRes get() = 0
+
+    /** Icon of the preference. */
+    val icon: Int
+        @DrawableRes get() = 0
+
+    /** Additional keywords for indexing. */
+    val keywords: Int
+        @StringRes get() = 0
+
+    /**
+     * Return the extras Bundle object associated with this preference.
+     *
+     * It is used to provide more information for metadata.
+     */
+    fun extras(context: Context): Bundle? = null
+
+    /**
+     * Returns if preference is indexable, default value is `true`.
+     *
+     * Return `false` only when the preference is always unavailable on current device. If it is
+     * conditional available, override [PreferenceAvailabilityProvider].
+     */
+    fun isIndexable(context: Context): Boolean = true
+
+    /**
+     * Returns if preference is enabled.
+     *
+     * UI framework normally does not allow user to interact with the preference widget when it is
+     * disabled.
+     *
+     * [dependencyOfEnabledState] is provided to support dependency, the [shouldDisableDependents]
+     * value of dependent preference is used to decide enabled state.
+     */
+    fun isEnabled(context: Context): Boolean {
+        val dependency = dependencyOfEnabledState(context) ?: return true
+        return !dependency.shouldDisableDependents(context)
+    }
+
+    /** Returns the key of depended preference to decide the enabled state. */
+    fun dependencyOfEnabledState(context: Context): PreferenceMetadata? = null
+
+    /** Returns whether this preference's dependents should be disabled. */
+    fun shouldDisableDependents(context: Context): Boolean = !isEnabled(context)
+
+    /** Returns if the preference is persistent in datastore. */
+    fun isPersistent(context: Context): Boolean = this is PersistentPreference<*>
+
+    /**
+     * Returns if preference value backup is allowed (by default returns `true` if preference is
+     * persistent).
+     */
+    fun allowBackup(context: Context): Boolean = isPersistent(context)
+
+    /** Returns preference intent. */
+    fun intent(context: Context): Intent? = null
+
+    /** Returns preference order. */
+    fun order(context: Context): Int? = null
+
+    /**
+     * Returns the preference title.
+     *
+     * Implement [PreferenceTitleProvider] interface if title content is generated dynamically.
+     */
+    fun getPreferenceTitle(context: Context): CharSequence? =
+        when {
+            title != 0 -> context.getText(title)
+            this is PreferenceTitleProvider -> getTitle(context)
+            else -> null
+        }
+
+    /**
+     * Returns the preference summary.
+     *
+     * Implement [PreferenceSummaryProvider] interface if summary content is generated dynamically
+     * (e.g. summary is provided per preference value).
+     */
+    fun getPreferenceSummary(context: Context): CharSequence? =
+        when {
+            summary != 0 -> context.getText(summary)
+            this is PreferenceSummaryProvider -> getSummary(context)
+            else -> null
+        }
+}
+
+/** Metadata of preference group. */
+@AnyThread
+open class PreferenceGroup(override val key: String, override val title: Int) : PreferenceMetadata
+
+/** Metadata of preference screen. */
+@AnyThread
+interface PreferenceScreenMetadata : PreferenceMetadata {
+
+    /**
+     * The screen title resource, which precedes [getScreenTitle] if provided.
+     *
+     * By default, screen title is same with [title].
+     */
+    val screenTitle: Int
+        get() = title
+
+    /** Returns dynamic screen title, use [screenTitle] whenever possible. */
+    fun getScreenTitle(context: Context): CharSequence? = null
+
+    /** Returns the fragment class to show the preference screen. */
+    fun fragmentClass(): Class<out Fragment>?
+
+    /**
+     * Indicates if [getPreferenceHierarchy] returns a complete hierarchy of the preference screen.
+     *
+     * If `true`, the result of [getPreferenceHierarchy] will be used to inflate preference screen.
+     * Otherwise, it is an intermediate state called hybrid mode, preference hierarchy is
+     * represented by other ways (e.g. XML resource) and [PreferenceMetadata]s in
+     * [getPreferenceHierarchy] will only be used to bind UI widgets.
+     */
+    fun hasCompleteHierarchy(): Boolean = true
+
+    /**
+     * Returns the hierarchy of preference screen.
+     *
+     * The implementation MUST include all preferences into the hierarchy regardless of the runtime
+     * conditions. DO NOT check any condition (except compile time flag) before adding a preference.
+     */
+    fun getPreferenceHierarchy(context: Context): PreferenceHierarchy
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt
new file mode 100644
index 0000000..84014f1
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenBindingKeyProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import android.content.Context
+
+/** Provides the associated preference screen key for binding. */
+interface PreferenceScreenBindingKeyProvider {
+
+    /** Returns the associated preference screen key. */
+    fun getPreferenceScreenBindingKey(context: Context): String?
+}
+
+/** Extra key to provide the preference screen key for binding. */
+const val EXTRA_BINDING_SCREEN_KEY = "settingslib:binding_screen_key"
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
new file mode 100644
index 0000000..48798da
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceScreenRegistry.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import android.content.Context
+import com.android.settingslib.datastore.KeyValueStore
+import com.google.common.base.Supplier
+import com.google.common.base.Suppliers
+import com.google.common.collect.ImmutableMap
+
+private typealias PreferenceScreenMap = ImmutableMap<String, PreferenceScreenMetadata>
+
+/** Registry of all available preference screens in the app. */
+object PreferenceScreenRegistry : ReadWritePermitProvider {
+
+    /** Provider of key-value store. */
+    private lateinit var keyValueStoreProvider: KeyValueStoreProvider
+
+    private var preferenceScreensSupplier: Supplier<PreferenceScreenMap> = Supplier {
+        ImmutableMap.of()
+    }
+
+    private val preferenceScreens: PreferenceScreenMap
+        get() = preferenceScreensSupplier.get()
+
+    private var readWritePermitProvider: ReadWritePermitProvider? = null
+
+    /** Sets the [KeyValueStoreProvider]. */
+    fun setKeyValueStoreProvider(keyValueStoreProvider: KeyValueStoreProvider) {
+        this.keyValueStoreProvider = keyValueStoreProvider
+    }
+
+    /**
+     * Returns the key-value store for given preference.
+     *
+     * Must call [setKeyValueStoreProvider] before invoking this method, otherwise
+     * [NullPointerException] is raised.
+     */
+    fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore? =
+        keyValueStoreProvider.getKeyValueStore(context, preference)
+
+    /** Sets supplier to provide available preference screens. */
+    fun setPreferenceScreensSupplier(supplier: Supplier<List<PreferenceScreenMetadata>>) {
+        preferenceScreensSupplier =
+            Suppliers.memoize {
+                val screensBuilder = ImmutableMap.builder<String, PreferenceScreenMetadata>()
+                for (screen in supplier.get()) screensBuilder.put(screen.key, screen)
+                screensBuilder.buildOrThrow()
+            }
+    }
+
+    /** Sets available preference screens. */
+    fun setPreferenceScreens(vararg screens: PreferenceScreenMetadata) {
+        val screensBuilder = ImmutableMap.builder<String, PreferenceScreenMetadata>()
+        for (screen in screens) screensBuilder.put(screen.key, screen)
+        preferenceScreensSupplier = Suppliers.ofInstance(screensBuilder.buildOrThrow())
+    }
+
+    /** Returns [PreferenceScreenMetadata] of particular key. */
+    operator fun get(key: String?): PreferenceScreenMetadata? =
+        if (key != null) preferenceScreens[key] else null
+
+    /**
+     * Sets the provider to check read write permit. Read and write requests are denied by default.
+     */
+    fun setReadWritePermitProvider(readWritePermitProvider: ReadWritePermitProvider?) {
+        this.readWritePermitProvider = readWritePermitProvider
+    }
+
+    override fun getReadPermit(
+        context: Context,
+        myUid: Int,
+        callingUid: Int,
+        preference: PreferenceMetadata,
+    ) =
+        readWritePermitProvider?.getReadPermit(context, myUid, callingUid, preference)
+            ?: ReadWritePermit.DISALLOW
+
+    override fun getWritePermit(
+        context: Context,
+        value: Any?,
+        myUid: Int,
+        callingUid: Int,
+        preference: PreferenceMetadata,
+    ) =
+        readWritePermitProvider?.getWritePermit(context, value, myUid, callingUid, preference)
+            ?: ReadWritePermit.DISALLOW
+}
+
+/** Provider of [KeyValueStore]. */
+fun interface KeyValueStoreProvider {
+
+    /**
+     * Returns the key-value store for given preference.
+     *
+     * Here are some use cases:
+     * - provide the default storage for all preferences
+     * - determine the storage per preference keys or the interfaces implemented by the preference
+     */
+    fun getKeyValueStore(context: Context, preference: PreferenceMetadata): KeyValueStore?
+}
+
+/** Provider of read and write permit. */
+interface ReadWritePermitProvider {
+
+    @ReadWritePermit
+    fun getReadPermit(
+        context: Context,
+        myUid: Int,
+        callingUid: Int,
+        preference: PreferenceMetadata,
+    ): Int
+
+    @ReadWritePermit
+    fun getWritePermit(
+        context: Context,
+        value: Any?,
+        myUid: Int,
+        callingUid: Int,
+        preference: PreferenceMetadata,
+    ): Int
+
+    companion object {
+        @JvmField
+        val ALLOW_ALL_READ_WRITE =
+            object : ReadWritePermitProvider {
+                override fun getReadPermit(
+                    context: Context,
+                    myUid: Int,
+                    callingUid: Int,
+                    preference: PreferenceMetadata,
+                ) = ReadWritePermit.ALLOW
+
+                override fun getWritePermit(
+                    context: Context,
+                    value: Any?,
+                    myUid: Int,
+                    callingUid: Int,
+                    preference: PreferenceMetadata,
+                ) = ReadWritePermit.ALLOW
+            }
+    }
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt
new file mode 100644
index 0000000..a3aa85d
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceStateProviders.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import android.content.Context
+
+/**
+ * Interface to provide dynamic preference title.
+ *
+ * Implement this interface implies that the preference title should not be cached for indexing.
+ */
+interface PreferenceTitleProvider {
+
+    /** Provides preference title. */
+    fun getTitle(context: Context): CharSequence?
+}
+
+/**
+ * Interface to provide dynamic preference summary.
+ *
+ * Implement this interface implies that the preference summary should not be cached for indexing.
+ */
+interface PreferenceSummaryProvider {
+
+    /** Provides preference summary. */
+    fun getSummary(context: Context): CharSequence?
+}
+
+/**
+ * Interface to provide the state of preference availability.
+ *
+ * UI framework normally does not show the preference widget if it is unavailable.
+ */
+interface PreferenceAvailabilityProvider {
+
+    /** Returns if the preference is available. */
+    fun isAvailable(context: Context): Boolean
+}
+
+/**
+ * Interface to provide the managed configuration state of the preference.
+ *
+ * See [Managed configurations](https://developer.android.com/work/managed-configurations) for the
+ * Android Enterprise support.
+ */
+interface PreferenceRestrictionProvider {
+
+    /** Returns if preference is restricted by managed configs. */
+    fun isRestricted(context: Context): Boolean
+}
+
+/**
+ * Preference lifecycle to deal with preference state.
+ *
+ * Implement this interface when preference depends on runtime conditions.
+ */
+interface PreferenceLifecycleProvider {
+
+    /**
+     * Called when preference is attached to UI.
+     *
+     * Subclass could override this API to register runtime condition listeners, and invoke
+     * `onPreferenceStateChanged(this)` on the given [preferenceStateObserver] to update UI when
+     * internal state (e.g. availability, enabled state, title, summary) is changed.
+     */
+    fun onAttach(context: Context, preferenceStateObserver: PreferenceStateObserver)
+
+    /**
+     * Called when preference is detached from UI.
+     *
+     * Clean up and release resource.
+     */
+    fun onDetach(context: Context)
+
+    /** Observer of preference state. */
+    interface PreferenceStateObserver {
+
+        /** Callbacks when preference state is changed. */
+        fun onPreferenceStateChanged(preference: PreferenceMetadata)
+    }
+}
diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt
new file mode 100644
index 0000000..ad996c7
--- /dev/null
+++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceTypes.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.metadata
+
+import android.content.Context
+import androidx.annotation.StringRes
+
+/**
+ * Common base class for preferences that have two selectable states, save a boolean value, and may
+ * have dependent preferences that are enabled/disabled based on the current state.
+ */
+interface TwoStatePreference : PreferenceMetadata, PersistentPreference<Boolean>, BooleanValue {
+
+    override fun shouldDisableDependents(context: Context) =
+        storage(context).getValue(key, Boolean::class.javaObjectType) != true ||
+            super.shouldDisableDependents(context)
+}
+
+/** A preference that provides a two-state toggleable option. */
+open class SwitchPreference
+@JvmOverloads
+constructor(
+    override val key: String,
+    @StringRes override val title: Int = 0,
+    @StringRes override val summary: Int = 0,
+) : TwoStatePreference
diff --git a/packages/SettingsLib/Preference/Android.bp b/packages/SettingsLib/Preference/Android.bp
new file mode 100644
index 0000000..9665dbd
--- /dev/null
+++ b/packages/SettingsLib/Preference/Android.bp
@@ -0,0 +1,23 @@
+package {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+    name: "SettingsLibPreference-srcs",
+    srcs: ["src/**/*.kt"],
+}
+
+android_library {
+    name: "SettingsLibPreference",
+    defaults: [
+        "SettingsLintDefaults",
+    ],
+    srcs: [":SettingsLibPreference-srcs"],
+    static_libs: [
+        "SettingsLibDataStore",
+        "SettingsLibMetadata",
+        "androidx.annotation_annotation",
+        "androidx.preference_preference",
+    ],
+    kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SettingsLib/Preference/AndroidManifest.xml b/packages/SettingsLib/Preference/AndroidManifest.xml
new file mode 100644
index 0000000..2d7f7ba
--- /dev/null
+++ b/packages/SettingsLib/Preference/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.settingslib.preference">
+
+  <uses-sdk android:minSdkVersion="21" />
+</manifest>
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt
new file mode 100644
index 0000000..9be0e71
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import android.content.Context
+import androidx.preference.DialogPreference
+import androidx.preference.ListPreference
+import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
+import androidx.preference.SeekBarPreference
+import com.android.settingslib.metadata.DiscreteIntValue
+import com.android.settingslib.metadata.DiscreteValue
+import com.android.settingslib.metadata.PreferenceAvailabilityProvider
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceScreenMetadata
+import com.android.settingslib.metadata.RangeValue
+
+/** Binding of preference widget and preference metadata. */
+interface PreferenceBinding {
+
+    /**
+     * Provides a new [Preference] widget instance.
+     *
+     * By default, it returns a new [Preference] object. Subclass could override this method to
+     * provide customized widget and do **one-off** initialization (e.g.
+     * [Preference.setOnPreferenceClickListener]). To update widget everytime when state is changed,
+     * override the [bind] method.
+     *
+     * Notes:
+     * - DO NOT set any properties defined in [PreferenceMetadata]. For example,
+     *   title/summary/icon/extras/isEnabled/isVisible/isPersistent/dependency. These properties
+     *   will be reset by [bind].
+     * - Override [bind] if needed to provide more information for customized widget.
+     */
+    fun createWidget(context: Context): Preference = Preference(context)
+
+    /**
+     * Binds preference widget with given metadata.
+     *
+     * Whenever metadata state is changed, this callback is invoked to update widget. By default,
+     * the common states like title, summary, enabled, etc. are already applied. Subclass should
+     * override this method to bind more data (e.g. read preference value from storage and apply it
+     * to widget).
+     *
+     * @param preference preference widget created by [createWidget]
+     * @param metadata metadata to apply
+     */
+    fun bind(preference: Preference, metadata: PreferenceMetadata) {
+        metadata.apply {
+            preference.key = key
+            if (icon != 0) {
+                preference.setIcon(icon)
+            } else {
+                preference.icon = null
+            }
+            val context = preference.context
+            preference.peekExtras()?.clear()
+            extras(context)?.let { preference.extras.putAll(it) }
+            preference.title = getPreferenceTitle(context)
+            preference.summary = getPreferenceSummary(context)
+            preference.isEnabled = isEnabled(context)
+            preference.isVisible =
+                (this as? PreferenceAvailabilityProvider)?.isAvailable(context) != false
+            preference.isPersistent = isPersistent(context)
+            metadata.order(context)?.let { preference.order = it }
+            // PreferenceRegistry will notify dependency change, so we do not need to set
+            // dependency here. This simplifies dependency management and avoid the
+            // IllegalStateException when call Preference.setDependency
+            preference.dependency = null
+            if (preference !is PreferenceScreen) { // avoid recursive loop when build graph
+                preference.fragment = (this as? PreferenceScreenCreator)?.fragmentClass()?.name
+                preference.intent = intent(context)
+            }
+            if (preference is DialogPreference) {
+                preference.dialogTitle = preference.title
+            }
+            if (preference is ListPreference && this is DiscreteValue<*>) {
+                preference.setEntries(valuesDescription)
+                if (this is DiscreteIntValue) {
+                    val intValues = context.resources.getIntArray(values)
+                    preference.entryValues = Array(intValues.size) { intValues[it].toString() }
+                } else {
+                    preference.setEntryValues(values)
+                }
+            } else if (preference is SeekBarPreference && this is RangeValue) {
+                preference.min = minValue
+                preference.max = maxValue
+                preference.seekBarIncrement = incrementStep
+            }
+        }
+    }
+}
+
+/** Abstract preference screen to provide preference hierarchy and binding factory. */
+interface PreferenceScreenCreator : PreferenceScreenMetadata, PreferenceScreenProvider {
+
+    val preferenceBindingFactory: PreferenceBindingFactory
+        get() = DefaultPreferenceBindingFactory
+
+    override fun createPreferenceScreen(factory: PreferenceScreenFactory) =
+        factory.getOrCreatePreferenceScreen().apply {
+            inflatePreferenceHierarchy(preferenceBindingFactory, getPreferenceHierarchy(context))
+        }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt
new file mode 100644
index 0000000..4c2e1ba
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import com.android.settingslib.metadata.PreferenceGroup
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.SwitchPreference
+
+/** Factory to map [PreferenceMetadata] to [PreferenceBinding]. */
+interface PreferenceBindingFactory {
+
+    /** Returns the [PreferenceBinding] associated with the [PreferenceMetadata]. */
+    fun getPreferenceBinding(metadata: PreferenceMetadata): PreferenceBinding?
+}
+
+/** Default [PreferenceBindingFactory]. */
+object DefaultPreferenceBindingFactory : PreferenceBindingFactory {
+
+    override fun getPreferenceBinding(metadata: PreferenceMetadata) =
+        metadata as? PreferenceBinding
+            ?: when (metadata) {
+                is SwitchPreference -> SwitchPreferenceBinding.INSTANCE
+                is PreferenceGroup -> PreferenceGroupBinding.INSTANCE
+                is PreferenceScreenCreator -> PreferenceScreenBinding.INSTANCE
+                else -> DefaultPreferenceBinding
+            }
+}
+
+/** A preference key based binding factory. */
+class KeyedPreferenceBindingFactory(private val bindings: Map<String, PreferenceBinding>) :
+    PreferenceBindingFactory {
+
+    override fun getPreferenceBinding(metadata: PreferenceMetadata) =
+        bindings[metadata.key] ?: DefaultPreferenceBindingFactory.getPreferenceBinding(metadata)
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt
new file mode 100644
index 0000000..ede970e
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import android.content.Context
+import androidx.preference.Preference
+import androidx.preference.PreferenceCategory
+import androidx.preference.PreferenceScreen
+import androidx.preference.SwitchPreferenceCompat
+import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceScreenMetadata
+import com.android.settingslib.metadata.PreferenceTitleProvider
+
+/** Binding of preference group associated with [PreferenceCategory]. */
+interface PreferenceScreenBinding : PreferenceBinding {
+
+    override fun bind(preference: Preference, metadata: PreferenceMetadata) {
+        super.bind(preference, metadata)
+        val context = preference.context
+        val screenMetadata = metadata as PreferenceScreenMetadata
+        // Pass the preference key to fragment, so that the fragment could find associated
+        // preference screen registered in PreferenceScreenRegistry
+        preference.extras.putString(EXTRA_BINDING_SCREEN_KEY, preference.key)
+        if (preference is PreferenceScreen) {
+            val screenTitle = screenMetadata.screenTitle
+            preference.title =
+                if (screenTitle != 0) {
+                    context.getString(screenTitle)
+                } else {
+                    screenMetadata.getScreenTitle(context)
+                        ?: (this as? PreferenceTitleProvider)?.getTitle(context)
+                }
+        }
+    }
+
+    companion object {
+        @JvmStatic val INSTANCE = object : PreferenceScreenBinding {}
+    }
+}
+
+/** Binding of preference group associated with [PreferenceCategory]. */
+interface PreferenceGroupBinding : PreferenceBinding {
+
+    override fun createWidget(context: Context) = PreferenceCategory(context)
+
+    companion object {
+        @JvmStatic val INSTANCE = object : PreferenceGroupBinding {}
+    }
+}
+
+/** A boolean value type preference associated with [SwitchPreferenceCompat]. */
+interface SwitchPreferenceBinding : PreferenceBinding {
+
+    override fun createWidget(context: Context): Preference = SwitchPreferenceCompat(context)
+
+    override fun bind(preference: Preference, metadata: PreferenceMetadata) {
+        super.bind(preference, metadata)
+        (metadata as? PersistentPreference<*>)
+            ?.storage(preference.context)
+            ?.getValue(metadata.key, Boolean::class.javaObjectType)
+            ?.let { (preference as SwitchPreferenceCompat).isChecked = it }
+    }
+
+    companion object {
+        @JvmStatic val INSTANCE = object : SwitchPreferenceBinding {}
+    }
+}
+
+/** Default [PreferenceBinding] for [Preference]. */
+object DefaultPreferenceBinding : PreferenceBinding
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt
new file mode 100644
index 0000000..02acfca
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import androidx.preference.PreferenceDataStore
+import com.android.settingslib.datastore.KeyValueStore
+
+/** Adapter to translate [KeyValueStore] into [PreferenceDataStore]. */
+class PreferenceDataStoreAdapter(private val keyValueStore: KeyValueStore) : PreferenceDataStore() {
+
+    override fun getBoolean(key: String, defValue: Boolean): Boolean =
+        keyValueStore.getValue(key, Boolean::class.javaObjectType) ?: defValue
+
+    override fun getFloat(key: String, defValue: Float): Float =
+        keyValueStore.getValue(key, Float::class.javaObjectType) ?: defValue
+
+    override fun getInt(key: String, defValue: Int): Int =
+        keyValueStore.getValue(key, Int::class.javaObjectType) ?: defValue
+
+    override fun getLong(key: String, defValue: Long): Long =
+        keyValueStore.getValue(key, Long::class.javaObjectType) ?: defValue
+
+    override fun getString(key: String, defValue: String?): String? =
+        keyValueStore.getValue(key, String::class.javaObjectType) ?: defValue
+
+    override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? =
+        (keyValueStore.getValue(key, Set::class.javaObjectType) as Set<String>?) ?: defValues
+
+    override fun putBoolean(key: String, value: Boolean) =
+        keyValueStore.setValue(key, Boolean::class.javaObjectType, value)
+
+    override fun putFloat(key: String, value: Float) =
+        keyValueStore.setValue(key, Float::class.javaObjectType, value)
+
+    override fun putInt(key: String, value: Int) =
+        keyValueStore.setValue(key, Int::class.javaObjectType, value)
+
+    override fun putLong(key: String, value: Long) =
+        keyValueStore.setValue(key, Long::class.javaObjectType, value)
+
+    override fun putString(key: String, value: String?) =
+        keyValueStore.setValue(key, String::class.javaObjectType, value)
+
+    override fun putStringSet(key: String, values: Set<String>?) =
+        keyValueStore.setValue(key, Set::class.javaObjectType, values)
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
new file mode 100644
index 0000000..2072009
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import android.content.Context
+import android.os.Bundle
+import androidx.annotation.XmlRes
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceScreen
+import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY
+import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+import com.android.settingslib.preference.PreferenceScreenBindingHelper.Companion.bindRecursively
+
+/** Fragment to display a preference screen. */
+open class PreferenceFragment :
+    PreferenceFragmentCompat(), PreferenceScreenProvider, PreferenceScreenBindingKeyProvider {
+
+    private var preferenceScreenBindingHelper: PreferenceScreenBindingHelper? = null
+
+    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+        preferenceScreen = createPreferenceScreen()
+    }
+
+    fun createPreferenceScreen(): PreferenceScreen? =
+        createPreferenceScreen(PreferenceScreenFactory(this))
+
+    override fun createPreferenceScreen(factory: PreferenceScreenFactory): PreferenceScreen? {
+        val context = factory.context
+        fun createPreferenceScreenFromResource() =
+            factory.inflate(getPreferenceScreenResId(context))
+
+        if (!usePreferenceScreenMetadata()) return createPreferenceScreenFromResource()
+
+        val screenKey = getPreferenceScreenBindingKey(context)
+        val screenCreator =
+            (PreferenceScreenRegistry[screenKey] as? PreferenceScreenCreator)
+                ?: return createPreferenceScreenFromResource()
+
+        val preferenceBindingFactory = screenCreator.preferenceBindingFactory
+        val preferenceHierarchy = screenCreator.getPreferenceHierarchy(context)
+        val preferenceScreen =
+            if (screenCreator.hasCompleteHierarchy()) {
+                factory.getOrCreatePreferenceScreen().apply {
+                    inflatePreferenceHierarchy(preferenceBindingFactory, preferenceHierarchy)
+                }
+            } else {
+                createPreferenceScreenFromResource()?.also {
+                    bindRecursively(it, preferenceBindingFactory, preferenceHierarchy)
+                } ?: return null
+            }
+        preferenceScreenBindingHelper =
+            PreferenceScreenBindingHelper(
+                context,
+                preferenceBindingFactory,
+                preferenceScreen,
+                preferenceHierarchy,
+            )
+        return preferenceScreen
+    }
+
+    /**
+     * Returns if preference screen metadata can be used to set up preference screen.
+     *
+     * This is for flagging purpose. If false (e.g. flag is disabled), xml resource is used to build
+     * preference screen.
+     */
+    protected open fun usePreferenceScreenMetadata(): Boolean = true
+
+    /** Returns the xml resource to create preference screen. */
+    @XmlRes protected open fun getPreferenceScreenResId(context: Context): Int = 0
+
+    override fun getPreferenceScreenBindingKey(context: Context): String? =
+        arguments?.getString(EXTRA_BINDING_SCREEN_KEY)
+
+    override fun onDestroy() {
+        preferenceScreenBindingHelper?.close()
+        super.onDestroy()
+    }
+
+    companion object {
+        /** Returns [PreferenceFragment] instance to display the preference screen of given key. */
+        fun of(screenKey: String): PreferenceFragment? {
+            val screenMetadata = PreferenceScreenRegistry[screenKey] ?: return null
+            if (
+                screenMetadata is PreferenceScreenCreator && screenMetadata.hasCompleteHierarchy()
+            ) {
+                return PreferenceFragment().apply {
+                    arguments = Bundle().apply { putString(EXTRA_BINDING_SCREEN_KEY, screenKey) }
+                }
+            }
+            return null
+        }
+    }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt
new file mode 100644
index 0000000..5ef7823
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import androidx.preference.PreferenceDataStore
+import androidx.preference.PreferenceGroup
+import com.android.settingslib.datastore.KeyValueStore
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceHierarchy
+import com.android.settingslib.metadata.PreferenceMetadata
+
+/** Inflates [PreferenceHierarchy] into given [PreferenceGroup] recursively. */
+fun PreferenceGroup.inflatePreferenceHierarchy(
+    preferenceBindingFactory: PreferenceBindingFactory,
+    hierarchy: PreferenceHierarchy,
+    storages: MutableMap<KeyValueStore, PreferenceDataStore> = mutableMapOf(),
+) {
+    fun PreferenceMetadata.preferenceBinding() = preferenceBindingFactory.getPreferenceBinding(this)
+
+    hierarchy.metadata.let { it.preferenceBinding()?.bind(this, it) }
+    hierarchy.forEach {
+        val metadata = it.metadata
+        val preferenceBinding = metadata.preferenceBinding() ?: return@forEach
+        val widget = preferenceBinding.createWidget(context)
+        if (it is PreferenceHierarchy) {
+            val preferenceGroup = widget as PreferenceGroup
+            // MUST add preference before binding, otherwise exception is raised when add child
+            addPreference(preferenceGroup)
+            preferenceGroup.inflatePreferenceHierarchy(preferenceBindingFactory, it)
+        } else {
+            preferenceBinding.bind(widget, metadata)
+            (metadata as? PersistentPreference<*>)?.storage(context)?.let { storage ->
+                widget.preferenceDataStore =
+                    storages.getOrPut(storage) { PreferenceDataStoreAdapter(storage) }
+            }
+            // MUST add preference after binding for persistent preference to get initial value
+            // (preference key is set within bind method)
+            addPreference(widget)
+        }
+    }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
new file mode 100644
index 0000000..3610894
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import androidx.preference.Preference
+import androidx.preference.PreferenceGroup
+import androidx.preference.PreferenceScreen
+import com.android.settingslib.datastore.KeyedDataObservable
+import com.android.settingslib.datastore.KeyedObservable
+import com.android.settingslib.datastore.KeyedObserver
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceHierarchy
+import com.android.settingslib.metadata.PreferenceLifecycleProvider
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+import com.google.common.collect.ImmutableMap
+import com.google.common.collect.ImmutableMultimap
+import java.util.concurrent.Executor
+
+/**
+ * Helper to bind preferences on given [preferenceScreen].
+ *
+ * When there is any preference change event detected (e.g. preference value changed, runtime
+ * states, dependency is updated), this helper class will re-bind [PreferenceMetadata] to update
+ * widget UI.
+ */
+class PreferenceScreenBindingHelper(
+    context: Context,
+    private val preferenceBindingFactory: PreferenceBindingFactory,
+    private val preferenceScreen: PreferenceScreen,
+    preferenceHierarchy: PreferenceHierarchy,
+) : KeyedDataObservable<String>(), AutoCloseable {
+
+    private val handler = Handler(Looper.getMainLooper())
+    private val executor =
+        object : Executor {
+            override fun execute(command: Runnable) {
+                handler.post(command)
+            }
+        }
+
+    private val preferences: ImmutableMap<String, PreferenceMetadata>
+    private val dependencies: ImmutableMultimap<String, String>
+    private val storages = mutableSetOf<KeyedObservable<String>>()
+
+    private val preferenceObserver: KeyedObserver<String?>
+
+    private val storageObserver =
+        KeyedObserver<String?> { key, _ ->
+            if (key != null) {
+                notifyChange(key, CHANGE_REASON_VALUE)
+            }
+        }
+
+    private val stateObserver =
+        object : PreferenceLifecycleProvider.PreferenceStateObserver {
+            override fun onPreferenceStateChanged(preference: PreferenceMetadata) {
+                notifyChange(preference.key, CHANGE_REASON_STATE)
+            }
+        }
+
+    init {
+        val preferencesBuilder = ImmutableMap.builder<String, PreferenceMetadata>()
+        val dependenciesBuilder = ImmutableMultimap.builder<String, String>()
+        fun PreferenceMetadata.addDependency(dependency: PreferenceMetadata) {
+            dependenciesBuilder.put(key, dependency.key)
+        }
+
+        fun PreferenceMetadata.add() {
+            preferencesBuilder.put(key, this)
+            dependencyOfEnabledState(context)?.addDependency(this)
+            if (this is PreferenceLifecycleProvider) onAttach(context, stateObserver)
+            if (this is PersistentPreference<*>) storages.add(storage(context))
+        }
+
+        fun PreferenceHierarchy.addPreferences() {
+            metadata.add()
+            forEach {
+                if (it is PreferenceHierarchy) {
+                    it.addPreferences()
+                } else {
+                    it.metadata.add()
+                }
+            }
+        }
+
+        preferenceHierarchy.addPreferences()
+        this.preferences = preferencesBuilder.buildOrThrow()
+        this.dependencies = dependenciesBuilder.build()
+
+        preferenceObserver = KeyedObserver { key, reason -> onPreferenceChange(key, reason) }
+        addObserver(preferenceObserver, executor)
+        for (storage in storages) storage.addObserver(storageObserver, executor)
+    }
+
+    private fun onPreferenceChange(key: String?, reason: Int) {
+        if (key == null) return
+
+        // bind preference to update UI
+        preferenceScreen.findPreference<Preference>(key)?.let {
+            preferenceBindingFactory.bind(it, preferences[key])
+        }
+
+        // check reason to avoid potential infinite loop
+        if (reason != CHANGE_REASON_DEPENDENT) {
+            notifyDependents(key, mutableSetOf())
+        }
+    }
+
+    /** Notifies dependents recursively. */
+    private fun notifyDependents(key: String, notifiedKeys: MutableSet<String>) {
+        if (!notifiedKeys.add(key)) return
+        for (dependency in dependencies[key]) {
+            notifyChange(dependency, CHANGE_REASON_DEPENDENT)
+            notifyDependents(dependency, notifiedKeys)
+        }
+    }
+
+    override fun close() {
+        removeObserver(preferenceObserver)
+        val context = preferenceScreen.context
+        for (preference in preferences.values) {
+            if (preference is PreferenceLifecycleProvider) preference.onDetach(context)
+        }
+        for (storage in storages) storage.removeObserver(storageObserver)
+    }
+
+    companion object {
+        /** Preference value is changed. */
+        private const val CHANGE_REASON_VALUE = 0
+        /** Preference state (title/summary, enable state, etc.) is changed. */
+        private const val CHANGE_REASON_STATE = 1
+        /** Dependent preference state is changed. */
+        private const val CHANGE_REASON_DEPENDENT = 2
+
+        /** Updates preference screen that has incomplete hierarchy. */
+        @JvmStatic
+        fun bind(preferenceScreen: PreferenceScreen) {
+            PreferenceScreenRegistry[preferenceScreen.key]?.run {
+                if (!hasCompleteHierarchy()) {
+                    val preferenceBindingFactory =
+                        (this as? PreferenceScreenCreator)?.preferenceBindingFactory ?: return
+                    bindRecursively(
+                        preferenceScreen,
+                        preferenceBindingFactory,
+                        getPreferenceHierarchy(preferenceScreen.context),
+                    )
+                }
+            }
+        }
+
+        internal fun bindRecursively(
+            preferenceScreen: PreferenceScreen,
+            preferenceBindingFactory: PreferenceBindingFactory,
+            preferenceHierarchy: PreferenceHierarchy,
+        ) =
+            preferenceScreen.bindRecursively(
+                preferenceBindingFactory,
+                preferenceHierarchy.getAllPreferences().associateBy { it.key },
+            )
+
+        private fun PreferenceGroup.bindRecursively(
+            preferenceBindingFactory: PreferenceBindingFactory,
+            preferences: Map<String, PreferenceMetadata>,
+        ) {
+            preferenceBindingFactory.bind(this, preferences[key])
+            val count = preferenceCount
+            for (index in 0 until count) {
+                val preference = getPreference(index)
+                if (preference is PreferenceGroup) {
+                    preference.bindRecursively(preferenceBindingFactory, preferences)
+                } else {
+                    preferenceBindingFactory.bind(preference, preferences[preference.key])
+                }
+            }
+        }
+
+        private fun PreferenceBindingFactory.bind(
+            preference: Preference,
+            metadata: PreferenceMetadata?,
+        ) = metadata?.let { getPreferenceBinding(it)?.bind(preference, it) }
+    }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt
new file mode 100644
index 0000000..7f99d7a
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import android.content.Context
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceManager
+import androidx.preference.PreferenceScreen
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+
+/** Factory to create preference screen. */
+class PreferenceScreenFactory {
+    /** Preference manager to create/inflate preference screen. */
+    val preferenceManager: PreferenceManager
+
+    /**
+     * Optional existing hierarchy to merge the new hierarchies into.
+     *
+     * Provide existing hierarchy will preserve the internal state (e.g. scrollbar position) for
+     * [PreferenceFragmentCompat].
+     */
+    private val rootScreen: PreferenceScreen?
+
+    /**
+     * Factory constructor from preference fragment.
+     *
+     * The fragment must be within a valid lifecycle.
+     */
+    constructor(preferenceFragment: PreferenceFragmentCompat) {
+        preferenceManager = preferenceFragment.preferenceManager
+        rootScreen = preferenceFragment.preferenceScreen
+    }
+
+    /** Factory constructor from [Context]. */
+    constructor(context: Context) : this(PreferenceManager(context))
+
+    /** Factory constructor from [PreferenceManager]. */
+    constructor(preferenceManager: PreferenceManager) {
+        this.preferenceManager = preferenceManager
+        rootScreen = null
+    }
+
+    /** Context of the factory to create preference screen. */
+    val context: Context
+        get() = preferenceManager.context
+
+    /** Returns the existing hierarchy or create a new empty preference screen. */
+    fun getOrCreatePreferenceScreen(): PreferenceScreen =
+        rootScreen ?: preferenceManager.createPreferenceScreen(context)
+
+    /**
+     * Inflates [PreferenceScreen] from xml resource.
+     *
+     * @param xmlRes The resource ID of the XML to inflate
+     * @return The root hierarchy (if one was not provided, the new hierarchy's root)
+     */
+    fun inflate(xmlRes: Int): PreferenceScreen? =
+        if (xmlRes != 0) {
+            preferenceManager.inflateFromResource(preferenceManager.context, xmlRes, rootScreen)
+        } else {
+            rootScreen
+        }
+
+    /**
+     * Creates [PreferenceScreen] of given key.
+     *
+     * The screen must be registered in [PreferenceScreenFactory] and provide a complete hierarchy.
+     */
+    fun createBindingScreen(screenKey: String?): PreferenceScreen? {
+        val metadata = PreferenceScreenRegistry[screenKey] ?: return null
+        if (metadata is PreferenceScreenCreator && metadata.hasCompleteHierarchy()) {
+            return metadata.createPreferenceScreen(this)
+        }
+        return null
+    }
+
+    companion object {
+        /** Creates [PreferenceScreen] from [PreferenceScreenRegistry]. */
+        @JvmStatic
+        fun createBindingScreen(preference: Preference): PreferenceScreen? {
+            val preferenceScreenCreator =
+                (PreferenceScreenRegistry[preference.key] as? PreferenceScreenCreator)
+                    ?: return null
+            if (!preferenceScreenCreator.hasCompleteHierarchy()) return null
+            val factory = PreferenceScreenFactory(preference.context)
+            val preferenceScreen = preferenceScreenCreator.createPreferenceScreen(factory)
+            factory.preferenceManager.setPreferences(preferenceScreen)
+            return preferenceScreen
+        }
+    }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt
new file mode 100644
index 0000000..0573292
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import android.content.Context
+import androidx.preference.PreferenceScreen
+
+/**
+ * Interface to provide [PreferenceScreen].
+ *
+ * When implemented by Activity/Fragment, the Activity/Fragment [Context] APIs (e.g. `getContext()`,
+ * `getActivity()`) MUST not be used: preference screen creation could happen in background service,
+ * where the Activity/Fragment lifecycle callbacks (`onCreate`, `onDestroy`, etc.) are not invoked
+ * and context APIs return null.
+ */
+interface PreferenceScreenProvider {
+
+    /**
+     * Creates [PreferenceScreen].
+     *
+     * Preference screen creation could happen in background service. The implementation MUST use
+     * [PreferenceScreenFactory.context] to obtain context.
+     */
+    fun createPreferenceScreen(factory: PreferenceScreenFactory): PreferenceScreen?
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingContract.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingContract.kt
new file mode 100644
index 0000000..65adec4
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingContract.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.bluetooth.devicesettings
+
+/** The contract between the device settings provider services and Settings. */
+object DeviceSettingContract {
+    const val INVISIBLE_PROFILES = "INVISIBLE_PROFILES"
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
index 457d6a3..769b6e6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
@@ -22,6 +22,7 @@
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
 import com.android.settingslib.bluetooth.devicesettings.ActionSwitchPreference
 import com.android.settingslib.bluetooth.devicesettings.DeviceSetting
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingContract
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingItem
 import com.android.settingslib.bluetooth.devicesettings.DeviceSettingsConfig
@@ -30,6 +31,9 @@
 import com.android.settingslib.bluetooth.devicesettings.MultiTogglePreference
 import com.android.settingslib.bluetooth.devicesettings.ToggleInfo
 import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel.AppProvidedItem
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem
 import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
 import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
 import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
@@ -103,9 +107,18 @@
 
     private fun DeviceSettingItem.toModel(): DeviceSettingConfigItemModel {
         return if (!TextUtils.isEmpty(preferenceKey)) {
-            DeviceSettingConfigItemModel.BuiltinItem(settingId, preferenceKey!!)
+            if (settingId == DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES) {
+                BluetoothProfilesItem(
+                    settingId,
+                    preferenceKey!!,
+                    extras.getStringArrayList(DeviceSettingContract.INVISIBLE_PROFILES)
+                        ?: emptyList()
+                )
+            } else {
+                CommonBuiltinItem(settingId, preferenceKey!!)
+            }
         } else {
-            DeviceSettingConfigItemModel.AppProvidedItem(settingId)
+            AppProvidedItem(settingId)
         }
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
index 33beb06..7eae5b2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
@@ -23,6 +23,7 @@
 import android.content.ServiceConnection
 import android.os.IBinder
 import android.os.IInterface
+import android.text.TextUtils
 import android.util.Log
 import com.android.settingslib.bluetooth.BluetoothUtils
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
@@ -84,6 +85,10 @@
                 }
                 setAction(intentAction)
             }
+
+        fun isValid(): Boolean {
+            return !TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(intentAction)
+        }
     }
 
     private var isServiceEnabled =
@@ -96,7 +101,8 @@
                     } else if (allStatus.all { it is ServiceConnectionStatus.Connected }) {
                         allStatus
                             .filterIsInstance<
-                                ServiceConnectionStatus.Connected<IDeviceSettingsProviderService>
+                                ServiceConnectionStatus.Connected<
+                                        IDeviceSettingsProviderService>
                             >()
                             .all { it.service.serviceStatus?.enabled == true }
                     } else {
@@ -215,6 +221,7 @@
                     )
                 }
             }
+            ?.filter { it.isValid() }
             ?.distinct()
             ?.associateBy(
                 { it },
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt
index c1ac763..08fb3fb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt
@@ -36,10 +36,23 @@
     @DeviceSettingId val settingId: Int
 
     /** A built-in item in Settings. */
-    data class BuiltinItem(
-        @DeviceSettingId override val settingId: Int,
-        val preferenceKey: String?
-    ) : DeviceSettingConfigItemModel
+    sealed interface BuiltinItem : DeviceSettingConfigItemModel {
+        @DeviceSettingId override val settingId: Int
+        val preferenceKey: String
+
+        /** A general built-in item in Settings. */
+        data class CommonBuiltinItem(
+            @DeviceSettingId override val settingId: Int,
+            override val preferenceKey: String,
+        ) : BuiltinItem
+
+        /** A bluetooth profiles in Settings. */
+        data class BluetoothProfilesItem(
+            @DeviceSettingId override val settingId: Int,
+            override val preferenceKey: String,
+            val invisibleProfiles: List<String>,
+        ) : BuiltinItem
+    }
 
     /** A remote item provided by other apps. */
     data class AppProvidedItem(@DeviceSettingId override val settingId: Int) :
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
index ce155b5..81b5634 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
@@ -91,7 +91,9 @@
         `when`(cachedDevice.address).thenReturn(BLUETOOTH_ADDRESS)
         `when`(
                 bluetoothDevice.getMetadata(
-                    DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+                    DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS
+                )
+            )
             .thenReturn(BLUETOOTH_DEVICE_METADATA.toByteArray())
 
         `when`(configService.queryLocalInterface(anyString())).thenReturn(configService)
@@ -114,7 +116,8 @@
                     connection.onServiceConnected(
                         ComponentName(
                             SETTING_PROVIDER_SERVICE_PACKAGE_NAME_1,
-                            SETTING_PROVIDER_SERVICE_CLASS_NAME_1),
+                            SETTING_PROVIDER_SERVICE_CLASS_NAME_1,
+                        ),
                         settingProviderService1,
                     )
                 SETTING_PROVIDER_SERVICE_INTENT_ACTION_2 ->
@@ -146,16 +149,24 @@
     fun getDeviceSettingsConfig_withMetadata_success() {
         testScope.runTest {
             `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
-            `when`(settingProviderService1.serviceStatus).thenReturn(
-                DeviceSettingsProviderServiceStatus(true)
-            )
-            `when`(settingProviderService2.serviceStatus).thenReturn(
-                DeviceSettingsProviderServiceStatus(true)
-            )
+            `when`(settingProviderService1.serviceStatus)
+                .thenReturn(DeviceSettingsProviderServiceStatus(true))
+            `when`(settingProviderService2.serviceStatus)
+                .thenReturn(DeviceSettingsProviderServiceStatus(true))
 
             val config = underTest.getDeviceSettingsConfig(cachedDevice)
 
             assertConfig(config!!, DEVICE_SETTING_CONFIG)
+            assertThat(config.mainItems[0])
+                .isInstanceOf(DeviceSettingConfigItemModel.AppProvidedItem::class.java)
+            assertThat(config.mainItems[1])
+                .isInstanceOf(
+                    DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem::class.java
+                )
+            assertThat(config.mainItems[2])
+                .isInstanceOf(
+                    DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem::class.java
+                )
         }
     }
 
@@ -163,16 +174,16 @@
     fun getDeviceSettingsConfig_noMetadata_returnNull() {
         testScope.runTest {
             `when`(
-                bluetoothDevice.getMetadata(
-                    DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
+                    bluetoothDevice.getMetadata(
+                        DeviceSettingServiceConnection.METADATA_FAST_PAIR_CUSTOMIZED_FIELDS
+                    )
+                )
                 .thenReturn("".toByteArray())
             `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
-            `when`(settingProviderService1.serviceStatus).thenReturn(
-                DeviceSettingsProviderServiceStatus(true)
-            )
-            `when`(settingProviderService2.serviceStatus).thenReturn(
-                DeviceSettingsProviderServiceStatus(true)
-            )
+            `when`(settingProviderService1.serviceStatus)
+                .thenReturn(DeviceSettingsProviderServiceStatus(true))
+            `when`(settingProviderService2.serviceStatus)
+                .thenReturn(DeviceSettingsProviderServiceStatus(true))
 
             val config = underTest.getDeviceSettingsConfig(cachedDevice)
 
@@ -184,12 +195,10 @@
     fun getDeviceSettingsConfig_providerServiceNotEnabled_returnNull() {
         testScope.runTest {
             `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
-            `when`(settingProviderService1.serviceStatus).thenReturn(
-                DeviceSettingsProviderServiceStatus(false)
-            )
-            `when`(settingProviderService2.serviceStatus).thenReturn(
-                DeviceSettingsProviderServiceStatus(true)
-            )
+            `when`(settingProviderService1.serviceStatus)
+                .thenReturn(DeviceSettingsProviderServiceStatus(false))
+            `when`(settingProviderService2.serviceStatus)
+                .thenReturn(DeviceSettingsProviderServiceStatus(true))
 
             val config = underTest.getDeviceSettingsConfig(cachedDevice)
 
@@ -219,12 +228,10 @@
                     .getArgument<IDeviceSettingsListener>(1)
                     .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1))
             }
-            `when`(settingProviderService1.serviceStatus).thenReturn(
-                DeviceSettingsProviderServiceStatus(true)
-            )
-            `when`(settingProviderService2.serviceStatus).thenReturn(
-                DeviceSettingsProviderServiceStatus(true)
-            )
+            `when`(settingProviderService1.serviceStatus)
+                .thenReturn(DeviceSettingsProviderServiceStatus(true))
+            `when`(settingProviderService2.serviceStatus)
+                .thenReturn(DeviceSettingsProviderServiceStatus(true))
             var setting: DeviceSettingModel? = null
 
             underTest
@@ -247,12 +254,10 @@
                     .getArgument<IDeviceSettingsListener>(1)
                     .onDeviceSettingsChanged(listOf(DEVICE_SETTING_2))
             }
-            `when`(settingProviderService1.serviceStatus).thenReturn(
-                DeviceSettingsProviderServiceStatus(true)
-            )
-            `when`(settingProviderService2.serviceStatus).thenReturn(
-                DeviceSettingsProviderServiceStatus(true)
-            )
+            `when`(settingProviderService1.serviceStatus)
+                .thenReturn(DeviceSettingsProviderServiceStatus(true))
+            `when`(settingProviderService2.serviceStatus)
+                .thenReturn(DeviceSettingsProviderServiceStatus(true))
             var setting: DeviceSettingModel? = null
 
             underTest
@@ -270,17 +275,15 @@
         testScope.runTest {
             `when`(configService.getDeviceSettingsConfig(any())).thenReturn(DEVICE_SETTING_CONFIG)
             `when`(settingProviderService2.registerDeviceSettingsListener(any(), any())).then {
-                    input ->
+                input ->
                 input
                     .getArgument<IDeviceSettingsListener>(1)
                     .onDeviceSettingsChanged(listOf(DEVICE_SETTING_HELP))
             }
-            `when`(settingProviderService1.serviceStatus).thenReturn(
-                DeviceSettingsProviderServiceStatus(true)
-            )
-            `when`(settingProviderService2.serviceStatus).thenReturn(
-                DeviceSettingsProviderServiceStatus(true)
-            )
+            `when`(settingProviderService1.serviceStatus)
+                .thenReturn(DeviceSettingsProviderServiceStatus(true))
+            `when`(settingProviderService2.serviceStatus)
+                .thenReturn(DeviceSettingsProviderServiceStatus(true))
             var setting: DeviceSettingModel? = null
 
             underTest
@@ -324,12 +327,10 @@
                     .getArgument<IDeviceSettingsListener>(1)
                     .onDeviceSettingsChanged(listOf(DEVICE_SETTING_1))
             }
-            `when`(settingProviderService1.serviceStatus).thenReturn(
-                DeviceSettingsProviderServiceStatus(true)
-            )
-            `when`(settingProviderService2.serviceStatus).thenReturn(
-                DeviceSettingsProviderServiceStatus(true)
-            )
+            `when`(settingProviderService1.serviceStatus)
+                .thenReturn(DeviceSettingsProviderServiceStatus(true))
+            `when`(settingProviderService2.serviceStatus)
+                .thenReturn(DeviceSettingsProviderServiceStatus(true))
             var setting: DeviceSettingModel? = null
 
             underTest
@@ -347,8 +348,10 @@
                     DeviceSettingState.Builder()
                         .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_HEADER)
                         .setPreferenceState(
-                            ActionSwitchPreferenceState.Builder().setChecked(false).build())
-                        .build())
+                            ActionSwitchPreferenceState.Builder().setChecked(false).build()
+                        )
+                        .build(),
+                )
         }
     }
 
@@ -362,12 +365,10 @@
                     .getArgument<IDeviceSettingsListener>(1)
                     .onDeviceSettingsChanged(listOf(DEVICE_SETTING_2))
             }
-            `when`(settingProviderService1.serviceStatus).thenReturn(
-                DeviceSettingsProviderServiceStatus(true)
-            )
-            `when`(settingProviderService2.serviceStatus).thenReturn(
-                DeviceSettingsProviderServiceStatus(true)
-            )
+            `when`(settingProviderService1.serviceStatus)
+                .thenReturn(DeviceSettingsProviderServiceStatus(true))
+            `when`(settingProviderService2.serviceStatus)
+                .thenReturn(DeviceSettingsProviderServiceStatus(true))
             var setting: DeviceSettingModel? = null
 
             underTest
@@ -385,8 +386,10 @@
                     DeviceSettingState.Builder()
                         .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_ANC)
                         .setPreferenceState(
-                            MultiTogglePreferenceState.Builder().setState(2).build())
-                        .build())
+                            MultiTogglePreferenceState.Builder().setState(2).build()
+                        )
+                        .build(),
+                )
         }
     }
 
@@ -437,7 +440,7 @@
 
     private fun assertConfig(
         actual: DeviceSettingConfigModel,
-        serviceResponse: DeviceSettingsConfig
+        serviceResponse: DeviceSettingsConfig,
     ) {
         assertThat(actual.mainItems.size).isEqualTo(serviceResponse.mainContentItems.size)
         for (i in 0..<actual.mainItems.size) {
@@ -451,7 +454,7 @@
 
     private fun assertConfigItem(
         actual: DeviceSettingConfigItemModel,
-        serviceResponse: DeviceSettingItem
+        serviceResponse: DeviceSettingItem,
     ) {
         assertThat(actual.settingId).isEqualTo(serviceResponse.settingId)
     }
@@ -485,24 +488,43 @@
                 "</DEVICE_SETTINGS_CONFIG_ACTION>"
         val DEVICE_INFO = DeviceInfo.Builder().setBluetoothAddress(BLUETOOTH_ADDRESS).build()
         const val DEVICE_SETTING_ID_HELP = 12345
-        val DEVICE_SETTING_ITEM_1 =
+        val DEVICE_SETTING_APP_PROVIDED_ITEM_1 =
             DeviceSettingItem(
                 DeviceSettingId.DEVICE_SETTING_ID_HEADER,
                 SETTING_PROVIDER_SERVICE_PACKAGE_NAME_1,
                 SETTING_PROVIDER_SERVICE_CLASS_NAME_1,
-                SETTING_PROVIDER_SERVICE_INTENT_ACTION_1)
-        val DEVICE_SETTING_ITEM_2 =
+                SETTING_PROVIDER_SERVICE_INTENT_ACTION_1,
+            )
+        val DEVICE_SETTING_APP_PROVIDED_ITEM_2 =
             DeviceSettingItem(
                 DeviceSettingId.DEVICE_SETTING_ID_ANC,
                 SETTING_PROVIDER_SERVICE_PACKAGE_NAME_2,
                 SETTING_PROVIDER_SERVICE_CLASS_NAME_2,
-                SETTING_PROVIDER_SERVICE_INTENT_ACTION_2)
+                SETTING_PROVIDER_SERVICE_INTENT_ACTION_2,
+            )
+        val DEVICE_SETTING_BUILT_IN_ITEM =
+            DeviceSettingItem(
+                DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_AUDIO_DEVICE_TYPE_GROUP,
+                "",
+                "",
+                "",
+                "device_type",
+            )
+        val DEVICE_SETTING_BUILT_IN_BT_PROFILES_ITEM =
+            DeviceSettingItem(
+                DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES,
+                "",
+                "",
+                "",
+                "bluetooth_profiles",
+            )
         val DEVICE_SETTING_HELP_ITEM =
             DeviceSettingItem(
                 DEVICE_SETTING_ID_HELP,
                 SETTING_PROVIDER_SERVICE_PACKAGE_NAME_2,
                 SETTING_PROVIDER_SERVICE_CLASS_NAME_2,
-                SETTING_PROVIDER_SERVICE_INTENT_ACTION_2)
+                SETTING_PROVIDER_SERVICE_INTENT_ACTION_2,
+            )
         val DEVICE_SETTING_1 =
             DeviceSetting.Builder()
                 .setSettingId(DeviceSettingId.DEVICE_SETTING_ID_HEADER)
@@ -511,7 +533,8 @@
                         .setTitle("title1")
                         .setHasSwitch(true)
                         .setAllowedChangingState(true)
-                        .build())
+                        .build()
+                )
                 .build()
         val DEVICE_SETTING_2 =
             DeviceSetting.Builder()
@@ -524,22 +547,30 @@
                             ToggleInfo.Builder()
                                 .setLabel("label1")
                                 .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
-                                .build())
+                                .build()
+                        )
                         .addToggleInfo(
                             ToggleInfo.Builder()
                                 .setLabel("label2")
                                 .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
-                                .build())
-                        .build())
+                                .build()
+                        )
+                        .build()
+                )
                 .build()
-        val DEVICE_SETTING_HELP = DeviceSetting.Builder()
-            .setSettingId(DEVICE_SETTING_ID_HELP)
-            .setPreference(DeviceSettingHelpPreference.Builder().setIntent(Intent()).build())
-            .build()
+        val DEVICE_SETTING_HELP =
+            DeviceSetting.Builder()
+                .setSettingId(DEVICE_SETTING_ID_HELP)
+                .setPreference(DeviceSettingHelpPreference.Builder().setIntent(Intent()).build())
+                .build()
         val DEVICE_SETTING_CONFIG =
             DeviceSettingsConfig(
-                listOf(DEVICE_SETTING_ITEM_1),
-                listOf(DEVICE_SETTING_ITEM_2),
+                listOf(
+                    DEVICE_SETTING_APP_PROVIDED_ITEM_1,
+                    DEVICE_SETTING_BUILT_IN_ITEM,
+                    DEVICE_SETTING_BUILT_IN_BT_PROFILES_ITEM,
+                ),
+                listOf(DEVICE_SETTING_APP_PROVIDED_ITEM_2),
                 DEVICE_SETTING_HELP_ITEM,
             )
     }
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index d26a906..a9e81c7 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -756,6 +756,7 @@
         "notification_flags_lib",
         "PlatformComposeCore",
         "PlatformComposeSceneTransitionLayout",
+        "PlatformComposeSceneTransitionLayoutTestsUtils",
         "androidx.compose.runtime_runtime",
         "androidx.compose.material3_material3",
         "androidx.compose.material_material-icons-extended",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 892f778..7c89592 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -288,6 +288,16 @@
 }
 
 flag {
+  name: "qs_quick_rebind_active_tiles"
+  namespace: "systemui"
+  description: "Rebind active custom tiles quickly."
+  bug: "362526228"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
     name: "coroutine_tracing"
     namespace: "systemui"
     description: "Adds thread-local data to System UI's global coroutine scopes to "
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index 7fb88e8..ae92d259 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -99,8 +99,8 @@
         BouncerContent(
             viewModel,
             dialogFactory,
-            Modifier.sysuiResTag(Bouncer.TestTags.Root)
-                .element(Bouncer.Elements.Content)
+            Modifier.element(Bouncer.Elements.Content)
+                .sysuiResTag(Bouncer.TestTags.Root)
                 .fillMaxSize()
         )
     }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index 3cb0d8a..df101c5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -128,7 +128,11 @@
                 }
             },
     ) {
-        SceneTransitionLayout(state = state, modifier = modifier.fillMaxSize()) {
+        SceneTransitionLayout(
+            state = state,
+            modifier = modifier.fillMaxSize(),
+            swipeSourceDetector = viewModel.edgeDetector,
+        ) {
             sceneByKey.forEach { (sceneKey, scene) ->
                 scene(
                     key = sceneKey,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index f3577fa..007b84a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -395,14 +395,8 @@
             return 0f
         }
 
-        fun animateTo(targetContent: T) {
-            swipeAnimation.animateOffset(
-                initialVelocity = velocity,
-                targetContent = targetContent,
-            )
-        }
-
         val fromContent = swipeAnimation.fromContent
+        val consumedVelocity: Float
         if (canChangeContent) {
             // If we are halfway between two contents, we check what the target will be based on the
             // velocity and offset of the transition, then we launch the animation.
@@ -427,18 +421,16 @@
                 } else {
                     fromContent
                 }
-
-            animateTo(targetContent = targetContent)
+            consumedVelocity = swipeAnimation.animateOffset(velocity, targetContent = targetContent)
         } else {
             // We are doing an overscroll preview animation between scenes.
             check(fromContent == swipeAnimation.currentContent) {
                 "canChangeContent is false but currentContent != fromContent"
             }
-            animateTo(targetContent = fromContent)
+            consumedVelocity = swipeAnimation.animateOffset(velocity, targetContent = fromContent)
         }
 
-        // The onStop animation consumes any remaining velocity.
-        return velocity
+        return consumedVelocity
     }
 
     /**
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
index 2a09a77..966bda4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
@@ -312,11 +312,16 @@
 
     fun isAnimatingOffset(): Boolean = offsetAnimation != null
 
+    /**
+     * Animate the offset to a [targetContent], using the [initialVelocity] and an optional [spec]
+     *
+     * @return the velocity consumed
+     */
     fun animateOffset(
         initialVelocity: Float,
         targetContent: T,
         spec: AnimationSpec<Float>? = null,
-    ) {
+    ): Float {
         check(!isAnimatingOffset()) { "SwipeAnimation.animateOffset() can only be called once" }
 
         val initialProgress = progress
@@ -374,7 +379,7 @@
         if (skipAnimation) {
             // Unblock the job.
             offsetAnimationRunnable.complete(null)
-            return
+            return 0f
         }
 
         val isTargetGreater = targetOffset > animatable.value
@@ -424,6 +429,9 @@
                 /* Ignore. */
             }
         }
+
+        // This animation always consumes the whole available velocity
+        return initialVelocity
     }
 
     /** An exception thrown during the animation to stop it immediately. */
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 79f82c9..5b59356 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -1111,7 +1111,7 @@
         assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)
 
         // Release the finger.
-        dragController.onDragStopped(velocity = -velocityThreshold)
+        dragController.onDragStopped(velocity = -velocityThreshold, expectedConsumed = false)
 
         // Exhaust all coroutines *without advancing the clock*. Given that we are at progress >=
         // 100% and that the overscroll on scene B is doing nothing, we are already idle.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
index e2bdc49..bb15208 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
@@ -30,10 +30,12 @@
 import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.haptics.msdl.msdlPlayer
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.DevicePostureController
 import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED
 import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED
+import com.android.systemui.testKosmos
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
@@ -89,6 +91,9 @@
 
     @Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback>
 
+    private val kosmos = testKosmos()
+    private val msdlPlayer = kosmos.msdlPlayer
+
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
@@ -112,7 +117,8 @@
                 mKeyguardMessageAreaControllerFactory,
                 mPostureController,
                 fakeFeatureFlags,
-                mSelectedUserInteractor
+                mSelectedUserInteractor,
+                msdlPlayer,
             )
         mKeyguardPatternView.onAttachedToWindow()
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java
index 8f9b7c8..12c866f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/MirrorWindowControlTest.java
@@ -30,11 +30,11 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.WindowManager;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.systemui.SysuiTestCase;
 
 import org.junit.Before;
@@ -48,7 +48,7 @@
 @RunWith(AndroidJUnit4.class)
 public class MirrorWindowControlTest extends SysuiTestCase {
 
-    @Mock WindowManager mWindowManager;
+    @Mock ViewCaptureAwareWindowManager mWindowManager;
     View mView;
     int mViewWidth;
     int mViewHeight;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt
index dd85d9b..fc57757 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/CaptioningRepositoryTest.kt
@@ -20,11 +20,15 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.userRepository
+import com.android.systemui.user.utils.FakeUserScopedService
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -39,10 +43,11 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@Suppress("UnspecifiedRegisterReceiverFlag")
 @RunWith(AndroidJUnit4::class)
 class CaptioningRepositoryTest : SysuiTestCase() {
 
+    private val kosmos = testKosmos()
+
     @Captor
     private lateinit var listenerCaptor: ArgumentCaptor<CaptioningManager.CaptioningChangeListener>
 
@@ -50,34 +55,33 @@
 
     private lateinit var underTest: CaptioningRepository
 
-    private val testScope = TestScope()
-
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
 
         underTest =
-            CaptioningRepositoryImpl(
-                captioningManager,
-                testScope.testScheduler,
-                testScope.backgroundScope
-            )
+            with(kosmos) {
+                CaptioningRepositoryImpl(
+                    FakeUserScopedService(captioningManager),
+                    userRepository,
+                    testScope.testScheduler,
+                    applicationCoroutineScope,
+                )
+            }
     }
 
     @Test
     fun isSystemAudioCaptioningEnabled_change_repositoryEmits() {
-        testScope.runTest {
-            `when`(captioningManager.isEnabled).thenReturn(false)
-            val isSystemAudioCaptioningEnabled = mutableListOf<Boolean>()
-            underTest.isSystemAudioCaptioningEnabled
-                .onEach { isSystemAudioCaptioningEnabled.add(it) }
-                .launchIn(backgroundScope)
+        kosmos.testScope.runTest {
+            `when`(captioningManager.isSystemAudioCaptioningEnabled).thenReturn(false)
+            val models by collectValues(underTest.captioningModel.filterNotNull())
             runCurrent()
 
+            `when`(captioningManager.isSystemAudioCaptioningEnabled).thenReturn(true)
             triggerOnSystemAudioCaptioningChange()
             runCurrent()
 
-            assertThat(isSystemAudioCaptioningEnabled)
+            assertThat(models.map { it.isSystemAudioCaptioningEnabled })
                 .containsExactlyElementsIn(listOf(false, true))
                 .inOrder()
         }
@@ -85,18 +89,16 @@
 
     @Test
     fun isSystemAudioCaptioningUiEnabled_change_repositoryEmits() {
-        testScope.runTest {
-            `when`(captioningManager.isSystemAudioCaptioningUiEnabled).thenReturn(false)
-            val isSystemAudioCaptioningUiEnabled = mutableListOf<Boolean>()
-            underTest.isSystemAudioCaptioningUiEnabled
-                .onEach { isSystemAudioCaptioningUiEnabled.add(it) }
-                .launchIn(backgroundScope)
+        kosmos.testScope.runTest {
+            `when`(captioningManager.isEnabled).thenReturn(false)
+            val models by collectValues(underTest.captioningModel.filterNotNull())
             runCurrent()
 
+            `when`(captioningManager.isSystemAudioCaptioningUiEnabled).thenReturn(true)
             triggerSystemAudioCaptioningUiChange()
             runCurrent()
 
-            assertThat(isSystemAudioCaptioningUiEnabled)
+            assertThat(models.map { it.isSystemAudioCaptioningUiEnabled })
                 .containsExactlyElementsIn(listOf(false, true))
                 .inOrder()
         }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
index 3b0057d..e531e65 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
@@ -22,6 +22,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
 import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
 import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
 import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -73,6 +74,7 @@
                     keyguardInteractor = kosmos.keyguardInteractor,
                     keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
                     dreamManager = dreamManager,
+                    communalSceneInteractor = kosmos.communalSceneInteractor,
                     bgScope = kosmos.applicationCoroutineScope,
                 )
                 .apply { start() }
@@ -158,6 +160,36 @@
             }
         }
 
+    @Test
+    fun shouldNotStartDreamWhenLaunchingWidget() =
+        testScope.runTest {
+            keyguardRepository.setKeyguardShowing(true)
+            keyguardRepository.setDreaming(false)
+            powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
+            kosmos.communalSceneInteractor.setIsLaunchingWidget(true)
+            whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true)
+            runCurrent()
+
+            transition(from = KeyguardState.DREAMING, to = KeyguardState.GLANCEABLE_HUB)
+
+            verify(dreamManager, never()).startDream()
+        }
+
+    @Test
+    fun shouldNotStartDreamWhenOccluded() =
+        testScope.runTest {
+            keyguardRepository.setKeyguardShowing(true)
+            keyguardRepository.setDreaming(false)
+            powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
+            keyguardRepository.setKeyguardOccluded(true)
+            whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true)
+            runCurrent()
+
+            transition(from = KeyguardState.DREAMING, to = KeyguardState.GLANCEABLE_HUB)
+
+            verify(dreamManager, never()).startDream()
+        }
+
     private suspend fun TestScope.transition(from: KeyguardState, to: KeyguardState) {
         kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
             from = from,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt
index c1dcf37..69ccc58 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderTest.kt
@@ -41,7 +41,6 @@
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.util.fakeMediaControllerFactory
 import com.android.systemui.media.controls.util.mediaFlags
-import com.android.systemui.plugins.activityStarter
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.SbnBuilder
 import com.android.systemui.testKosmos
@@ -86,7 +85,6 @@
             context,
             testDispatcher,
             testScope,
-            kosmos.activityStarter,
             mediaControllerFactory,
             mediaFlags,
             kosmos.imageLoader,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index 4b132c4..a0bb017 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -18,9 +18,12 @@
 
 package com.android.systemui.scene.ui.viewmodel
 
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.view.MotionEvent
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.DefaultEdgeDetector
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.domain.interactor.falsingInteractor
 import com.android.systemui.classifier.fakeFalsingManager
@@ -37,6 +40,10 @@
 import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
+import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
@@ -60,6 +67,7 @@
     private val testScope by lazy { kosmos.testScope }
     private val sceneInteractor by lazy { kosmos.sceneInteractor }
     private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource }
+    private val fakeShadeRepository by lazy { kosmos.fakeShadeRepository }
     private val sceneContainerConfig by lazy { kosmos.sceneContainerConfig }
     private val falsingManager by lazy { kosmos.fakeFalsingManager }
 
@@ -75,6 +83,8 @@
                 sceneInteractor = sceneInteractor,
                 falsingInteractor = kosmos.falsingInteractor,
                 powerInteractor = kosmos.powerInteractor,
+                shadeInteractor = kosmos.shadeInteractor,
+                splitEdgeDetector = kosmos.splitEdgeDetector,
                 logger = kosmos.sceneLogger,
                 motionEventHandlerReceiver = { motionEventHandler ->
                     this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler
@@ -287,4 +297,48 @@
 
             assertThat(actionableContentKey).isEqualTo(Overlays.QuickSettingsShade)
         }
+
+    @Test
+    @DisableFlags(DualShade.FLAG_NAME)
+    fun edgeDetector_singleShade_usesDefaultEdgeDetector() =
+        testScope.runTest {
+            val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode)
+            fakeShadeRepository.setShadeLayoutWide(false)
+            assertThat(shadeMode).isEqualTo(ShadeMode.Single)
+
+            assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector)
+        }
+
+    @Test
+    @DisableFlags(DualShade.FLAG_NAME)
+    fun edgeDetector_splitShade_usesDefaultEdgeDetector() =
+        testScope.runTest {
+            val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode)
+            fakeShadeRepository.setShadeLayoutWide(true)
+            assertThat(shadeMode).isEqualTo(ShadeMode.Split)
+
+            assertThat(underTest.edgeDetector).isEqualTo(DefaultEdgeDetector)
+        }
+
+    @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun edgeDetector_dualShade_narrowScreen_usesSplitEdgeDetector() =
+        testScope.runTest {
+            val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode)
+            fakeShadeRepository.setShadeLayoutWide(false)
+
+            assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
+            assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector)
+        }
+
+    @Test
+    @EnableFlags(DualShade.FLAG_NAME)
+    fun edgeDetector_dualShade_wideScreen_usesSplitEdgeDetector() =
+        testScope.runTest {
+            val shadeMode by collectLastValue(kosmos.shadeInteractor.shadeMode)
+            fakeShadeRepository.setShadeLayoutWide(true)
+
+            assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
+            assertThat(underTest.edgeDetector).isEqualTo(kosmos.splitEdgeDetector)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt
new file mode 100644
index 0000000..3d76d28
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorTest.kt
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.viewmodel
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.End
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Bottom
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Left
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.Right
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopLeft
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Resolved.TopRight
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.Start
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopEnd
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge.TopStart
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SplitEdgeDetectorTest : SysuiTestCase() {
+
+    private val edgeSize = 40
+    private val screenWidth = 800
+    private val screenHeight = 600
+
+    private var edgeSplitFraction = 0.7f
+
+    private val underTest =
+        SplitEdgeDetector(
+            topEdgeSplitFraction = { edgeSplitFraction },
+            edgeSize = edgeSize.dp,
+        )
+
+    @Test
+    fun source_noEdge_detectsNothing() {
+        val detectedEdge =
+            swipeVerticallyFrom(
+                x = screenWidth / 2,
+                y = screenHeight / 2,
+            )
+        assertThat(detectedEdge).isNull()
+    }
+
+    @Test
+    fun source_swipeVerticallyOnTopLeft_detectsTopLeft() {
+        val detectedEdge =
+            swipeVerticallyFrom(
+                x = 1,
+                y = edgeSize - 1,
+            )
+        assertThat(detectedEdge).isEqualTo(TopLeft)
+    }
+
+    @Test
+    fun source_swipeHorizontallyOnTopLeft_detectsLeft() {
+        val detectedEdge =
+            swipeHorizontallyFrom(
+                x = 1,
+                y = edgeSize - 1,
+            )
+        assertThat(detectedEdge).isEqualTo(Left)
+    }
+
+    @Test
+    fun source_swipeVerticallyOnTopRight_detectsTopRight() {
+        val detectedEdge =
+            swipeVerticallyFrom(
+                x = screenWidth - 1,
+                y = edgeSize - 1,
+            )
+        assertThat(detectedEdge).isEqualTo(TopRight)
+    }
+
+    @Test
+    fun source_swipeHorizontallyOnTopRight_detectsRight() {
+        val detectedEdge =
+            swipeHorizontallyFrom(
+                x = screenWidth - 1,
+                y = edgeSize - 1,
+            )
+        assertThat(detectedEdge).isEqualTo(Right)
+    }
+
+    @Test
+    fun source_swipeVerticallyToLeftOfSplit_detectsTopLeft() {
+        val detectedEdge =
+            swipeVerticallyFrom(
+                x = (screenWidth * edgeSplitFraction).toInt() - 1,
+                y = edgeSize - 1,
+            )
+        assertThat(detectedEdge).isEqualTo(TopLeft)
+    }
+
+    @Test
+    fun source_swipeVerticallyToRightOfSplit_detectsTopRight() {
+        val detectedEdge =
+            swipeVerticallyFrom(
+                x = (screenWidth * edgeSplitFraction).toInt() + 1,
+                y = edgeSize - 1,
+            )
+        assertThat(detectedEdge).isEqualTo(TopRight)
+    }
+
+    @Test
+    fun source_edgeSplitFractionUpdatesDynamically() {
+        val middleX = (screenWidth * 0.5f).toInt()
+        val topY = 0
+
+        // Split closer to the right; middle of screen is considered "left".
+        edgeSplitFraction = 0.6f
+        assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopLeft)
+
+        // Split closer to the left; middle of screen is considered "right".
+        edgeSplitFraction = 0.4f
+        assertThat(swipeVerticallyFrom(x = middleX, y = topY)).isEqualTo(TopRight)
+
+        // Illegal fraction.
+        edgeSplitFraction = 1.2f
+        assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) }
+
+        // Illegal fraction.
+        edgeSplitFraction = -0.3f
+        assertFailsWith<IllegalArgumentException> { swipeVerticallyFrom(x = middleX, y = topY) }
+    }
+
+    @Test
+    fun source_swipeVerticallyOnBottom_detectsBottom() {
+        val detectedEdge =
+            swipeVerticallyFrom(
+                x = screenWidth / 3,
+                y = screenHeight - (edgeSize / 2),
+            )
+        assertThat(detectedEdge).isEqualTo(Bottom)
+    }
+
+    @Test
+    fun source_swipeHorizontallyOnBottom_detectsNothing() {
+        val detectedEdge =
+            swipeHorizontallyFrom(
+                x = screenWidth / 3,
+                y = screenHeight - (edgeSize - 1),
+            )
+        assertThat(detectedEdge).isNull()
+    }
+
+    @Test
+    fun source_swipeHorizontallyOnLeft_detectsLeft() {
+        val detectedEdge =
+            swipeHorizontallyFrom(
+                x = edgeSize - 1,
+                y = screenHeight / 2,
+            )
+        assertThat(detectedEdge).isEqualTo(Left)
+    }
+
+    @Test
+    fun source_swipeVerticallyOnLeft_detectsNothing() {
+        val detectedEdge =
+            swipeVerticallyFrom(
+                x = edgeSize - 1,
+                y = screenHeight / 2,
+            )
+        assertThat(detectedEdge).isNull()
+    }
+
+    @Test
+    fun source_swipeHorizontallyOnRight_detectsRight() {
+        val detectedEdge =
+            swipeHorizontallyFrom(
+                x = screenWidth - edgeSize + 1,
+                y = screenHeight / 2,
+            )
+        assertThat(detectedEdge).isEqualTo(Right)
+    }
+
+    @Test
+    fun source_swipeVerticallyOnRight_detectsNothing() {
+        val detectedEdge =
+            swipeVerticallyFrom(
+                x = screenWidth - edgeSize + 1,
+                y = screenHeight / 2,
+            )
+        assertThat(detectedEdge).isNull()
+    }
+
+    @Test
+    fun resolve_startInLtr_resolvesLeft() {
+        val resolvedEdge = Start.resolve(LayoutDirection.Ltr)
+        assertThat(resolvedEdge).isEqualTo(Left)
+    }
+
+    @Test
+    fun resolve_startInRtl_resolvesRight() {
+        val resolvedEdge = Start.resolve(LayoutDirection.Rtl)
+        assertThat(resolvedEdge).isEqualTo(Right)
+    }
+
+    @Test
+    fun resolve_endInLtr_resolvesRight() {
+        val resolvedEdge = End.resolve(LayoutDirection.Ltr)
+        assertThat(resolvedEdge).isEqualTo(Right)
+    }
+
+    @Test
+    fun resolve_endInRtl_resolvesLeft() {
+        val resolvedEdge = End.resolve(LayoutDirection.Rtl)
+        assertThat(resolvedEdge).isEqualTo(Left)
+    }
+
+    @Test
+    fun resolve_topStartInLtr_resolvesTopLeft() {
+        val resolvedEdge = TopStart.resolve(LayoutDirection.Ltr)
+        assertThat(resolvedEdge).isEqualTo(TopLeft)
+    }
+
+    @Test
+    fun resolve_topStartInRtl_resolvesTopRight() {
+        val resolvedEdge = TopStart.resolve(LayoutDirection.Rtl)
+        assertThat(resolvedEdge).isEqualTo(TopRight)
+    }
+
+    @Test
+    fun resolve_topEndInLtr_resolvesTopRight() {
+        val resolvedEdge = TopEnd.resolve(LayoutDirection.Ltr)
+        assertThat(resolvedEdge).isEqualTo(TopRight)
+    }
+
+    @Test
+    fun resolve_topEndInRtl_resolvesTopLeft() {
+        val resolvedEdge = TopEnd.resolve(LayoutDirection.Rtl)
+        assertThat(resolvedEdge).isEqualTo(TopLeft)
+    }
+
+    private fun swipeVerticallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? {
+        return swipeFrom(x, y, Orientation.Vertical)
+    }
+
+    private fun swipeHorizontallyFrom(x: Int, y: Int): SceneContainerEdge.Resolved? {
+        return swipeFrom(x, y, Orientation.Horizontal)
+    }
+
+    private fun swipeFrom(x: Int, y: Int, orientation: Orientation): SceneContainerEdge.Resolved? {
+        return underTest.source(
+            layoutSize = IntSize(width = screenWidth, height = screenHeight),
+            position = IntOffset(x, y),
+            density = Density(1f),
+            orientation = orientation,
+        )
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
index 3283ea1..9464c75 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
@@ -24,7 +24,6 @@
 import android.platform.test.flag.junit.FlagsParameterization
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.parameterizeSceneContainerFlag
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -39,6 +38,7 @@
 import com.android.systemui.power.data.repository.fakePowerRepository
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.shade.data.repository.fakeShadeRepository
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.shade.shadeTestUtil
 import com.android.systemui.shade.shared.flag.DualShade
@@ -66,18 +66,18 @@
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
 class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
-    val kosmos = testKosmos()
-    val testScope = kosmos.testScope
-    val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
-    val deviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository }
-    val disableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository }
-    val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
-    val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
-    val powerRepository by lazy { kosmos.fakePowerRepository }
-    val shadeTestUtil by lazy { kosmos.shadeTestUtil }
-    val userRepository by lazy { kosmos.fakeUserRepository }
-    val userSetupRepository by lazy { kosmos.fakeUserSetupRepository }
-    val dozeParameters by lazy { kosmos.dozeParameters }
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val deviceProvisioningRepository by lazy { kosmos.fakeDeviceProvisioningRepository }
+    private val disableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository }
+    private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
+    private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
+    private val powerRepository by lazy { kosmos.fakePowerRepository }
+    private val shadeRepository by lazy { kosmos.fakeShadeRepository }
+    private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+    private val userRepository by lazy { kosmos.fakeUserRepository }
+    private val userSetupRepository by lazy { kosmos.fakeUserSetupRepository }
+    private val dozeParameters by lazy { kosmos.dozeParameters }
 
     lateinit var underTest: ShadeInteractorImpl
 
@@ -497,4 +497,24 @@
 
             assertThat(shadeMode).isEqualTo(ShadeMode.Dual)
         }
+
+    @Test
+    fun getTopEdgeSplitFraction_narrowScreen_splitInHalf() =
+        testScope.runTest {
+            // Ensure isShadeLayoutWide is collected.
+            val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide)
+            shadeRepository.setShadeLayoutWide(false)
+
+            assertThat(underTest.getTopEdgeSplitFraction()).isEqualTo(0.5f)
+        }
+
+    @Test
+    fun getTopEdgeSplitFraction_wideScreen_leftSideLarger() =
+        testScope.runTest {
+            // Ensure isShadeLayoutWide is collected.
+            val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide)
+            shadeRepository.setShadeLayoutWide(true)
+
+            assertThat(underTest.getTopEdgeSplitFraction()).isGreaterThan(0.5f)
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 840aa92..26e1a4d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -26,6 +26,7 @@
 import com.android.settingslib.notification.data.repository.updateNotificationPolicy
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.DisableSceneContainer
 import com.android.systemui.flags.EnableSceneContainer
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.andSceneContainer
@@ -36,6 +37,7 @@
 import com.android.systemui.power.data.repository.fakePowerRepository
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.shadeTestUtil
 import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository
 import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
@@ -51,6 +53,7 @@
 import com.android.systemui.util.ui.value
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -153,7 +156,7 @@
     fun shouldShowEmptyShadeView_trueWhenNoNotifs() =
         testScope.runTest {
             val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
-            val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldIncludeFooterView by collectFooterViewVisibility()
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -196,7 +199,7 @@
     fun shouldShowEmptyShadeView_trueWhenQsExpandedInSplitShade() =
         testScope.runTest {
             val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
-            val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldIncludeFooterView by collectFooterViewVisibility()
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -217,7 +220,7 @@
     fun shouldShowEmptyShadeView_trueWhenLockedShade() =
         testScope.runTest {
             val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
-            val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldIncludeFooterView by collectFooterViewVisibility()
 
             // WHEN has no notifs
             activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -315,7 +318,7 @@
     @Test
     fun shouldIncludeFooterView_trueWhenShade() =
         testScope.runTest {
-            val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldIncludeFooterView by collectFooterViewVisibility()
             val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has notifs
@@ -333,7 +336,7 @@
     @Test
     fun shouldIncludeFooterView_trueWhenLockedShade() =
         testScope.runTest {
-            val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldIncludeFooterView by collectFooterViewVisibility()
             val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has notifs
@@ -351,7 +354,7 @@
     @Test
     fun shouldIncludeFooterView_falseWhenKeyguard() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldInclude by collectFooterViewVisibility()
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -366,7 +369,7 @@
     @Test
     fun shouldIncludeFooterView_falseWhenUserNotSetUp() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldInclude by collectFooterViewVisibility()
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -384,7 +387,7 @@
     @Test
     fun shouldIncludeFooterView_falseWhenStartingToSleep() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldInclude by collectFooterViewVisibility()
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -402,7 +405,7 @@
     @Test
     fun shouldIncludeFooterView_falseWhenQsExpandedDefault() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldInclude by collectFooterViewVisibility()
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -421,7 +424,7 @@
     @Test
     fun shouldIncludeFooterView_trueWhenQsExpandedSplitShade() =
         testScope.runTest {
-            val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldIncludeFooterView by collectFooterViewVisibility()
             val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
 
             // WHEN has notifs
@@ -444,7 +447,7 @@
     @Test
     fun shouldIncludeFooterView_falseWhenRemoteInputActive() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldInclude by collectFooterViewVisibility()
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -462,7 +465,7 @@
     @Test
     fun shouldIncludeFooterView_animatesWhenShade() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldInclude by collectFooterViewVisibility()
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -478,7 +481,7 @@
     @Test
     fun shouldIncludeFooterView_notAnimatingOnKeyguard() =
         testScope.runTest {
-            val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+            val shouldInclude by collectFooterViewVisibility()
 
             // WHEN has notifs
             activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -492,6 +495,22 @@
         }
 
     @Test
+    @EnableSceneContainer
+    fun shouldShowFooterView_falseWhenShadeIsClosed() =
+        testScope.runTest {
+            val shouldShow by collectLastValue(underTest.shouldShowFooterView)
+
+            // WHEN shade is closed
+            fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            shadeTestUtil.setShadeExpansion(0f)
+            runCurrent()
+
+            // THEN footer is hidden
+            assertThat(shouldShow?.value).isFalse()
+        }
+
+    @Test
+    @DisableSceneContainer
     fun shouldHideFooterView_trueWhenShadeIsClosed() =
         testScope.runTest {
             val shouldHide by collectLastValue(underTest.shouldHideFooterView)
@@ -506,6 +525,7 @@
         }
 
     @Test
+    @DisableSceneContainer
     fun shouldHideFooterView_falseWhenShadeIsOpen() =
         testScope.runTest {
             val shouldHide by collectLastValue(underTest.shouldHideFooterView)
@@ -520,6 +540,7 @@
         }
 
     @Test
+    @DisableSceneContainer
     fun shouldHideFooterView_falseWhenQSPartiallyOpen() =
         testScope.runTest {
             val shouldHide by collectLastValue(underTest.shouldHideFooterView)
@@ -642,4 +663,10 @@
 
             assertThat(animationsEnabled).isTrue()
         }
+
+    private fun TestScope.collectFooterViewVisibility() =
+        collectLastValue(
+            if (SceneContainerFlag.isEnabled) underTest.shouldShowFooterView
+            else underTest.shouldIncludeFooterView
+        )
 }
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 141d035..e1808fa 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -2002,10 +2002,10 @@
     <!-- Shadow for dream overlay status bar complications -->
     <dimen name="dream_overlay_status_bar_key_text_shadow_dx">0.5dp</dimen>
     <dimen name="dream_overlay_status_bar_key_text_shadow_dy">0.5dp</dimen>
-    <dimen name="dream_overlay_status_bar_key_text_shadow_radius">1dp</dimen>
+    <dimen name="dream_overlay_status_bar_key_text_shadow_radius">3dp</dimen>
     <dimen name="dream_overlay_status_bar_ambient_text_shadow_dx">0.5dp</dimen>
     <dimen name="dream_overlay_status_bar_ambient_text_shadow_dy">0.5dp</dimen>
-    <dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">2dp</dimen>
+    <dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">3dp</dimen>
     <dimen name="dream_overlay_icon_inset_dimen">0dp</dimen>
 
     <!-- Default device corner radius, used for assist UI -->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index dd84bc6..92e5432 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -271,7 +271,8 @@
                         mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
                         keyguardSecurityCallback, mLatencyTracker, mFalsingCollector,
                         emergencyButtonController, mMessageAreaControllerFactory,
-                        mDevicePostureController, mFeatureFlags, mSelectedUserInteractor);
+                        mDevicePostureController, mFeatureFlags, mSelectedUserInteractor,
+                        mMSDLPlayer);
             } else if (keyguardInputView instanceof KeyguardPasswordView) {
                 return new KeyguardPasswordViewController((KeyguardPasswordView) keyguardInputView,
                         mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index caa74780..f74d93e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -36,6 +36,7 @@
 import com.android.internal.widget.LockscreenCredential;
 import com.android.keyguard.EmergencyButtonController.EmergencyButtonCallback;
 import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.systemui.bouncer.ui.helper.BouncerHapticHelper;
 import com.android.systemui.classifier.FalsingClassifier;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.flags.FeatureFlags;
@@ -43,6 +44,8 @@
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
 
+import com.google.android.msdl.domain.MSDLPlayer;
+
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -67,6 +70,7 @@
     private LockPatternView mLockPatternView;
     private CountDownTimer mCountdownTimer;
     private AsyncTask<?, ?, ?> mPendingLockCheck;
+    private MSDLPlayer mMSDLPlayer;
 
     private EmergencyButtonCallback mEmergencyButtonCallback = new EmergencyButtonCallback() {
         @Override
@@ -75,6 +79,10 @@
         }
     };
 
+    private final LockPatternView.ExternalHapticsPlayer mExternalHapticsPlayer = () -> {
+        BouncerHapticHelper.INSTANCE.playPatternDotFeedback(mMSDLPlayer, mView);
+    };
+
     /**
      * Useful for clearing out the wrong pattern after a delay
      */
@@ -166,6 +174,10 @@
                 boolean isValidPattern) {
             boolean dismissKeyguard = mSelectedUserInteractor.getSelectedUserId() == userId;
             if (matched) {
+                BouncerHapticHelper.INSTANCE.playMSDLAuthenticationFeedback(
+                        /* authenticationSucceeded= */true,
+                        /* player =*/mMSDLPlayer
+                );
                 getKeyguardSecurityCallback().reportUnlockAttempt(userId, true, 0);
                 if (dismissKeyguard) {
                     mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct);
@@ -173,6 +185,10 @@
                     getKeyguardSecurityCallback().dismiss(true, userId, SecurityMode.Pattern);
                 }
             } else {
+                BouncerHapticHelper.INSTANCE.playMSDLAuthenticationFeedback(
+                        /* authenticationSucceeded= */false,
+                        /* player =*/mMSDLPlayer
+                );
                 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
                 if (isValidPattern) {
                     getKeyguardSecurityCallback().reportUnlockAttempt(userId, false, timeoutMs);
@@ -200,7 +216,7 @@
             EmergencyButtonController emergencyButtonController,
             KeyguardMessageAreaController.Factory messageAreaControllerFactory,
             DevicePostureController postureController, FeatureFlags featureFlags,
-            SelectedUserInteractor selectedUserInteractor) {
+            SelectedUserInteractor selectedUserInteractor, MSDLPlayer msdlPlayer) {
         super(view, securityMode, keyguardSecurityCallback, emergencyButtonController,
                 messageAreaControllerFactory, featureFlags, selectedUserInteractor);
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
@@ -212,6 +228,7 @@
                 featureFlags.isEnabled(LOCKSCREEN_ENABLE_LANDSCAPE));
         mLockPatternView = mView.findViewById(R.id.lockPatternView);
         mPostureController = postureController;
+        mMSDLPlayer = msdlPlayer;
     }
 
     @Override
@@ -249,6 +266,7 @@
         if (deadline != 0) {
             handleAttemptLockout(deadline);
         }
+        mLockPatternView.setExternalHapticsPlayer(mExternalHapticsPlayer);
     }
 
     @Override
@@ -262,6 +280,7 @@
             cancelBtn.setOnClickListener(null);
         }
         mPostureController.removeCallback(mPostureCallback);
+        mLockPatternView.setExternalHapticsPlayer(null);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java b/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java
index 443441f..eb4de68 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/MirrorWindowControl.java
@@ -18,6 +18,9 @@
 
 import static android.view.WindowManager.LayoutParams;
 
+import static com.android.app.viewcapture.ViewCaptureFactory.getViewCaptureAwareWindowManagerInstance;
+import static com.android.systemui.Flags.enableViewCaptureTracing;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -29,8 +32,8 @@
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.view.WindowManager;
 
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
 import com.android.systemui.res.R;
 
 /**
@@ -70,11 +73,12 @@
      * @see #setDefaultPosition(LayoutParams)
      */
     private final Point mControlPosition = new Point();
-    private final WindowManager mWindowManager;
+    private final ViewCaptureAwareWindowManager mWindowManager;
 
     MirrorWindowControl(Context context) {
         mContext = context;
-        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+        mWindowManager = getViewCaptureAwareWindowManagerInstance(mContext,
+                enableViewCaptureTracing());
     }
 
     public void setWindowDelegate(@Nullable MirrorWindowDelegate windowDelegate) {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/model/CaptioningModel.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/CaptioningModel.kt
new file mode 100644
index 0000000..4eb2274
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/CaptioningModel.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.data.model
+
+data class CaptioningModel(
+    val isSystemAudioCaptioningUiEnabled: Boolean,
+    val isSystemAudioCaptioningEnabled: Boolean,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt
index bf749d4..5414b62 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/CaptioningRepository.kt
@@ -16,98 +16,90 @@
 
 package com.android.systemui.accessibility.data.repository
 
+import android.annotation.SuppressLint
 import android.view.accessibility.CaptioningManager
+import com.android.systemui.accessibility.data.model.CaptioningModel
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
 interface CaptioningRepository {
 
-    /** The system audio caption enabled state. */
-    val isSystemAudioCaptioningEnabled: StateFlow<Boolean>
+    /** Current state of Live Captions. */
+    val captioningModel: StateFlow<CaptioningModel?>
 
-    /** The system audio caption UI enabled state. */
-    val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean>
-
-    /** Sets [isSystemAudioCaptioningEnabled]. */
+    /** Sets [CaptioningModel.isSystemAudioCaptioningEnabled]. */
     suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean)
 }
 
-class CaptioningRepositoryImpl(
-    private val captioningManager: CaptioningManager,
-    private val backgroundCoroutineContext: CoroutineContext,
-    coroutineScope: CoroutineScope,
+@OptIn(ExperimentalCoroutinesApi::class)
+class CaptioningRepositoryImpl
+@Inject
+constructor(
+    private val userScopedCaptioningManagerProvider: UserScopedService<CaptioningManager>,
+    userRepository: UserRepository,
+    @Background private val backgroundCoroutineContext: CoroutineContext,
+    @Application coroutineScope: CoroutineScope,
 ) : CaptioningRepository {
 
-    private val captioningChanges: SharedFlow<CaptioningChange> =
-        callbackFlow {
-                val listener = CaptioningChangeProducingListener(this)
-                captioningManager.addCaptioningChangeListener(listener)
-                awaitClose { captioningManager.removeCaptioningChangeListener(listener) }
-            }
-            .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0)
+    @SuppressLint("NonInjectedService") // this uses user-aware context
+    private val captioningManager: StateFlow<CaptioningManager?> =
+        userRepository.selectedUser
+            .map { userScopedCaptioningManagerProvider.forUser(it.userInfo.userHandle) }
+            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)
 
-    override val isSystemAudioCaptioningEnabled: StateFlow<Boolean> =
-        captioningChanges
-            .filterIsInstance(CaptioningChange.IsSystemAudioCaptioningEnabled::class)
-            .map { it.isEnabled }
-            .onStart { emit(captioningManager.isSystemAudioCaptioningEnabled) }
-            .stateIn(
-                coroutineScope,
-                SharingStarted.WhileSubscribed(),
-                captioningManager.isSystemAudioCaptioningEnabled,
-            )
-
-    override val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean> =
-        captioningChanges
-            .filterIsInstance(CaptioningChange.IsSystemUICaptioningEnabled::class)
-            .map { it.isEnabled }
-            .onStart { emit(captioningManager.isSystemAudioCaptioningUiEnabled) }
-            .stateIn(
-                coroutineScope,
-                SharingStarted.WhileSubscribed(),
-                captioningManager.isSystemAudioCaptioningUiEnabled,
-            )
+    override val captioningModel: StateFlow<CaptioningModel?> =
+        captioningManager
+            .filterNotNull()
+            .flatMapLatest { it.captioningModel() }
+            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)
 
     override suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean) {
         withContext(backgroundCoroutineContext) {
-            captioningManager.isSystemAudioCaptioningEnabled = isEnabled
+            captioningManager.value?.isSystemAudioCaptioningEnabled = isEnabled
         }
     }
 
-    private sealed interface CaptioningChange {
+    private fun CaptioningManager.captioningModel(): Flow<CaptioningModel> {
+        return conflatedCallbackFlow {
+                val listener =
+                    object : CaptioningManager.CaptioningChangeListener() {
 
-        data class IsSystemAudioCaptioningEnabled(val isEnabled: Boolean) : CaptioningChange
+                        override fun onSystemAudioCaptioningChanged(enabled: Boolean) {
+                            trySend(Unit)
+                        }
 
-        data class IsSystemUICaptioningEnabled(val isEnabled: Boolean) : CaptioningChange
-    }
-
-    private class CaptioningChangeProducingListener(
-        private val scope: ProducerScope<CaptioningChange>
-    ) : CaptioningManager.CaptioningChangeListener() {
-
-        override fun onSystemAudioCaptioningChanged(enabled: Boolean) {
-            emitChange(CaptioningChange.IsSystemAudioCaptioningEnabled(enabled))
-        }
-
-        override fun onSystemAudioCaptioningUiChanged(enabled: Boolean) {
-            emitChange(CaptioningChange.IsSystemUICaptioningEnabled(enabled))
-        }
-
-        private fun emitChange(change: CaptioningChange) {
-            scope.launch { scope.send(change) }
-        }
+                        override fun onSystemAudioCaptioningUiChanged(enabled: Boolean) {
+                            trySend(Unit)
+                        }
+                    }
+                addCaptioningChangeListener(listener)
+                awaitClose { removeCaptioningChangeListener(listener) }
+            }
+            .onStart { emit(Unit) }
+            .map {
+                CaptioningModel(
+                    isSystemAudioCaptioningEnabled = isSystemAudioCaptioningEnabled,
+                    isSystemAudioCaptioningUiEnabled = isSystemAudioCaptioningUiEnabled,
+                )
+            }
+            .flowOn(backgroundCoroutineContext)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt b/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt
index 1d493c6..840edf4 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/CaptioningInteractor.kt
@@ -17,16 +17,22 @@
 package com.android.systemui.accessibility.domain.interactor
 
 import com.android.systemui.accessibility.data.repository.CaptioningRepository
-import kotlinx.coroutines.flow.StateFlow
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
 
-class CaptioningInteractor(private val repository: CaptioningRepository) {
+@SysUISingleton
+class CaptioningInteractor @Inject constructor(private val repository: CaptioningRepository) {
 
-    val isSystemAudioCaptioningEnabled: StateFlow<Boolean>
-        get() = repository.isSystemAudioCaptioningEnabled
+    val isSystemAudioCaptioningEnabled: Flow<Boolean> =
+        repository.captioningModel.filterNotNull().map { it.isSystemAudioCaptioningEnabled }
 
-    val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean>
-        get() = repository.isSystemAudioCaptioningUiEnabled
+    val isSystemAudioCaptioningUiEnabled: Flow<Boolean> =
+        repository.captioningModel.filterNotNull().map { it.isSystemAudioCaptioningUiEnabled }
 
-    suspend fun setIsSystemAudioCaptioningEnabled(enabled: Boolean) =
+    suspend fun setIsSystemAudioCaptioningEnabled(enabled: Boolean) {
         repository.setIsSystemAudioCaptioningEnabled(enabled)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticHelper.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticHelper.kt
new file mode 100644
index 0000000..1faacff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticHelper.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.ui.helper
+
+import android.view.HapticFeedbackConstants
+import android.view.View
+import com.android.keyguard.AuthInteractionProperties
+import com.android.systemui.Flags
+//noinspection CleanArchitectureDependencyViolation: Data layer only referenced for this enum class
+import com.google.android.msdl.data.model.MSDLToken
+import com.google.android.msdl.domain.MSDLPlayer
+
+/** A helper object to deliver haptic feedback in bouncer interactions. */
+object BouncerHapticHelper {
+
+    private val authInteractionProperties = AuthInteractionProperties()
+
+    /**
+     * Deliver MSDL feedback as a result of authenticating through a bouncer.
+     *
+     * @param[authenticationSucceeded] Whether the authentication was successful or not.
+     * @param[player] The [MSDLPlayer] that delivers the correct feedback.
+     */
+    fun playMSDLAuthenticationFeedback(
+        authenticationSucceeded: Boolean,
+        player: MSDLPlayer?,
+    ) {
+        if (player == null || !Flags.msdlFeedback()) {
+            return
+        }
+
+        val token =
+            if (authenticationSucceeded) {
+                MSDLToken.UNLOCK
+            } else {
+                MSDLToken.FAILURE
+            }
+        player.playToken(token, authInteractionProperties)
+    }
+
+    /**
+     * Deliver feedback when dragging through cells in the pattern bouncer. This function can play
+     * MSDL feedback using a [MSDLPlayer], or fallback to a default haptic feedback using the
+     * [View.performHapticFeedback] API and a [View].
+     *
+     * @param[player] [MSDLPlayer] for MSDL feedback.
+     * @param[view] A [View] for default haptic feedback using [View.performHapticFeedback]
+     */
+    fun playPatternDotFeedback(player: MSDLPlayer?, view: View?) {
+        if (player == null || !Flags.msdlFeedback()) {
+            view?.performHapticFeedback(
+                HapticFeedbackConstants.VIRTUAL_KEY,
+                HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
+            )
+        } else {
+            player.playToken(MSDLToken.DRAG_INDICATOR)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
index c69cea4..04393fe 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.CoreStartable
 import com.android.systemui.Flags.glanceableHubAllowKeyguardWhenDreaming
 import com.android.systemui.Flags.restartDreamOnUnocclude
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
@@ -55,6 +56,7 @@
     private val keyguardInteractor: KeyguardInteractor,
     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val dreamManager: DreamManager,
+    private val communalSceneInteractor: CommunalSceneInteractor,
     @Background private val bgScope: CoroutineScope,
 ) : CoreStartable {
     /** Flow that emits when the dream should be started underneath the glanceable hub. */
@@ -66,6 +68,8 @@
                 not(keyguardInteractor.isDreaming),
                 // TODO(b/362830856): Remove this workaround.
                 keyguardInteractor.isKeyguardShowing,
+                not(communalSceneInteractor.isLaunchingWidget),
+                not(keyguardInteractor.isKeyguardOccluded),
             )
             .filter { it }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 21a704d..8818c3a 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -202,6 +202,13 @@
         return context.getSystemService(CaptioningManager.class);
     }
 
+    @Provides
+    @Singleton
+    static UserScopedService<CaptioningManager> provideUserScopedCaptioningManager(
+            Context context) {
+        return new UserScopedServiceImpl<>(context, CaptioningManager.class);
+    }
+
     /** */
     @Provides
     @Singleton
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 1bc91ca..67625d0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -81,6 +81,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardDismissInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardStateCallbackInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardWakeDirectlyToGoneInteractor;
 import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindParamsApplier;
 import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindViewBinder;
@@ -126,6 +127,7 @@
     private final Lazy<DeviceEntryInteractor> mDeviceEntryInteractorLazy;
     private final Executor mMainExecutor;
     private final Lazy<KeyguardStateCallbackStartable> mKeyguardStateCallbackStartableLazy;
+    private final KeyguardStateCallbackInteractor mKeyguardStateCallbackInteractor;
 
     private static RemoteAnimationTarget[] wrap(TransitionInfo info, boolean wallpapers,
             SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap,
@@ -350,7 +352,8 @@
             Lazy<KeyguardStateCallbackStartable> keyguardStateCallbackStartableLazy,
             KeyguardWakeDirectlyToGoneInteractor keyguardWakeDirectlyToGoneInteractor,
             KeyguardDismissInteractor keyguardDismissInteractor,
-            Lazy<DeviceEntryInteractor> deviceEntryInteractorLazy) {
+            Lazy<DeviceEntryInteractor> deviceEntryInteractorLazy,
+            KeyguardStateCallbackInteractor keyguardStateCallbackInteractor) {
         super();
         mKeyguardViewMediator = keyguardViewMediator;
         mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher;
@@ -363,6 +366,7 @@
         mSceneInteractorLazy = sceneInteractorLazy;
         mMainExecutor = mainExecutor;
         mKeyguardStateCallbackStartableLazy = keyguardStateCallbackStartableLazy;
+        mKeyguardStateCallbackInteractor = keyguardStateCallbackInteractor;
         mDeviceEntryInteractorLazy = deviceEntryInteractorLazy;
 
         if (KeyguardWmStateRefactor.isEnabled()) {
@@ -455,6 +459,8 @@
             checkPermission();
             if (SceneContainerFlag.isEnabled()) {
                 mKeyguardStateCallbackStartableLazy.get().addCallback(callback);
+            } else if (KeyguardWmStateRefactor.isEnabled()) {
+                mKeyguardStateCallbackInteractor.addCallback(callback);
             } else {
                 mKeyguardViewMediator.addStateMonitorCallback(callback);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index d38c952..3b1569d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -2288,6 +2288,10 @@
     }
 
     private void updateInputRestrictedLocked() {
+        if (KeyguardWmStateRefactor.isEnabled()) {
+            return;
+        }
+
         boolean inputRestricted = isInputRestricted();
         if (mInputRestricted != inputRestricted) {
             mInputRestricted = inputRestricted;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index db5a63b..58c8a04 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -73,7 +73,7 @@
         if (SceneContainerFlag.isEnabled) return
         listenForGoneToAodOrDozing()
         listenForGoneToDreaming()
-        listenForGoneToLockscreenOrHub()
+        listenForGoneToLockscreenOrHubOrOccluded()
         listenForGoneToOccluded()
         listenForGoneToDreamingLockscreenHosted()
     }
@@ -89,22 +89,19 @@
      */
     private fun listenForGoneToOccluded() {
         scope.launch("$TAG#listenForGoneToOccluded") {
-            keyguardInteractor.showDismissibleKeyguard
-                .filterRelevantKeyguardState()
-                .sample(keyguardInteractor.isKeyguardOccluded, ::Pair)
-                .collect { (_, isKeyguardOccluded) ->
-                    if (isKeyguardOccluded) {
-                        startTransitionTo(
-                            KeyguardState.OCCLUDED,
-                            ownerReason = "Dismissible keyguard with occlusion"
-                        )
-                    }
+            keyguardInteractor.showDismissibleKeyguard.filterRelevantKeyguardState().collect {
+                if (keyguardInteractor.isKeyguardOccluded.value) {
+                    startTransitionTo(
+                        KeyguardState.OCCLUDED,
+                        ownerReason = "Dismissible keyguard with occlusion"
+                    )
                 }
+            }
         }
     }
 
     // Primarily for when the user chooses to lock down the device
-    private fun listenForGoneToLockscreenOrHub() {
+    private fun listenForGoneToLockscreenOrHubOrOccluded() {
         if (KeyguardWmStateRefactor.isEnabled) {
             scope.launch("$TAG#listenForGoneToLockscreenOrHub") {
                 biometricSettingsRepository.isCurrentUserInLockdown
@@ -137,7 +134,7 @@
                     }
             }
         } else {
-            scope.launch("$TAG#listenForGoneToLockscreenOrHub") {
+            scope.launch("$TAG#listenForGoneToLockscreenOrHubOrOccluded") {
                 keyguardInteractor.isKeyguardShowing
                     .filterRelevantKeyguardStateAnd { isKeyguardShowing -> isKeyguardShowing }
                     .sample(communalSceneInteractor.isIdleOnCommunalNotEditMode, ::Pair)
@@ -145,6 +142,8 @@
                         val to =
                             if (isIdleOnCommunal) {
                                 KeyguardState.GLANCEABLE_HUB
+                            } else if (keyguardInteractor.isKeyguardOccluded.value) {
+                                KeyguardState.OCCLUDED
                             } else {
                                 KeyguardState.LOCKSCREEN
                             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt
new file mode 100644
index 0000000..420fbd4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardStateCallbackInteractor.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import android.os.DeadObjectException
+import android.os.RemoteException
+import com.android.internal.policy.IKeyguardStateCallback
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyguard.KeyguardWmStateRefactor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+/**
+ * Updates KeyguardStateCallbacks provided to KeyguardService with KeyguardTransitionInteractor
+ * state.
+ *
+ * This borrows heavily from [KeyguardStateCallbackStartable], which requires Flexiglass. This class
+ * can be removed after Flexiglass launches.
+ */
+@SysUISingleton
+class KeyguardStateCallbackInteractor
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val selectedUserInteractor: SelectedUserInteractor,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) : CoreStartable {
+    private val callbacks = mutableListOf<IKeyguardStateCallback>()
+
+    override fun start() {
+        if (!KeyguardWmStateRefactor.isEnabled || SceneContainerFlag.isEnabled) {
+            return
+        }
+
+        applicationScope.launch {
+            combine(
+                selectedUserInteractor.selectedUser,
+                keyguardTransitionInteractor.currentKeyguardState,
+                ::Pair
+            ).collectLatest { (selectedUser, currentState) ->
+                val iterator = callbacks.iterator()
+                    withContext(backgroundDispatcher) {
+                        while (iterator.hasNext()) {
+                            val callback = iterator.next()
+                            try {
+                                 callback.onShowingStateChanged(
+                                    currentState != KeyguardState.GONE,
+                                    selectedUser
+                                )
+                                callback.onInputRestrictedStateChanged(
+                                    currentState != KeyguardState.GONE)
+                            } catch (e: RemoteException) {
+                                if (e is DeadObjectException) {
+                                    iterator.remove()
+                                }
+                            }
+                        }
+                    }
+                }
+        }
+    }
+
+    fun addCallback(callback: IKeyguardStateCallback) {
+        KeyguardWmStateRefactor.isUnexpectedlyInLegacyMode()
+        callbacks.add(callback)
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
index d9c48fa..25b8fd3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -29,6 +29,7 @@
     private val auditLogger: KeyguardTransitionAuditLogger,
     private val bootInteractor: KeyguardTransitionBootInteractor,
     private val statusBarDisableFlagsInteractor: StatusBarDisableFlagsInteractor,
+    private val keyguardStateCallbackInteractor: KeyguardStateCallbackInteractor,
 ) : CoreStartable {
 
     override fun start() {
@@ -55,6 +56,7 @@
         auditLogger.start()
         bootInteractor.start()
         statusBarDisableFlagsInteractor.start()
+        keyguardStateCallbackInteractor.start()
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
index 24c57be..4ad437c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -76,6 +76,7 @@
 import com.android.systemui.media.controls.shared.model.MediaButton
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.MediaDeviceData
+import com.android.systemui.media.controls.shared.model.MediaNotificationAction
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
 import com.android.systemui.media.controls.ui.view.MediaViewHolder
@@ -943,7 +944,7 @@
                     desc.subtitle,
                     desc.title,
                     artworkIcon,
-                    listOf(mediaAction),
+                    listOf(),
                     listOf(0),
                     MediaButton(playOrPause = mediaAction),
                     packageName,
@@ -1074,13 +1075,13 @@
         }
 
         // Control buttons
-        // If flag is enabled and controller has a PlaybackState, create actions from session info
+        // If controller has a PlaybackState, create actions from session info
         // Otherwise, use the notification actions
-        var actionIcons: List<MediaAction> = emptyList()
+        var actionIcons: List<MediaNotificationAction> = emptyList()
         var actionsToShowCollapsed: List<Int> = emptyList()
         val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
         if (semanticActions == null) {
-            val actions = createActionsFromNotification(context, activityStarter, sbn)
+            val actions = createActionsFromNotification(context, sbn)
             actionIcons = actions.first
             actionsToShowCollapsed = actions.second
         }
@@ -1464,7 +1465,7 @@
         val updated =
             data.copy(
                 token = null,
-                actions = actions,
+                actions = listOf(),
                 semanticActions = MediaButton(playOrPause = resumeAction),
                 actionsToShowInCompact = listOf(0),
                 active = false,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
index bcf748e..f2825d0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaActions.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.media.controls.shared.MediaControlDrawables
 import com.android.systemui.media.controls.shared.model.MediaAction
 import com.android.systemui.media.controls.shared.model.MediaButton
+import com.android.systemui.media.controls.shared.model.MediaNotificationAction
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
@@ -217,11 +218,10 @@
 /** Generate action buttons based on notification actions */
 fun createActionsFromNotification(
     context: Context,
-    activityStarter: ActivityStarter,
     sbn: StatusBarNotification
-): Pair<List<MediaAction>, List<Int>> {
+): Pair<List<MediaNotificationAction>, List<Int>> {
     val notif = sbn.notification
-    val actionIcons: MutableList<MediaAction> = ArrayList()
+    val actionIcons: MutableList<MediaNotificationAction> = ArrayList()
     val actions = notif.actions
     var actionsToShowCollapsed =
         notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList()
@@ -250,25 +250,6 @@
                 continue
             }
 
-            val runnable =
-                action.actionIntent?.let { actionIntent ->
-                    Runnable {
-                        when {
-                            actionIntent.isActivity ->
-                                activityStarter.startPendingIntentDismissingKeyguard(
-                                    action.actionIntent
-                                )
-                            action.isAuthenticationRequired ->
-                                activityStarter.dismissKeyguardThenExecute(
-                                    { sendPendingIntent(action.actionIntent) },
-                                    {},
-                                    true
-                                )
-                            else -> sendPendingIntent(actionIntent)
-                        }
-                    }
-                }
-
             val themeText =
                 com.android.settingslib.Utils.getColorAttr(
                         context,
@@ -285,13 +266,53 @@
                     .setTint(themeText)
                     .loadDrawable(context)
 
-            val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null)
+            val mediaAction =
+                MediaNotificationAction(
+                    action.isAuthenticationRequired,
+                    action.actionIntent,
+                    mediaActionIcon,
+                    action.title
+                )
             actionIcons.add(mediaAction)
         }
     }
     return Pair(actionIcons, actionsToShowCollapsed)
 }
 
+/**
+ * Converts [MediaNotificationAction] list into [MediaAction] list
+ *
+ * @param actions list of [MediaNotificationAction]
+ * @param activityStarter starter for activities
+ * @return list of [MediaAction]
+ */
+fun getNotificationActions(
+    actions: List<MediaNotificationAction>,
+    activityStarter: ActivityStarter
+): List<MediaAction> {
+    return actions.map { action ->
+        val runnable =
+            action.actionIntent?.let { actionIntent ->
+                Runnable {
+                    when {
+                        actionIntent.isActivity ->
+                            activityStarter.startPendingIntentDismissingKeyguard(
+                                action.actionIntent
+                            )
+                        action.isAuthenticationRequired ->
+                            activityStarter.dismissKeyguardThenExecute(
+                                { sendPendingIntent(action.actionIntent) },
+                                {},
+                                true
+                            )
+                        else -> sendPendingIntent(actionIntent)
+                    }
+                }
+            }
+        MediaAction(action.icon, runnable, action.contentDescription, background = null)
+    }
+}
+
 private fun sendPendingIntent(intent: PendingIntent): Boolean {
     return try {
         intent.send(
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
index f9fef8e..53cc15b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt
@@ -54,10 +54,10 @@
 import com.android.systemui.media.controls.shared.model.MediaButton
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.MediaDeviceData
+import com.android.systemui.media.controls.shared.model.MediaNotificationAction
 import com.android.systemui.media.controls.util.MediaControllerFactory
 import com.android.systemui.media.controls.util.MediaDataUtils
 import com.android.systemui.media.controls.util.MediaFlags
-import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
 import com.android.systemui.statusbar.notification.row.HybridGroupManager
@@ -80,7 +80,6 @@
     @Application val context: Context,
     @Main val mainDispatcher: CoroutineDispatcher,
     @Background val backgroundScope: CoroutineScope,
-    private val activityStarter: ActivityStarter,
     private val mediaControllerFactory: MediaControllerFactory,
     private val mediaFlags: MediaFlags,
     private val imageLoader: ImageLoader,
@@ -209,15 +208,14 @@
             val device: MediaDeviceData? = getDeviceInfoForRemoteCast(key, sbn)
 
             // Control buttons
-            // If flag is enabled and controller has a PlaybackState, create actions from session
-            // info
+            // If controller has a PlaybackState, create actions from session info
             // Otherwise, use the notification actions
-            var actionIcons: List<MediaAction> = emptyList()
+            var actionIcons: List<MediaNotificationAction> = emptyList()
             var actionsToShowCollapsed: List<Int> = emptyList()
             val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
             logD(TAG) { "Semantic actions: $semanticActions" }
             if (semanticActions == null) {
-                val actions = createActionsFromNotification(context, activityStarter, sbn)
+                val actions = createActionsFromNotification(context, sbn)
                 actionIcons = actions.first
                 actionsToShowCollapsed = actions.second
                 logD(TAG) { "[!!] Semantic actions: $semanticActions" }
@@ -329,7 +327,7 @@
                 artist = desc.subtitle,
                 song = desc.title,
                 artworkIcon = artworkIcon,
-                actionIcons = listOf(mediaAction),
+                actionIcons = listOf(),
                 actionsToShowInCompact = listOf(0),
                 semanticActions = MediaButton(playOrPause = mediaAction),
                 token = token,
@@ -514,7 +512,7 @@
         val artist: CharSequence?,
         val song: CharSequence?,
         val artworkIcon: Icon?,
-        val actionIcons: List<MediaAction>,
+        val actionIcons: List<MediaNotificationAction>,
         val actionsToShowInCompact: List<Int>,
         val semanticActions: MediaButton?,
         val token: MediaSession.Token?,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index 4555810..5f0a9f8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -71,12 +71,14 @@
 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification
 import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
 import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
+import com.android.systemui.media.controls.shared.MediaLogger
 import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
 import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC
 import com.android.systemui.media.controls.shared.model.MediaAction
 import com.android.systemui.media.controls.shared.model.MediaButton
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.MediaDeviceData
+import com.android.systemui.media.controls.shared.model.MediaNotificationAction
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
 import com.android.systemui.media.controls.ui.view.MediaViewHolder
@@ -149,6 +151,7 @@
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     private val mediaDataRepository: MediaDataRepository,
     private val mediaDataLoader: dagger.Lazy<MediaDataLoader>,
+    private val mediaLogger: MediaLogger,
 ) : CoreStartable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
 
     companion object {
@@ -228,6 +231,7 @@
         keyguardUpdateMonitor: KeyguardUpdateMonitor,
         mediaDataRepository: MediaDataRepository,
         mediaDataLoader: dagger.Lazy<MediaDataLoader>,
+        mediaLogger: MediaLogger,
     ) : this(
         context,
         applicationScope,
@@ -253,6 +257,7 @@
         keyguardUpdateMonitor,
         mediaDataRepository,
         mediaDataLoader,
+        mediaLogger,
     )
 
     private val appChangeReceiver =
@@ -794,7 +799,7 @@
                     desc.subtitle,
                     desc.title,
                     artworkIcon,
-                    listOf(mediaAction),
+                    listOf(),
                     listOf(0),
                     MediaButton(playOrPause = mediaAction),
                     packageName,
@@ -832,12 +837,48 @@
                 return@withContext
             }
 
-            val currentEntry = mediaDataRepository.mediaEntries.value[key]
-            val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
-            val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
-            val resumeAction: Runnable? = currentEntry?.resumeAction
-            val hasCheckedForResume = currentEntry?.hasCheckedForResume == true
-            val active = currentEntry?.active ?: true
+            val mediaController = mediaControllerFactory.create(result.token!!)
+            val oldEntry = mediaDataRepository.mediaEntries.value[key]
+            val instanceId = oldEntry?.instanceId ?: logger.getNewInstanceId()
+            val createdTimestampMillis = oldEntry?.createdTimestampMillis ?: 0L
+            val resumeAction: Runnable? = oldEntry?.resumeAction
+            val hasCheckedForResume = oldEntry?.hasCheckedForResume == true
+            val active = oldEntry?.active ?: true
+
+            val mediaData =
+                MediaData(
+                    userId = sbn.normalizedUserId,
+                    initialized = true,
+                    app = result.appName,
+                    appIcon = result.appIcon,
+                    artist = result.artist,
+                    song = result.song,
+                    artwork = result.artworkIcon,
+                    actions = result.actionIcons,
+                    actionsToShowInCompact = result.actionsToShowInCompact,
+                    semanticActions = result.semanticActions,
+                    packageName = sbn.packageName,
+                    token = result.token,
+                    clickIntent = result.clickIntent,
+                    device = result.device,
+                    active = active,
+                    resumeAction = resumeAction,
+                    playbackLocation = result.playbackLocation,
+                    notificationKey = key,
+                    hasCheckedForResume = hasCheckedForResume,
+                    isPlaying = result.isPlaying,
+                    isClearable = !sbn.isOngoing,
+                    lastActive = lastActive,
+                    createdTimestampMillis = createdTimestampMillis,
+                    instanceId = instanceId,
+                    appUid = result.appUid,
+                    isExplicit = result.isExplicit,
+                )
+
+            if (isSameMediaData(context, mediaController, mediaData, oldEntry)) {
+                mediaLogger.logDuplicateMediaNotification(key)
+                return@withContext
+            }
 
             // We need to log the correct media added.
             if (isNewlyActiveEntry) {
@@ -848,7 +889,7 @@
                     instanceId,
                     result.playbackLocation
                 )
-            } else if (result.playbackLocation != currentEntry?.playbackLocation) {
+            } else if (result.playbackLocation != oldEntry?.playbackLocation) {
                 logger.logPlaybackLocationChange(
                     result.appUid,
                     sbn.packageName,
@@ -857,40 +898,7 @@
                 )
             }
 
-            withContext(mainDispatcher) {
-                onMediaDataLoaded(
-                    key,
-                    oldKey,
-                    MediaData(
-                        userId = sbn.normalizedUserId,
-                        initialized = true,
-                        app = result.appName,
-                        appIcon = result.appIcon,
-                        artist = result.artist,
-                        song = result.song,
-                        artwork = result.artworkIcon,
-                        actions = result.actionIcons,
-                        actionsToShowInCompact = result.actionsToShowInCompact,
-                        semanticActions = result.semanticActions,
-                        packageName = sbn.packageName,
-                        token = result.token,
-                        clickIntent = result.clickIntent,
-                        device = result.device,
-                        active = active,
-                        resumeAction = resumeAction,
-                        playbackLocation = result.playbackLocation,
-                        notificationKey = key,
-                        hasCheckedForResume = hasCheckedForResume,
-                        isPlaying = result.isPlaying,
-                        isClearable = !sbn.isOngoing,
-                        lastActive = lastActive,
-                        createdTimestampMillis = createdTimestampMillis,
-                        instanceId = instanceId,
-                        appUid = result.appUid,
-                        isExplicit = result.isExplicit,
-                    )
-                )
-            }
+            withContext(mainDispatcher) { onMediaDataLoaded(key, oldKey, mediaData) }
         }
 
     @Deprecated("Cleanup when media_load_metadata_via_media_data_loader is cleaned up")
@@ -1001,13 +1009,13 @@
         }
 
         // Control buttons
-        // If flag is enabled and controller has a PlaybackState, create actions from session info
+        // If controller has a PlaybackState, create actions from session info
         // Otherwise, use the notification actions
-        var actionIcons: List<MediaAction> = emptyList()
+        var actionIcons: List<MediaNotificationAction> = emptyList()
         var actionsToShowCollapsed: List<Int> = emptyList()
         val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user)
         if (semanticActions == null) {
-            val actions = createActionsFromNotification(context, activityStarter, sbn)
+            val actions = createActionsFromNotification(context, sbn)
             actionIcons = actions.first
             actionsToShowCollapsed = actions.second
         }
@@ -1022,57 +1030,72 @@
             else MediaData.PLAYBACK_CAST_LOCAL
         val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) }
 
-        val currentEntry = mediaDataRepository.mediaEntries.value.get(key)
-        val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId()
+        val oldEntry = mediaDataRepository.mediaEntries.value.get(key)
+        val instanceId = oldEntry?.instanceId ?: logger.getNewInstanceId()
         val appUid = appInfo?.uid ?: Process.INVALID_UID
 
+        val lastActive = systemClock.elapsedRealtime()
+        val createdTimestampMillis = oldEntry?.createdTimestampMillis ?: 0L
+        val resumeAction: Runnable? = mediaDataRepository.mediaEntries.value[key]?.resumeAction
+        val hasCheckedForResume =
+            mediaDataRepository.mediaEntries.value[key]?.hasCheckedForResume == true
+        val active = mediaDataRepository.mediaEntries.value[key]?.active ?: true
+        var mediaData =
+            MediaData(
+                sbn.normalizedUserId,
+                true,
+                appName,
+                smallIcon,
+                artist,
+                song,
+                artWorkIcon,
+                actionIcons,
+                actionsToShowCollapsed,
+                semanticActions,
+                sbn.packageName,
+                token,
+                notif.contentIntent,
+                device,
+                active,
+                resumeAction = resumeAction,
+                playbackLocation = playbackLocation,
+                notificationKey = key,
+                hasCheckedForResume = hasCheckedForResume,
+                isPlaying = isPlaying,
+                isClearable = !sbn.isOngoing,
+                lastActive = lastActive,
+                createdTimestampMillis = createdTimestampMillis,
+                instanceId = instanceId,
+                appUid = appUid,
+                isExplicit = isExplicit,
+                smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()),
+            )
+
+        if (isSameMediaData(context, mediaController, mediaData, oldEntry)) {
+            mediaLogger.logDuplicateMediaNotification(key)
+            return
+        }
+
         if (isNewlyActiveEntry) {
             logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId)
             logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation)
-        } else if (playbackLocation != currentEntry?.playbackLocation) {
+        } else if (playbackLocation != oldEntry?.playbackLocation) {
             logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation)
         }
 
-        val lastActive = systemClock.elapsedRealtime()
-        val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L
         foregroundExecutor.execute {
-            val resumeAction: Runnable? = mediaDataRepository.mediaEntries.value[key]?.resumeAction
-            val hasCheckedForResume =
+            val oldResumeAction: Runnable? =
+                mediaDataRepository.mediaEntries.value[key]?.resumeAction
+            val oldHasCheckedForResume =
                 mediaDataRepository.mediaEntries.value[key]?.hasCheckedForResume == true
-            val active = mediaDataRepository.mediaEntries.value[key]?.active ?: true
-            onMediaDataLoaded(
-                key,
-                oldKey,
-                MediaData(
-                    sbn.normalizedUserId,
-                    true,
-                    appName,
-                    smallIcon,
-                    artist,
-                    song,
-                    artWorkIcon,
-                    actionIcons,
-                    actionsToShowCollapsed,
-                    semanticActions,
-                    sbn.packageName,
-                    token,
-                    notif.contentIntent,
-                    device,
-                    active,
-                    resumeAction = resumeAction,
-                    playbackLocation = playbackLocation,
-                    notificationKey = key,
-                    hasCheckedForResume = hasCheckedForResume,
-                    isPlaying = isPlaying,
-                    isClearable = !sbn.isOngoing,
-                    lastActive = lastActive,
-                    createdTimestampMillis = createdTimestampMillis,
-                    instanceId = instanceId,
-                    appUid = appUid,
-                    isExplicit = isExplicit,
-                    smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()),
+            val oldActive = mediaDataRepository.mediaEntries.value[key]?.active ?: true
+            mediaData =
+                mediaData.copy(
+                    resumeAction = oldResumeAction,
+                    hasCheckedForResume = oldHasCheckedForResume,
+                    active = oldActive
                 )
-            )
+            onMediaDataLoaded(key, oldKey, mediaData)
         }
     }
 
@@ -1402,7 +1425,7 @@
         val updated =
             data.copy(
                 token = null,
-                actions = actions,
+                actions = listOf(),
                 semanticActions = MediaButton(playOrPause = resumeAction),
                 actionsToShowInCompact = listOf(0),
                 active = false,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt
new file mode 100644
index 0000000..55d7b1d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.controls.domain.pipeline
+
+import android.annotation.WorkerThread
+import android.app.PendingIntent
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.media.session.MediaController
+import android.media.session.PlaybackState
+import android.util.Log
+import com.android.systemui.Flags.mediaControlsPostsOptimization
+import com.android.systemui.biometrics.Utils.toBitmap
+import com.android.systemui.media.controls.shared.model.MediaData
+
+private const val TAG = "MediaProcessingHelper"
+
+/**
+ * Compares [new] media data to [old] media data.
+ *
+ * @param context Context
+ * @param newController media controller of the new media data.
+ * @param new new media data.
+ * @param old old media data.
+ * @return whether new and old contain same data
+ */
+fun isSameMediaData(
+    context: Context,
+    newController: MediaController,
+    new: MediaData,
+    old: MediaData?
+): Boolean {
+    if (old == null || !mediaControlsPostsOptimization()) return false
+
+    return new.userId == old.userId &&
+        new.app == old.app &&
+        new.artist == old.artist &&
+        new.song == old.song &&
+        new.packageName == old.packageName &&
+        new.isExplicit == old.isExplicit &&
+        new.appUid == old.appUid &&
+        new.notificationKey == old.notificationKey &&
+        new.isPlaying == old.isPlaying &&
+        new.isClearable == old.isClearable &&
+        new.playbackLocation == old.playbackLocation &&
+        new.device == old.device &&
+        new.initialized == old.initialized &&
+        new.resumption == old.resumption &&
+        new.token == old.token &&
+        new.resumeProgress == old.resumeProgress &&
+        areClickIntentsEqual(new.clickIntent, old.clickIntent) &&
+        areActionsEqual(context, newController, new, old) &&
+        areIconsEqual(context, new.artwork, old.artwork) &&
+        areIconsEqual(context, new.appIcon, old.appIcon)
+}
+
+/** Returns whether actions lists are equal. */
+fun areCustomActionListsEqual(
+    first: List<PlaybackState.CustomAction>?,
+    second: List<PlaybackState.CustomAction>?
+): Boolean {
+    // Same object, or both null
+    if (first === second) {
+        return true
+    }
+
+    // Only one null, or different number of actions
+    if ((first == null || second == null) || (first.size != second.size)) {
+        return false
+    }
+
+    // Compare individual actions
+    first.asSequence().zip(second.asSequence()).forEach { (firstAction, secondAction) ->
+        if (!areCustomActionsEqual(firstAction, secondAction)) {
+            return false
+        }
+    }
+    return true
+}
+
+private fun areCustomActionsEqual(
+    firstAction: PlaybackState.CustomAction,
+    secondAction: PlaybackState.CustomAction
+): Boolean {
+    if (
+        firstAction.action != secondAction.action ||
+            firstAction.name != secondAction.name ||
+            firstAction.icon != secondAction.icon
+    ) {
+        return false
+    }
+
+    if ((firstAction.extras == null) != (secondAction.extras == null)) {
+        return false
+    }
+    if (firstAction.extras != null) {
+        firstAction.extras.keySet().forEach { key ->
+            if (firstAction.extras[key] != secondAction.extras[key]) {
+                return false
+            }
+        }
+    }
+    return true
+}
+
+@WorkerThread
+private fun areIconsEqual(context: Context, new: Icon?, old: Icon?): Boolean {
+    if (new == old) return true
+    if (new == null || old == null || new.type != old.type) return false
+    return if (new.type == Icon.TYPE_BITMAP || new.type == Icon.TYPE_ADAPTIVE_BITMAP) {
+        if (new.bitmap.isRecycled || old.bitmap.isRecycled) {
+            Log.e(TAG, "Cannot compare recycled bitmap")
+            return false
+        }
+        new.bitmap.sameAs(old.bitmap)
+    } else {
+        val newDrawable = new.loadDrawable(context)
+        val oldDrawable = old.loadDrawable(context)
+
+        return newDrawable?.toBitmap()?.sameAs(oldDrawable?.toBitmap()) ?: false
+    }
+}
+
+private fun areActionsEqual(
+    context: Context,
+    newController: MediaController,
+    new: MediaData,
+    old: MediaData
+): Boolean {
+    val oldState = MediaController(context, old.token!!).playbackState
+    return if (
+        new.semanticActions == null &&
+            old.semanticActions == null &&
+            new.actions.size == old.actions.size
+    ) {
+        var same = true
+        new.actions.asSequence().zip(old.actions.asSequence()).forEach {
+            if (
+                it.first.actionIntent?.intent?.filterEquals(it.second.actionIntent?.intent) !=
+                    true ||
+                    it.first.icon != it.second.icon ||
+                    it.first.contentDescription != it.second.contentDescription
+            ) {
+                same = false
+                return@forEach
+            }
+        }
+        same
+    } else if (new.semanticActions != null && old.semanticActions != null) {
+        oldState?.actions == newController.playbackState?.actions &&
+            areCustomActionListsEqual(
+                oldState?.customActions,
+                newController.playbackState?.customActions
+            )
+    } else {
+        false
+    }
+}
+
+private fun areClickIntentsEqual(newIntent: PendingIntent?, oldIntent: PendingIntent?): Boolean {
+    if ((newIntent == null && oldIntent == null) || newIntent === oldIntent) return true
+    if (newIntent == null || oldIntent == null) return false
+
+    return newIntent.intent?.filterEquals(oldIntent.intent) == true
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
index fc31903..275f1ee 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
@@ -16,12 +16,14 @@
 
 package com.android.systemui.media.controls.domain.pipeline
 
+import android.annotation.WorkerThread
 import android.media.session.MediaController
 import android.media.session.MediaSession
 import android.media.session.PlaybackState
 import android.os.SystemProperties
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
@@ -32,6 +34,7 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.time.SystemClock
+import java.util.concurrent.Executor
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 
@@ -49,6 +52,8 @@
 @Inject
 constructor(
     private val mediaControllerFactory: MediaControllerFactory,
+    @Background private val bgExecutor: Executor,
+    @Main private val uiExecutor: Executor,
     @Main private val mainExecutor: DelayableExecutor,
     private val logger: MediaTimeoutLogger,
     statusBarStateController: SysuiStatusBarStateController,
@@ -147,19 +152,21 @@
         }
 
         reusedListener?.let {
-            val wasPlaying = it.isPlaying()
-            logger.logUpdateListener(key, wasPlaying)
-            it.setMediaData(data)
-            it.key = key
-            mediaListeners[key] = it
-            if (wasPlaying != it.isPlaying()) {
-                // If a player becomes active because of a migration, we'll need to broadcast
-                // its state. Doing it now would lead to reentrant callbacks, so let's wait
-                // until we're done.
-                mainExecutor.execute {
-                    if (mediaListeners[key]?.isPlaying() == true) {
-                        logger.logDelayedUpdate(key)
-                        timeoutCallback.invoke(key, false /* timedOut */)
+            bgExecutor.execute {
+                val wasPlaying = it.isPlaying()
+                logger.logUpdateListener(key, wasPlaying)
+                it.setMediaData(data)
+                it.key = key
+                mediaListeners[key] = it
+                if (wasPlaying != it.isPlaying()) {
+                    // If a player becomes active because of a migration, we'll need to broadcast
+                    // its state. Doing it now would lead to reentrant callbacks, so let's wait
+                    // until we're done.
+                    mainExecutor.execute {
+                        if (mediaListeners[key]?.isPlaying() == true) {
+                            logger.logDelayedUpdate(key)
+                            timeoutCallback.invoke(key, false /* timedOut */)
+                        }
                     }
                 }
             }
@@ -217,18 +224,20 @@
             private set
 
         fun Int.isPlaying() = isPlayingState(this)
+
         fun isPlaying() = lastState?.state?.isPlaying() ?: false
 
         init {
-            setMediaData(data)
+            bgExecutor.execute { setMediaData(data) }
         }
 
         fun destroy() {
-            mediaController?.unregisterCallback(this)
+            bgExecutor.execute { mediaController?.unregisterCallback(this) }
             cancellation?.run()
             destroyed = true
         }
 
+        @WorkerThread
         fun setMediaData(data: MediaData) {
             sessionToken = data.token
             destroyed = false
@@ -258,7 +267,7 @@
             if (resumption == true) {
                 // Some apps create a session when MBS is queried. We should unregister the
                 // controller since it will no longer be valid, but don't cancel the timeout
-                mediaController?.unregisterCallback(this)
+                bgExecutor.execute { mediaController?.unregisterCallback(this) }
             } else {
                 // For active controls, if the session is destroyed, clean up everything since we
                 // will need to recreate it if this key is updated later
@@ -284,7 +293,7 @@
 
             if ((!actionsSame || !playingStateSame) && state != null && dispatchEvents) {
                 logger.logStateCallback(key)
-                stateCallback.invoke(key, state)
+                uiExecutor.execute { stateCallback.invoke(key, state) }
             }
 
             if (playingStateSame && !resumptionChanged) {
@@ -313,7 +322,7 @@
                 expireMediaTimeout(key, "playback started - $state, $key")
                 timedOut = false
                 if (dispatchEvents) {
-                    timeoutCallback(key, timedOut)
+                    uiExecutor.execute { timeoutCallback(key, timedOut) }
                 }
             }
         }
@@ -337,60 +346,13 @@
         }
     }
 
-    private fun areCustomActionListsEqual(
-        first: List<PlaybackState.CustomAction>?,
-        second: List<PlaybackState.CustomAction>?
-    ): Boolean {
-        // Same object, or both null
-        if (first === second) {
-            return true
-        }
-
-        // Only one null, or different number of actions
-        if ((first == null || second == null) || (first.size != second.size)) {
-            return false
-        }
-
-        // Compare individual actions
-        first.asSequence().zip(second.asSequence()).forEach { (firstAction, secondAction) ->
-            if (!areCustomActionsEqual(firstAction, secondAction)) {
-                return false
-            }
-        }
-        return true
-    }
-
-    private fun areCustomActionsEqual(
-        firstAction: PlaybackState.CustomAction,
-        secondAction: PlaybackState.CustomAction
-    ): Boolean {
-        if (
-            firstAction.action != secondAction.action ||
-                firstAction.name != secondAction.name ||
-                firstAction.icon != secondAction.icon
-        ) {
-            return false
-        }
-
-        if ((firstAction.extras == null) != (secondAction.extras == null)) {
-            return false
-        }
-        if (firstAction.extras != null) {
-            firstAction.extras.keySet().forEach { key ->
-                if (firstAction.extras.get(key) != secondAction.extras.get(key)) {
-                    return false
-                }
-            }
-        }
-        return true
-    }
-
     /** Listens to changes in recommendation card data and schedules a timeout for its expiration */
     private inner class RecommendationListener(var key: String, data: SmartspaceMediaData) {
         private var timedOut = false
         var destroyed = false
         var expiration = Long.MAX_VALUE
             private set
+
         var cancellation: Runnable? = null
             private set
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
index 245f6f8..130868d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.bluetooth.BroadcastDialogController
 import com.android.systemui.media.controls.data.repository.MediaFilterRepository
 import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor
+import com.android.systemui.media.controls.domain.pipeline.getNotificationActions
 import com.android.systemui.media.controls.shared.model.MediaControlModel
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.util.MediaSmartspaceLogger
@@ -102,7 +103,7 @@
                 artwork = artwork,
                 deviceData = device,
                 semanticActionButtons = semanticActions,
-                notificationActionButtons = actions,
+                notificationActionButtons = getNotificationActions(data.actions, activityStarter),
                 actionsToShowInCollapsed = actionsToShowInCompact,
                 isDismissible = isClearable,
                 isResume = resumption,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt
index 2b710b5..7d20e17 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaLogger.kt
@@ -114,6 +114,15 @@
         )
     }
 
+    fun logDuplicateMediaNotification(key: String) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { str1 = key },
+            { "duplicate media notification $str1 posted" }
+        )
+    }
+
     companion object {
         private const val TAG = "MediaLog"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt
index 40b3477..aed8609 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt
@@ -39,7 +39,7 @@
     /** Album artwork. */
     val artwork: Icon? = null,
     /** List of generic action buttons for the media player, based on notification actions */
-    val actions: List<MediaAction> = emptyList(),
+    val actions: List<MediaNotificationAction> = emptyList(),
     /** Same as above, but shown on smaller versions of the player, like in QQS or keyguard. */
     val actionsToShowInCompact: List<Int> = emptyList(),
     /**
@@ -162,6 +162,14 @@
     val rebindId: Int? = null
 )
 
+/** State of a media action from notification. */
+data class MediaNotificationAction(
+    val isAuthenticationRequired: Boolean,
+    val actionIntent: PendingIntent?,
+    val icon: Drawable?,
+    val contentDescription: CharSequence?
+)
+
 /** State of the media device. */
 data class MediaDeviceData
 @JvmOverloads
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
index 87610cf..8bec46a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java
@@ -21,6 +21,7 @@
 import static com.android.settingslib.flags.Flags.legacyLeAudioSharing;
 import static com.android.systemui.Flags.communalHub;
 import static com.android.systemui.Flags.mediaLockscreenLaunchAnimation;
+import static com.android.systemui.media.controls.domain.pipeline.MediaActionsKt.getNotificationActions;
 import static com.android.systemui.media.controls.shared.model.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS;
 
 import android.animation.Animator;
@@ -1170,7 +1171,7 @@
 
             // Set all the generic buttons
             List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
-            List<MediaAction> actions = data.getActions();
+            List<MediaAction> actions = getNotificationActions(data.getActions(), mActivityStarter);
             int i = 0;
             for (; i < actions.size() && i < genericButtons.size(); i++) {
                 boolean showInCompact = actionsWhenCollapsed.contains(i);
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt
index a5c07bc..11854d9 100644
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeUserActionsViewModel.kt
@@ -18,9 +18,13 @@
 
 import com.android.compose.animation.scene.Back
 import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay
+import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
 import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
@@ -34,8 +38,10 @@
     override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
         setActions(
             mapOf(
-                Swipe.Up to SceneFamilies.Home,
                 Back to SceneFamilies.Home,
+                Swipe.Up to SceneFamilies.Home,
+                Swipe(direction = SwipeDirection.Down, fromSource = SceneContainerEdge.TopRight) to
+                    ReplaceByOverlay(Overlays.QuickSettingsShade),
             )
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index cbcf68c..2f843ac 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -50,10 +50,12 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
+import com.android.systemui.Flags;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.util.concurrency.DelayableExecutor;
+import com.android.systemui.util.time.SystemClock;
 
 import dagger.assisted.Assisted;
 import dagger.assisted.AssistedFactory;
@@ -95,6 +97,7 @@
     // Bind retry control.
     private static final int MAX_BIND_RETRIES = 5;
     private static final long DEFAULT_BIND_RETRY_DELAY = 5 * DateUtils.SECOND_IN_MILLIS;
+    private static final long ACTIVE_TILE_BIND_RETRY_DELAY = 1 * DateUtils.SECOND_IN_MILLIS;
     private static final long LOW_MEMORY_BIND_RETRY_DELAY = 20 * DateUtils.SECOND_IN_MILLIS;
     private static final long TILE_SERVICE_ONCLICK_ALLOW_LIST_DEFAULT_DURATION_MS = 15_000;
     private static final String PROPERTY_TILE_SERVICE_ONCLICK_ALLOW_LIST_DURATION =
@@ -107,6 +110,7 @@
     private final Intent mIntent;
     private final UserHandle mUser;
     private final DelayableExecutor mExecutor;
+    private final SystemClock mSystemClock;
     private final IBinder mToken = new Binder();
     private final PackageManagerAdapter mPackageManagerAdapter;
     private final BroadcastDispatcher mBroadcastDispatcher;
@@ -120,7 +124,6 @@
     private IBinder mClickBinder;
 
     private int mBindTryCount;
-    private long mBindRetryDelay = DEFAULT_BIND_RETRY_DELAY;
     private AtomicBoolean isDeathRebindScheduled = new AtomicBoolean(false);
     private AtomicBoolean mBound = new AtomicBoolean(false);
     private AtomicBoolean mPackageReceiverRegistered = new AtomicBoolean(false);
@@ -138,7 +141,8 @@
     TileLifecycleManager(@Main Handler handler, Context context, IQSService service,
             PackageManagerAdapter packageManagerAdapter, BroadcastDispatcher broadcastDispatcher,
             @Assisted Intent intent, @Assisted UserHandle user, ActivityManager activityManager,
-            IDeviceIdleController deviceIdleController, @Background DelayableExecutor executor) {
+            IDeviceIdleController deviceIdleController, @Background DelayableExecutor executor,
+            SystemClock systemClock) {
         mContext = context;
         mHandler = handler;
         mIntent = intent;
@@ -146,6 +150,7 @@
         mIntent.putExtra(TileService.EXTRA_TOKEN, mToken);
         mUser = user;
         mExecutor = executor;
+        mSystemClock = systemClock;
         mPackageManagerAdapter = packageManagerAdapter;
         mBroadcastDispatcher = broadcastDispatcher;
         mActivityManager = activityManager;
@@ -436,25 +441,31 @@
             // If mBound is true (meaning that we should be bound), then reschedule binding for
             // later.
             if (mBound.get() && checkComponentState()) {
-                if (isDeathRebindScheduled.compareAndSet(false, true)) {
+                if (isDeathRebindScheduled.compareAndSet(false, true)) { // if already not scheduled
+
+
                     mExecutor.executeDelayed(() -> {
                         // Only rebind if we are supposed to, but remove the scheduling anyway.
                         if (mBound.get()) {
                             setBindService(true);
                         }
-                        isDeathRebindScheduled.set(false);
+                        isDeathRebindScheduled.set(false); // allow scheduling again
                     }, getRebindDelay());
                 }
             }
         });
     }
 
+    private long mLastRebind = 0;
     /**
      * @return the delay to automatically rebind after a service died. It provides a longer delay if
      * the device is a low memory state because the service is likely to get killed again by the
      * system. In this case we want to rebind later and not to cause a loop of a frequent rebinds.
+     * It also provides a longer delay if called quickly (a few seconds) after a first call.
      */
     private long getRebindDelay() {
+        final long now = mSystemClock.currentTimeMillis();
+
         final ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
         mActivityManager.getMemoryInfo(info);
 
@@ -462,7 +473,20 @@
         if (info.lowMemory) {
             delay = LOW_MEMORY_BIND_RETRY_DELAY;
         } else {
-            delay = mBindRetryDelay;
+            if (Flags.qsQuickRebindActiveTiles()) {
+                final long elapsedTimeSinceLastRebind = now - mLastRebind;
+                final boolean justAttemptedRebind =
+                        elapsedTimeSinceLastRebind < DEFAULT_BIND_RETRY_DELAY;
+                if (isActiveTile() && !justAttemptedRebind) {
+                    delay = ACTIVE_TILE_BIND_RETRY_DELAY;
+                } else {
+                    delay = DEFAULT_BIND_RETRY_DELAY;
+                }
+            } else {
+                delay = DEFAULT_BIND_RETRY_DELAY;
+            }
+
+            mLastRebind = now;
         }
         if (mDebug) Log.i(TAG, "Rebinding with a delay=" + delay + " - " + getComponent());
         return delay;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
index d10471d..c5fa8cf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
@@ -44,7 +44,7 @@
 /**
  * Manages the priority which lets {@link TileServices} make decisions about which tiles
  * to bind.  Also holds on to and manages the {@link TileLifecycleManager}, informing it
- * of when it is allowed to bind based on decisions frome the {@link TileServices}.
+ * of when it is allowed to bind based on decisions from the {@link TileServices}.
  */
 public class TileServiceManager {
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt
index d3dc302..bd1872d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeUserActionsViewModel.kt
@@ -18,9 +18,13 @@
 
 import com.android.compose.animation.scene.Back
 import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.UserActionResult.ReplaceByOverlay
+import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.ui.viewmodel.SceneContainerEdge
 import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
@@ -43,6 +47,13 @@
             .map { editing ->
                 buildMap {
                     put(Swipe.Up, UserActionResult(SceneFamilies.Home))
+                    put(
+                        Swipe(
+                            direction = SwipeDirection.Down,
+                            fromSource = SceneContainerEdge.TopLeft
+                        ),
+                        ReplaceByOverlay(Overlays.NotificationsShade)
+                    )
                     if (!editing) {
                         put(Back, UserActionResult(SceneFamilies.Home))
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
index 3d6d00e..a5f4a89 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
@@ -23,8 +23,6 @@
 import android.content.res.Resources
 import android.net.Uri
 import android.os.Handler
-import android.os.UserHandle
-import android.provider.Settings
 import android.util.Log
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.animation.DialogTransitionAnimator
@@ -50,11 +48,11 @@
     notificationManager: NotificationManager,
     userContextProvider: UserContextProvider,
     keyguardDismissUtil: KeyguardDismissUtil,
-    private val dialogTransitionAnimator: DialogTransitionAnimator,
-    private val panelInteractor: PanelInteractor,
-    private val traceurMessageSender: TraceurMessageSender,
+    dialogTransitionAnimator: DialogTransitionAnimator,
+    panelInteractor: PanelInteractor,
+    traceurMessageSender: TraceurMessageSender,
     private val issueRecordingState: IssueRecordingState,
-    private val iActivityManager: IActivityManager,
+    iActivityManager: IActivityManager,
 ) :
     RecordingService(
         controller,
@@ -66,6 +64,18 @@
         keyguardDismissUtil
     ) {
 
+    private val commandHandler =
+        IssueRecordingServiceCommandHandler(
+            bgExecutor,
+            dialogTransitionAnimator,
+            panelInteractor,
+            traceurMessageSender,
+            issueRecordingState,
+            iActivityManager,
+            notificationManager,
+            userContextProvider,
+        )
+
     override fun getTag(): String = TAG
 
     override fun getChannelId(): String = CHANNEL_ID
@@ -76,10 +86,7 @@
         Log.d(getTag(), "handling action: ${intent?.action}")
         when (intent?.action) {
             ACTION_START -> {
-                bgExecutor.execute {
-                    traceurMessageSender.startTracing(issueRecordingState.traceConfig)
-                }
-                issueRecordingState.isRecording = true
+                commandHandler.handleStartCommand()
                 if (!issueRecordingState.recordScreen) {
                     // If we don't want to record the screen, the ACTION_SHOW_START_NOTIF action
                     // will circumvent the RecordingService's screen recording start code.
@@ -87,41 +94,13 @@
                 }
             }
             ACTION_STOP,
-            ACTION_STOP_NOTIF -> {
-                // ViewCapture needs to save it's data before it is disabled, or else the data will
-                // be lost. This is expected to change in the near future, and when that happens
-                // this line should be removed.
-                bgExecutor.execute {
-                    if (issueRecordingState.traceConfig.longTrace) {
-                        Settings.Global.putInt(
-                            contentResolver,
-                            NOTIFY_SESSION_ENDED_SETTING,
-                            DISABLED
-                        )
-                    }
-                    traceurMessageSender.stopTracing()
-                }
-                issueRecordingState.isRecording = false
-            }
+            ACTION_STOP_NOTIF -> commandHandler.handleStopCommand(contentResolver)
             ACTION_SHARE -> {
-                bgExecutor.execute {
-                    mNotificationManager.cancelAsUser(
-                        null,
-                        intent.getIntExtra(EXTRA_NOTIFICATION_ID, mNotificationId),
-                        UserHandle(mUserContextTracker.userContext.userId)
-                    )
-
-                    val screenRecording = intent.getParcelableExtra(EXTRA_PATH, Uri::class.java)
-                    if (issueRecordingState.takeBugreport) {
-                        iActivityManager.requestBugReportWithExtraAttachment(screenRecording)
-                    } else {
-                        traceurMessageSender.shareTraces(applicationContext, screenRecording)
-                    }
-                }
-
-                dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations()
-                panelInteractor.collapsePanels()
-
+                commandHandler.handleShareCommand(
+                    intent.getIntExtra(EXTRA_NOTIFICATION_ID, mNotificationId),
+                    intent.getParcelableExtra(EXTRA_PATH, Uri::class.java),
+                    this
+                )
                 // Unlike all other actions, action_share has different behavior for the screen
                 // recording qs tile than it does for the record issue qs tile. Return sticky to
                 // avoid running any of the base class' code for this action.
@@ -135,8 +114,6 @@
     companion object {
         private const val TAG = "IssueRecordingService"
         private const val CHANNEL_ID = "issue_record"
-        private const val NOTIFY_SESSION_ENDED_SETTING = "should_notify_trace_session_ended"
-        private const val DISABLED = 0
 
         /**
          * Get an intent to stop the issue recording service.
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt
new file mode 100644
index 0000000..32de0f3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandler.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.recordissue
+
+import android.app.IActivityManager
+import android.app.NotificationManager
+import android.content.ContentResolver
+import android.content.Context
+import android.net.Uri
+import android.os.UserHandle
+import android.provider.Settings
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor
+import com.android.systemui.settings.UserContextProvider
+import java.util.concurrent.Executor
+
+private const val NOTIFY_SESSION_ENDED_SETTING = "should_notify_trace_session_ended"
+private const val DISABLED = 0
+
+/**
+ * This class exists to unit test the business logic encapsulated in IssueRecordingService. Android
+ * specifically calls out that there is no supported way to test IntentServices here:
+ * https://developer.android.com/training/testing/other-components/services
+ */
+class IssueRecordingServiceCommandHandler(
+    private val bgExecutor: Executor,
+    private val dialogTransitionAnimator: DialogTransitionAnimator,
+    private val panelInteractor: PanelInteractor,
+    private val traceurMessageSender: TraceurMessageSender,
+    private val issueRecordingState: IssueRecordingState,
+    private val iActivityManager: IActivityManager,
+    private val notificationManager: NotificationManager,
+    private val userContextProvider: UserContextProvider,
+) {
+
+    fun handleStartCommand() {
+        bgExecutor.execute { traceurMessageSender.startTracing(issueRecordingState.traceConfig) }
+        issueRecordingState.isRecording = true
+    }
+
+    fun handleStopCommand(contentResolver: ContentResolver) {
+        bgExecutor.execute {
+            if (issueRecordingState.traceConfig.longTrace) {
+                Settings.Global.putInt(contentResolver, NOTIFY_SESSION_ENDED_SETTING, DISABLED)
+            }
+            traceurMessageSender.stopTracing()
+        }
+        issueRecordingState.isRecording = false
+    }
+
+    fun handleShareCommand(notificationId: Int, screenRecording: Uri?, context: Context) {
+        bgExecutor.execute {
+            notificationManager.cancelAsUser(
+                null,
+                notificationId,
+                UserHandle(userContextProvider.userContext.userId)
+            )
+
+            if (issueRecordingState.takeBugreport) {
+                iActivityManager.requestBugReportWithExtraAttachment(screenRecording)
+            } else {
+                traceurMessageSender.shareTraces(context, screenRecording)
+            }
+        }
+
+        dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations()
+        panelInteractor.collapsePanels()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index 00944b8..834db98 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.scene
 
+import androidx.compose.ui.unit.dp
 import com.android.systemui.CoreStartable
 import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule
 import com.android.systemui.scene.domain.SceneDomainModule
@@ -30,6 +31,8 @@
 import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.flag.DualShade
 import dagger.Binds
 import dagger.Module
@@ -119,5 +122,15 @@
                         .mapValues { checkNotNull(it.value) }
             )
         }
+
+        @Provides
+        fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector {
+            return SplitEdgeDetector(
+                topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
+                // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to
+                //  replace this constant with dynamic window insets.
+                edgeSize = 40.dp
+            )
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index 4061ad8..a4c7d00 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.scene
 
+import androidx.compose.ui.unit.dp
 import com.android.systemui.CoreStartable
 import com.android.systemui.notifications.ui.composable.NotificationsShadeSessionModule
 import com.android.systemui.scene.domain.SceneDomainModule
@@ -30,6 +31,8 @@
 import com.android.systemui.scene.shared.model.Overlays
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.shade.shared.flag.DualShade
 import dagger.Binds
 import dagger.Module
@@ -129,5 +132,15 @@
                         .mapValues { checkNotNull(it.value) }
             )
         }
+
+        @Provides
+        fun splitEdgeDetector(shadeInteractor: ShadeInteractor): SplitEdgeDetector {
+            return SplitEdgeDetector(
+                topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
+                // TODO(b/338577208): This should be 60dp at the top in the dual-shade UI. Better to
+                //  replace this constant with dynamic window insets.
+                edgeSize = 40.dp
+            )
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index 4c6341b..5482394 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -19,9 +19,11 @@
 import android.view.MotionEvent
 import androidx.compose.runtime.getValue
 import com.android.compose.animation.scene.ContentKey
+import com.android.compose.animation.scene.DefaultEdgeDetector
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.OverlayKey
 import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SwipeSourceDetector
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
 import com.android.systemui.classifier.Classifier
@@ -33,12 +35,15 @@
 import com.android.systemui.scene.shared.logger.SceneLogger
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.composable.Overlay
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
 
 /** Models UI state for the scene container. */
 class SceneContainerViewModel
@@ -47,6 +52,8 @@
     private val sceneInteractor: SceneInteractor,
     private val falsingInteractor: FalsingInteractor,
     private val powerInteractor: PowerInteractor,
+    private val shadeInteractor: ShadeInteractor,
+    private val splitEdgeDetector: SplitEdgeDetector,
     private val logger: SceneLogger,
     @Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
 ) : ExclusiveActivatable() {
@@ -59,6 +66,20 @@
     /** Whether the container is visible. */
     val isVisible: Boolean by hydrator.hydratedStateOf("isVisible", sceneInteractor.isVisible)
 
+    /**
+     * The [SwipeSourceDetector] to use for defining which edges of the screen can be defined in the
+     * [UserAction]s for this container.
+     */
+    val edgeDetector: SwipeSourceDetector by
+        hydrator.hydratedStateOf(
+            traceName = "edgeDetector",
+            initialValue = DefaultEdgeDetector,
+            source =
+                shadeInteractor.shadeMode.map {
+                    if (it is ShadeMode.Dual) splitEdgeDetector else DefaultEdgeDetector
+                }
+        )
+
     override suspend fun onActivated(): Nothing {
         try {
             // Sends a MotionEventHandler to the owner of the view-model so they can report
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt
new file mode 100644
index 0000000..f88bcb5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetector.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.viewmodel
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.FixedSizeEdgeDetector
+import com.android.compose.animation.scene.SwipeSource
+import com.android.compose.animation.scene.SwipeSourceDetector
+
+/**
+ * The edge of a [SceneContainer]. It differs from a standard [Edge] by splitting the top edge into
+ * top-left and top-right.
+ */
+enum class SceneContainerEdge(private val resolveEdge: (LayoutDirection) -> Resolved) :
+    SwipeSource {
+    TopLeft(resolveEdge = { Resolved.TopLeft }),
+    TopRight(resolveEdge = { Resolved.TopRight }),
+    TopStart(
+        resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopLeft else Resolved.TopRight }
+    ),
+    TopEnd(
+        resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.TopRight else Resolved.TopLeft }
+    ),
+    Bottom(resolveEdge = { Resolved.Bottom }),
+    Left(resolveEdge = { Resolved.Left }),
+    Right(resolveEdge = { Resolved.Right }),
+    Start(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Left else Resolved.Right }),
+    End(resolveEdge = { if (it == LayoutDirection.Ltr) Resolved.Right else Resolved.Left });
+
+    override fun resolve(layoutDirection: LayoutDirection): Resolved {
+        return resolveEdge(layoutDirection)
+    }
+
+    enum class Resolved : SwipeSource.Resolved {
+        TopLeft,
+        TopRight,
+        Bottom,
+        Left,
+        Right,
+    }
+}
+
+/**
+ * A [SwipeSourceDetector] that detects edges similarly to [FixedSizeEdgeDetector], except that the
+ * top edge is split in two: top-left and top-right. The split point between the two is dynamic and
+ * may change during runtime.
+ *
+ * Callers who need to detect the start and end edges based on the layout direction (LTR vs RTL)
+ * should subscribe to [SceneContainerEdge.TopStart] and [SceneContainerEdge.TopEnd] instead. These
+ * will be resolved at runtime to [SceneContainerEdge.Resolved.TopLeft] and
+ * [SceneContainerEdge.Resolved.TopRight] appropriately. Similarly, [SceneContainerEdge.Start] and
+ * [SceneContainerEdge.End] will be resolved appropriately to [SceneContainerEdge.Resolved.Left] and
+ * [SceneContainerEdge.Resolved.Right].
+ *
+ * @param topEdgeSplitFraction A function which returns the fraction between [0..1] (i.e.,
+ *   percentage) of screen width to consider the split point between "top-left" and "top-right"
+ *   edges. It is called on each source detection event.
+ * @param edgeSize The fixed size of each edge.
+ */
+class SplitEdgeDetector(
+    val topEdgeSplitFraction: () -> Float,
+    val edgeSize: Dp,
+) : SwipeSourceDetector {
+
+    private val fixedEdgeDetector = FixedSizeEdgeDetector(edgeSize)
+
+    override fun source(
+        layoutSize: IntSize,
+        position: IntOffset,
+        density: Density,
+        orientation: Orientation,
+    ): SceneContainerEdge.Resolved? {
+        val fixedEdge =
+            fixedEdgeDetector.source(
+                layoutSize,
+                position,
+                density,
+                orientation,
+            )
+        return when (fixedEdge) {
+            Edge.Resolved.Top -> {
+                val topEdgeSplitFraction = topEdgeSplitFraction()
+                require(topEdgeSplitFraction in 0f..1f) {
+                    "topEdgeSplitFraction must return a value between 0.0 and 1.0"
+                }
+                val isLeftSide = position.x < layoutSize.width * topEdgeSplitFraction
+                if (isLeftSide) SceneContainerEdge.Resolved.TopLeft
+                else SceneContainerEdge.Resolved.TopRight
+            }
+            Edge.Resolved.Left -> SceneContainerEdge.Resolved.Left
+            Edge.Resolved.Bottom -> SceneContainerEdge.Resolved.Bottom
+            Edge.Resolved.Right -> SceneContainerEdge.Resolved.Right
+            null -> null
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
index ed590c3..553d1f5 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
@@ -103,11 +103,8 @@
     override val userContentResolver: ContentResolver
         get() = userContext.contentResolver
 
-    override val userInfo: UserInfo
-        get() {
-            val user = userId
-            return userProfiles.first { it.id == user }
-        }
+    override var userInfo: UserInfo by SynchronizedDelegate(UserInfo(context.userId, "", 0))
+        protected set
 
     /**
      * Returns a [List<UserInfo>] of all profiles associated with the current user.
@@ -187,6 +184,7 @@
             userHandle = handle
             userContext = ctx
             userProfiles = profiles.map { UserInfo(it) }
+            userInfo = profiles.first { it.id == user }
         }
         return ctx to profiles
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index 830649b..4ed4af6 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -42,6 +42,7 @@
 import android.util.MathUtils;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
+import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.ViewGroup;
 import android.view.WindowInsets;
@@ -463,6 +464,9 @@
         mJavaAdapter.alwaysCollectFlow(
                 mCommunalTransitionViewModelLazy.get().isUmoOnCommunal(),
                 this::setShouldUpdateSquishinessOnMedia);
+        mJavaAdapter.alwaysCollectFlow(
+                mShadeInteractor.isAnyExpanded(),
+                this::onAnyExpandedChanged);
     }
 
     private void initNotificationStackScrollLayoutController() {
@@ -482,6 +486,10 @@
         }
     }
 
+    private void onAnyExpandedChanged(boolean isAnyExpanded) {
+        mQsFrame.setVisibility(isAnyExpanded ? View.VISIBLE : View.INVISIBLE);
+    }
+
     private void onNotificationScrolled(int newScrollPosition) {
         updateExpansionEnabledAmbient();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index 73e86a2..3cd91be 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.shade.domain.interactor
 
+import androidx.annotation.FloatRange
 import com.android.systemui.shade.shared.model.ShadeMode
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
@@ -69,6 +70,20 @@
      * wide as the entire screen.
      */
     val isShadeLayoutWide: StateFlow<Boolean>
+
+    /**
+     * The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold
+     * between "top-left" and "top-right" for the purposes of dual-shade invocation.
+     *
+     * When the dual-shade is not wide, this always returns 0.5 (the top edge is evenly split). On
+     * wide layouts however, a larger fraction is returned because only the area of the system
+     * status icons is considered top-right.
+     *
+     * Note that this fraction only determines the split between the absolute left and right
+     * directions. In RTL layouts, the "top-start" edge will resolve to "top-right", and "top-end"
+     * will resolve to "top-left".
+     */
+    @FloatRange(from = 0.0, to = 1.0) fun getTopEdgeSplitFraction(): Float
 }
 
 /** ShadeInteractor methods with implementations that differ between non-empty impls. */
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
index d51fd28..6c0b55a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
@@ -47,4 +47,6 @@
     override val isExpandToQsEnabled: Flow<Boolean> = inactiveFlowBoolean
     override val shadeMode: StateFlow<ShadeMode> = MutableStateFlow(ShadeMode.Single)
     override val isShadeLayoutWide: StateFlow<Boolean> = inactiveFlowBoolean
+
+    override fun getTopEdgeSplitFraction(): Float = 0.5f
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
index 3552092..b8d2dd2 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.shade.domain.interactor
 
+import androidx.annotation.FloatRange
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
@@ -104,6 +105,16 @@
 
     override val isShadeLayoutWide: StateFlow<Boolean> = shadeRepository.isShadeLayoutWide
 
+    @FloatRange(from = 0.0, to = 1.0)
+    override fun getTopEdgeSplitFraction(): Float {
+        // Note: this implicitly relies on isShadeLayoutWide being hot (i.e. collected). This
+        // assumption allows us to query its value on demand (during swipe source detection) instead
+        // of running another infinite coroutine.
+        // TODO(b/338577208): Instead of being fixed at 0.8f, this should dynamically updated based
+        //  on the position of system-status icons in the status bar.
+        return if (shadeRepository.isShadeLayoutWide.value) 0.8f else 0.5f
+    }
+
     override val shadeMode: StateFlow<ShadeMode> =
         isShadeLayoutWide
             .map(this::determineShadeMode)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt
index af21e75..d36412c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationSettingsRepositoryModule.kt
@@ -16,17 +16,23 @@
 
 package com.android.systemui.statusbar.notification.data
 
+import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.settings.SecureSettingsRepositoryModule
 import com.android.systemui.settings.SystemSettingsRepositoryModule
 import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
 import com.android.systemui.shared.settings.data.repository.SecureSettingsRepository
 import com.android.systemui.shared.settings.data.repository.SystemSettingsRepository
+import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionLogger
 import dagger.Module
 import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
 
 @Module(includes = [SecureSettingsRepositoryModule::class, SystemSettingsRepositoryModule::class])
 object NotificationSettingsRepositoryModule {
@@ -42,6 +48,19 @@
             backgroundScope,
             backgroundDispatcher,
             secureSettingsRepository,
-            systemSettingsRepository
-        )
+            systemSettingsRepository)
+
+    @Provides
+    @IntoMap
+    @ClassKey(NotificationSettingsRepository::class)
+    @SysUISingleton
+    fun provideCoreStartable(
+        @Application applicationScope: CoroutineScope,
+        repository: NotificationSettingsRepository,
+        logger: VisualInterruptionDecisionLogger
+    ) = CoreStartable {
+        applicationScope.launch {
+            repository.isCooldownEnabled.collect { value -> logger.logCooldownSetting(value) }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
index b83259d..38cab82 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
@@ -102,6 +102,15 @@
             { "AvalancheSuppressor: $str1" }
         )
     }
+
+    fun logCooldownSetting(isEnabled: Boolean) {
+        buffer.log(
+            TAG,
+            INFO,
+            { bool1 = isEnabled },
+            { "Cooldown enabled: $bool1" }
+        )
+    }
 }
 
 private const val TAG = "VisualInterruptionDecisionProvider"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
index 580431a..969ff1b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
@@ -68,6 +68,7 @@
         if (mLabelTextId != null) {
             mLabelView.setText(mLabelTextId);
         }
+        mLabelView.setAccessibilityHeading(true);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index ef1bcfc..cccac4b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -682,7 +682,10 @@
                 //  doesn't get updated quickly enough and can cause the footer to flash when
                 //  closing the shade. As such, we temporarily also check the ambientState directly.
                 if (((FooterView) view).shouldBeHidden() || !ambientState.isShadeExpanded()) {
-                    viewState.hidden = true;
+                    // Note: This is no longer necessary in flexiglass.
+                    if (!SceneContainerFlag.isEnabled()) {
+                        viewState.hidden = true;
+                    }
                 } else {
                     final float footerEnd = algorithmState.mCurrentExpandedYPosition
                             + view.getIntrinsicHeight();
@@ -691,7 +694,6 @@
                             noSpaceForFooter || (ambientState.isClearAllInProgress()
                                     && !hasNonClearableNotifs(algorithmState));
                 }
-
             } else {
                 final boolean shadeClosed = !ambientState.isShadeExpanded();
                 final boolean isShelfShowing = algorithmState.firstViewInShelf != null;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index d770b20..dc9615c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -188,15 +188,26 @@
                         .startHistoryIntent(view, /* showHistory= */ true)
                 },
             )
-        launch {
-            viewModel.shouldIncludeFooterView.collect { animatedVisibility ->
-                footerView.setVisible(
-                    /* visible = */ animatedVisibility.value,
-                    /* animate = */ animatedVisibility.isAnimating,
-                )
+        if (SceneContainerFlag.isEnabled) {
+            launch {
+                viewModel.shouldShowFooterView.collect { animatedVisibility ->
+                    footerView.setVisible(
+                        /* visible = */ animatedVisibility.value,
+                        /* animate = */ animatedVisibility.isAnimating,
+                    )
+                }
             }
+        } else {
+            launch {
+                viewModel.shouldIncludeFooterView.collect { animatedVisibility ->
+                    footerView.setVisible(
+                        /* visible = */ animatedVisibility.value,
+                        /* animate = */ animatedVisibility.isAnimating,
+                    )
+                }
+            }
+            launch { viewModel.shouldHideFooterView.collect { footerView.setShouldBeHidden(it) } }
         }
-        launch { viewModel.shouldHideFooterView.collect { footerView.setShouldBeHidden(it) } }
         disposableHandle.awaitCancellationThenDispose()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index e55492e6..4e2a46d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor
 import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
 import com.android.systemui.util.kotlin.FlowDumperImpl
+import com.android.systemui.util.kotlin.combine
 import com.android.systemui.util.kotlin.sample
 import com.android.systemui.util.ui.AnimatableEvent
 import com.android.systemui.util.ui.AnimatedValue
@@ -120,6 +121,7 @@
      * This essentially corresponds to having the view set to INVISIBLE.
      */
     val shouldHideFooterView: Flow<Boolean> by lazy {
+        SceneContainerFlag.assertInLegacyMode()
         if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
             flowOf(false)
         } else {
@@ -143,6 +145,7 @@
      * be hidden by another condition (see [shouldHideFooterView] above).
      */
     val shouldIncludeFooterView: Flow<AnimatedValue<Boolean>> by lazy {
+        SceneContainerFlag.assertInLegacyMode()
         if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
             flowOf(AnimatedValue.NotAnimating(false))
         } else {
@@ -207,6 +210,76 @@
         }
     }
 
+    // This flow replaces shouldHideFooterView+shouldIncludeFooterView in flexiglass.
+    val shouldShowFooterView: Flow<AnimatedValue<Boolean>> by lazy {
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
+            flowOf(AnimatedValue.NotAnimating(false))
+        } else {
+            combine(
+                    activeNotificationsInteractor.areAnyNotificationsPresent,
+                    userSetupInteractor.isUserSetUp,
+                    notificationStackInteractor.isShowingOnLockscreen,
+                    shadeInteractor.isQsFullscreen,
+                    remoteInputInteractor.isRemoteInputActive,
+                    shadeInteractor.shadeExpansion.map { it < 0.5f }.distinctUntilChanged(),
+                ) {
+                    hasNotifications,
+                    isUserSetUp,
+                    isShowingOnLockscreen,
+                    qsFullScreen,
+                    isRemoteInputActive,
+                    shadeLessThanHalfwayExpanded ->
+                    when {
+                        !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+                        // Hide the footer until the user setup is complete, to prevent access
+                        // to settings (b/193149550).
+                        !isUserSetUp -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+                        // Do not show the footer if the lockscreen is visible (incl. AOD),
+                        // except if the shade is opened on top. See also b/219680200.
+                        // Do not animate, as that makes the footer appear briefly when
+                        // transitioning between the shade and keyguard.
+                        isShowingOnLockscreen -> VisibilityChange.DISAPPEAR_WITHOUT_ANIMATION
+                        // Do not show the footer if quick settings are fully expanded (except
+                        // for the foldable split shade view). See b/201427195 && b/222699879.
+                        qsFullScreen -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+                        // Hide the footer if remote input is active (i.e. user is replying to a
+                        // notification). See b/75984847.
+                        isRemoteInputActive -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+                        // If the shade is not expanded enough, the footer shouldn't be visible.
+                        shadeLessThanHalfwayExpanded -> VisibilityChange.DISAPPEAR_WITH_ANIMATION
+                        else -> VisibilityChange.APPEAR_WITH_ANIMATION
+                    }
+                }
+                .distinctUntilChanged(
+                    // Equivalent unless visibility changes
+                    areEquivalent = { a: VisibilityChange, b: VisibilityChange ->
+                        a.visible == b.visible
+                    }
+                )
+                // Should we animate the visibility change?
+                .sample(
+                    // TODO(b/322167853): This check is currently duplicated in FooterViewModel,
+                    //  but instead it should be a field in ShadeAnimationInteractor.
+                    combine(
+                            shadeInteractor.isShadeFullyExpanded,
+                            shadeInteractor.isShadeTouchable,
+                            ::Pair
+                        )
+                        .onStart { emit(Pair(false, false)) }
+                ) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) ->
+                    // Animate if the shade is interactive, but NOT on the lockscreen. Having
+                    // animations enabled while on the lockscreen makes the footer appear briefly
+                    // when transitioning between the shade and keyguard.
+                    val shouldAnimate =
+                        isShadeFullyExpanded && animationsEnabled && visibilityChange.canAnimate
+                    AnimatableEvent(visibilityChange.visible, shouldAnimate)
+                }
+                .toAnimatedValueFlow()
+                .dumpWhileCollecting("shouldShowFooterView")
+                .flowOn(bgDispatcher)
+        }
+    }
+
     enum class VisibilityChange(val visible: Boolean, val canAnimate: Boolean) {
         DISAPPEAR_WITHOUT_ANIMATION(visible = false, canAnimate = false),
         DISAPPEAR_WITH_ANIMATION(visible = false, canAnimate = true),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index dd4b000..f3b9371 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -182,6 +182,7 @@
     private boolean mBouncerShowingOverDream;
     private int mAttemptsToShowBouncer = 0;
     private DelayableExecutor mExecutor;
+    private boolean mIsSleeping = false;
 
     private final PrimaryBouncerExpansionCallback mExpansionCallback =
             new PrimaryBouncerExpansionCallback() {
@@ -713,7 +714,11 @@
      * {@link #needsFullscreenBouncer()}.
      */
     protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
-        if (needsFullscreenBouncer() && !mDozing) {
+        boolean showBouncer = needsFullscreenBouncer() && !mDozing;
+        if (Flags.simPinRaceConditionOnRestart()) {
+            showBouncer = showBouncer && !mIsSleeping;
+        }
+        if (showBouncer) {
             // The keyguard might be showing (already). So we need to hide it.
             if (!primaryBouncerIsShowing()) {
                 if (SceneContainerFlag.isEnabled()) {
@@ -1041,6 +1046,7 @@
 
     @Override
     public void onStartedWakingUp() {
+        mIsSleeping = false;
         setRootViewAnimationDisabled(false);
         NavigationBarView navBarView = mCentralSurfaces.getNavigationBarView();
         if (navBarView != null) {
@@ -1054,6 +1060,7 @@
 
     @Override
     public void onStartedGoingToSleep() {
+        mIsSleeping = true;
         setRootViewAnimationDisabled(true);
         NavigationBarView navBarView = mCentralSurfaces.getNavigationBarView();
         if (navBarView != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt
index 9715772..28a43df 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt
@@ -16,35 +16,16 @@
 
 package com.android.systemui.volume.dagger
 
-import android.view.accessibility.CaptioningManager
 import com.android.systemui.accessibility.data.repository.CaptioningRepository
 import com.android.systemui.accessibility.data.repository.CaptioningRepositoryImpl
-import com.android.systemui.accessibility.domain.interactor.CaptioningInteractor
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
+import dagger.Binds
 import dagger.Module
-import dagger.Provides
-import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.CoroutineScope
 
 @Module
 interface CaptioningModule {
 
-    companion object {
-
-        @Provides
-        @SysUISingleton
-        fun provideCaptioningRepository(
-            captioningManager: CaptioningManager,
-            @Background coroutineContext: CoroutineContext,
-            @Application coroutineScope: CoroutineScope,
-        ): CaptioningRepository =
-            CaptioningRepositoryImpl(captioningManager, coroutineContext, coroutineScope)
-
-        @Provides
-        @SysUISingleton
-        fun provideCaptioningInteractor(repository: CaptioningRepository): CaptioningInteractor =
-            CaptioningInteractor(repository)
-    }
+    @Binds
+    @SysUISingleton
+    fun bindCaptioningRepository(impl: CaptioningRepositoryImpl): CaptioningRepository
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt
index 52f2ce6..2e5e389 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt
@@ -26,7 +26,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.stateIn
 
 @VolumePanelScope
 class CaptioningAvailabilityCriteria
@@ -45,7 +45,7 @@
                     else VolumePanelUiEvent.VOLUME_PANEL_LIVE_CAPTION_TOGGLE_GONE
                 )
             }
-            .shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1)
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
     override fun isAvailable(): Flow<Boolean> = availability
 }
diff --git a/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json b/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json
new file mode 100644
index 0000000..f37580d
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json
@@ -0,0 +1,831 @@
+{
+  "frame_ids": [
+    "before",
+    0,
+    16,
+    32,
+    48,
+    64,
+    80,
+    96,
+    112,
+    128,
+    144,
+    160,
+    176,
+    192,
+    208,
+    224,
+    240,
+    256,
+    272,
+    288,
+    304,
+    320,
+    336,
+    352,
+    368,
+    384,
+    400,
+    416,
+    432,
+    448,
+    464,
+    480,
+    496,
+    512,
+    528,
+    544,
+    560,
+    576,
+    592,
+    608,
+    624,
+    640,
+    656,
+    672,
+    688,
+    704,
+    720,
+    736,
+    752,
+    768,
+    784,
+    800,
+    816,
+    832,
+    848,
+    864,
+    880,
+    896,
+    912,
+    928,
+    944,
+    960,
+    976,
+    992,
+    1008,
+    1024,
+    "after"
+  ],
+  "features": [
+    {
+      "name": "content_alpha",
+      "type": "float",
+      "data_points": [
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        0.9954499,
+        0.9805035,
+        0.9527822,
+        0.9092045,
+        0.84588075,
+        0.7583043,
+        0.6424476,
+        0.49766344,
+        0.33080608,
+        0.15650165,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        {
+          "type": "not_found"
+        }
+      ]
+    },
+    {
+      "name": "content_scale",
+      "type": "scale",
+      "data_points": [
+        "default",
+        {
+          "x": 0.9995097,
+          "y": 0.9995097,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.997352,
+          "y": 0.997352,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.990635,
+          "y": 0.990635,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.97249764,
+          "y": 0.97249764,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.94287145,
+          "y": 0.94287145,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.9128026,
+          "y": 0.9128026,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8859569,
+          "y": 0.8859569,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8629254,
+          "y": 0.8629254,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8442908,
+          "y": 0.8442908,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8303209,
+          "y": 0.8303209,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8205137,
+          "y": 0.8205137,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.81387186,
+          "y": 0.81387186,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80941653,
+          "y": 0.80941653,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80641484,
+          "y": 0.80641484,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80437464,
+          "y": 0.80437464,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80297637,
+          "y": 0.80297637,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80201286,
+          "y": 0.80201286,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8013477,
+          "y": 0.8013477,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8008894,
+          "y": 0.8008894,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8005756,
+          "y": 0.8005756,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80036324,
+          "y": 0.80036324,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8002219,
+          "y": 0.8002219,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80012995,
+          "y": 0.80012995,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8000721,
+          "y": 0.8000721,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80003715,
+          "y": 0.80003715,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8000173,
+          "y": 0.8000173,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.800007,
+          "y": 0.800007,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8000022,
+          "y": 0.8000022,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8000004,
+          "y": 0.8000004,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.79999995,
+          "y": 0.79999995,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "type": "not_found"
+        }
+      ]
+    },
+    {
+      "name": "content_offset",
+      "type": "dpOffset",
+      "data_points": [
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0.5714286
+        },
+        {
+          "x": 0,
+          "y": 2.857143
+        },
+        {
+          "x": 0,
+          "y": 7.142857
+        },
+        {
+          "x": 0,
+          "y": 13.714286
+        },
+        {
+          "x": 0,
+          "y": 23.142857
+        },
+        {
+          "x": 0,
+          "y": 36.285713
+        },
+        {
+          "x": 0,
+          "y": 53.714287
+        },
+        {
+          "x": 0,
+          "y": 75.42857
+        },
+        {
+          "x": 0,
+          "y": 100.28571
+        },
+        {
+          "x": 0,
+          "y": 126.57143
+        },
+        {
+          "x": 0,
+          "y": 151.42857
+        },
+        {
+          "x": 0,
+          "y": 174
+        },
+        {
+          "x": 0,
+          "y": 193.42857
+        },
+        {
+          "x": 0,
+          "y": 210.28572
+        },
+        {
+          "x": 0,
+          "y": 224.85715
+        },
+        {
+          "x": 0,
+          "y": 237.14285
+        },
+        {
+          "x": 0,
+          "y": 247.71428
+        },
+        {
+          "x": 0,
+          "y": 256.85715
+        },
+        {
+          "x": 0,
+          "y": 264.57144
+        },
+        {
+          "x": 0,
+          "y": 271.42856
+        },
+        {
+          "x": 0,
+          "y": 277.14285
+        },
+        {
+          "x": 0,
+          "y": 282
+        },
+        {
+          "x": 0,
+          "y": 286.2857
+        },
+        {
+          "x": 0,
+          "y": 289.7143
+        },
+        {
+          "x": 0,
+          "y": 292.57144
+        },
+        {
+          "x": 0,
+          "y": 294.85715
+        },
+        {
+          "x": 0,
+          "y": 296.85715
+        },
+        {
+          "x": 0,
+          "y": 298.2857
+        },
+        {
+          "x": 0,
+          "y": 299.14285
+        },
+        {
+          "x": 0,
+          "y": 299.7143
+        },
+        {
+          "x": 0,
+          "y": 300
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "type": "not_found"
+        }
+      ]
+    },
+    {
+      "name": "background_alpha",
+      "type": "float",
+      "data_points": [
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        0.9900334,
+        0.8403853,
+        0.71002257,
+        0.5979084,
+        0.50182605,
+        0.41945767,
+        0.34874845,
+        0.28797746,
+        0.23573697,
+        0.19087732,
+        0.1524564,
+        0.11970067,
+        0.091962695,
+        0.068702936,
+        0.049464583,
+        0.033859253,
+        0.021552086,
+        0.012255073,
+        0.005717635,
+        0.0017191172,
+        6.711483e-05,
+        0,
+        {
+          "type": "not_found"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
new file mode 100644
index 0000000..22946c8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.ui.composable
+
+import android.app.AlertDialog
+import android.platform.test.annotations.MotionTest
+import android.testing.TestableLooper.RunWithLooper
+import androidx.activity.BackEventCompat
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.isFinite
+import androidx.compose.ui.geometry.isUnspecified
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.Scale
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.isElement
+import com.android.compose.animation.scene.testing.lastAlphaForTesting
+import com.android.compose.animation.scene.testing.lastScaleForTesting
+import com.android.compose.theme.PlatformTheme
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerUserActionsViewModel
+import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel
+import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.motion.createSysUiComposeMotionTestRule
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
+import com.android.systemui.scene.shared.logger.sceneLogger
+import com.android.systemui.scene.shared.model.SceneContainerConfig
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
+import com.android.systemui.scene.ui.composable.Scene
+import com.android.systemui.scene.ui.composable.SceneContainer
+import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import org.json.JSONObject
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+import platform.test.motion.compose.ComposeFeatureCaptures.positionInRoot
+import platform.test.motion.compose.ComposeRecordingSpec
+import platform.test.motion.compose.MotionControl
+import platform.test.motion.compose.feature
+import platform.test.motion.compose.recordMotion
+import platform.test.motion.compose.runTest
+import platform.test.motion.golden.DataPoint
+import platform.test.motion.golden.DataPointType
+import platform.test.motion.golden.DataPointTypes
+import platform.test.motion.golden.FeatureCapture
+import platform.test.motion.golden.UnknownTypeException
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.Displays.Phone
+
+/** MotionTest for the Bouncer Predictive Back animation */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper
+@EnableSceneContainer
+@MotionTest
+class BouncerPredictiveBackTest : SysuiTestCase() {
+
+    private val deviceSpec = DeviceEmulationSpec(Phone)
+    private val kosmos = testKosmos()
+
+    @get:Rule val motionTestRule = createSysUiComposeMotionTestRule(kosmos, deviceSpec)
+    private val androidComposeTestRule =
+        motionTestRule.toolkit.composeContentTestRule as AndroidComposeTestRule<*, *>
+
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
+    private val Kosmos.sceneKeys by Fixture { listOf(Scenes.Lockscreen, Scenes.Bouncer) }
+    private val Kosmos.initialSceneKey by Fixture { Scenes.Bouncer }
+    private val Kosmos.sceneContainerConfig by Fixture {
+        val navigationDistances =
+            mapOf(
+                Scenes.Lockscreen to 1,
+                Scenes.Bouncer to 0,
+            )
+        SceneContainerConfig(sceneKeys, initialSceneKey, emptyList(), navigationDistances)
+    }
+
+    private val transitionState by lazy {
+        MutableStateFlow<ObservableTransitionState>(
+            ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey)
+        )
+    }
+    private val sceneContainerViewModel by lazy {
+        SceneContainerViewModel(
+                sceneInteractor = kosmos.sceneInteractor,
+                falsingInteractor = kosmos.falsingInteractor,
+                powerInteractor = kosmos.powerInteractor,
+                shadeInteractor = kosmos.shadeInteractor,
+                splitEdgeDetector = kosmos.splitEdgeDetector,
+                logger = kosmos.sceneLogger,
+                motionEventHandlerReceiver = {},
+            )
+            .apply { setTransitionState(transitionState) }
+    }
+
+    private val bouncerDialogFactory =
+        object : BouncerDialogFactory {
+            override fun invoke(): AlertDialog {
+                throw AssertionError()
+            }
+        }
+    private val bouncerSceneActionsViewModelFactory =
+        object : BouncerUserActionsViewModel.Factory {
+            override fun create() = BouncerUserActionsViewModel(kosmos.bouncerInteractor)
+        }
+    private lateinit var bouncerSceneContentViewModel: BouncerSceneContentViewModel
+    private val bouncerSceneContentViewModelFactory =
+        object : BouncerSceneContentViewModel.Factory {
+            override fun create() = bouncerSceneContentViewModel
+        }
+    private val bouncerScene =
+        BouncerScene(
+            bouncerSceneActionsViewModelFactory,
+            bouncerSceneContentViewModelFactory,
+            bouncerDialogFactory
+        )
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        bouncerSceneContentViewModel = kosmos.bouncerSceneContentViewModel
+
+        val startable = kosmos.sceneContainerStartable
+        startable.start()
+    }
+
+    @Test
+    fun bouncerPredictiveBackMotion() =
+        motionTestRule.runTest {
+            val motion =
+                recordMotion(
+                    content = { play ->
+                        PlatformTheme {
+                            BackGestureAnimation(play)
+                            SceneContainer(
+                                viewModel =
+                                    rememberViewModel("BouncerPredictiveBackTest") {
+                                        sceneContainerViewModel
+                                    },
+                                sceneByKey =
+                                    mapOf(
+                                        Scenes.Lockscreen to FakeLockscreen(),
+                                        Scenes.Bouncer to bouncerScene
+                                    ),
+                                initialSceneKey = Scenes.Bouncer,
+                                overlayByKey = emptyMap(),
+                                dataSourceDelegator = kosmos.sceneDataSourceDelegator
+                            )
+                        }
+                    },
+                    ComposeRecordingSpec(
+                        MotionControl(
+                            delayRecording = {
+                                awaitCondition {
+                                    sceneInteractor.transitionState.value.isTransitioning()
+                                }
+                            }
+                        ) {
+                            awaitCondition {
+                                sceneInteractor.transitionState.value.isIdle(Scenes.Lockscreen)
+                            }
+                        }
+                    ) {
+                        feature(isElement(Bouncer.Elements.Content), elementAlpha, "content_alpha")
+                        feature(isElement(Bouncer.Elements.Content), elementScale, "content_scale")
+                        feature(
+                            isElement(Bouncer.Elements.Content),
+                            positionInRoot,
+                            "content_offset"
+                        )
+                        feature(
+                            isElement(Bouncer.Elements.Background),
+                            elementAlpha,
+                            "background_alpha"
+                        )
+                    }
+                )
+
+            assertThat(motion).timeSeriesMatchesGolden()
+        }
+
+    @Composable
+    private fun BackGestureAnimation(play: Boolean) {
+        val backProgress = remember { Animatable(0f) }
+
+        LaunchedEffect(play) {
+            if (play) {
+                val dispatcher = androidComposeTestRule.activity.onBackPressedDispatcher
+                androidComposeTestRule.runOnUiThread {
+                    dispatcher.dispatchOnBackStarted(backEvent())
+                }
+                backProgress.animateTo(
+                    targetValue = 1f,
+                    animationSpec = tween(durationMillis = 500)
+                ) {
+                    androidComposeTestRule.runOnUiThread {
+                        dispatcher.dispatchOnBackProgressed(
+                            backEvent(progress = backProgress.value)
+                        )
+                        if (backProgress.value == 1f) {
+                            dispatcher.onBackPressed()
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private fun backEvent(progress: Float = 0f): BackEventCompat {
+        return BackEventCompat(
+            touchX = 0f,
+            touchY = 0f,
+            progress = progress,
+            swipeEdge = BackEventCompat.EDGE_LEFT,
+        )
+    }
+
+    private class FakeLockscreen : ExclusiveActivatable(), Scene {
+        override val key: SceneKey = Scenes.Lockscreen
+        override val userActions: Flow<Map<UserAction, UserActionResult>> = flowOf()
+
+        @Composable
+        override fun SceneScope.Content(modifier: Modifier) {
+            Box(modifier = modifier, contentAlignment = Alignment.Center) {
+                Text(text = "Fake Lockscreen")
+            }
+        }
+
+        override suspend fun onActivated() = awaitCancellation()
+    }
+
+    companion object {
+        private val elementAlpha =
+            FeatureCapture<SemanticsNode, Float>("alpha") {
+                DataPoint.of(it.lastAlphaForTesting, DataPointTypes.float)
+            }
+
+        private val elementScale =
+            FeatureCapture<SemanticsNode, Scale>("scale") {
+                DataPoint.of(it.lastScaleForTesting, scale)
+            }
+
+        private val scale: DataPointType<Scale> =
+            DataPointType(
+                "scale",
+                jsonToValue = {
+                    when (it) {
+                        "unspecified" -> Scale.Unspecified
+                        "default" -> Scale.Default
+                        "zero" -> Scale.Zero
+                        is JSONObject -> {
+                            val pivot = it.get("pivot")
+                            Scale(
+                                scaleX = it.getDouble("x").toFloat(),
+                                scaleY = it.getDouble("y").toFloat(),
+                                pivot =
+                                    when (pivot) {
+                                        "unspecified" -> Offset.Unspecified
+                                        "infinite" -> Offset.Infinite
+                                        is JSONObject ->
+                                            Offset(
+                                                pivot.getDouble("x").toFloat(),
+                                                pivot.getDouble("y").toFloat()
+                                            )
+                                        else -> throw UnknownTypeException()
+                                    }
+                            )
+                        }
+                        else -> throw UnknownTypeException()
+                    }
+                },
+                valueToJson = {
+                    when (it) {
+                        Scale.Unspecified -> "unspecified"
+                        Scale.Default -> "default"
+                        Scale.Zero -> "zero"
+                        else -> {
+                            JSONObject().apply {
+                                put("x", it.scaleX)
+                                put("y", it.scaleY)
+                                put(
+                                    "pivot",
+                                    when {
+                                        it.pivot.isUnspecified -> "unspecified"
+                                        !it.pivot.isFinite -> "infinite"
+                                        else ->
+                                            JSONObject().apply {
+                                                put("x", it.pivot.x)
+                                                put("y", it.pivot.y)
+                                            }
+                                    }
+                                )
+                            }
+                        }
+                    }
+                }
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
index ad7a5b6..3c74374 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
@@ -967,7 +967,8 @@
         assertThat(data.resumption).isTrue()
         assertThat(data.song).isEqualTo(SESSION_TITLE)
         assertThat(data.app).isEqualTo(APP_NAME)
-        assertThat(data.actions).hasSize(1)
+        // resume button is a semantic action.
+        assertThat(data.actions).hasSize(0)
         assertThat(data.semanticActions!!.playOrPause).isNotNull()
         assertThat(data.lastActive).isAtLeast(currentTime)
         verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
@@ -994,7 +995,8 @@
         assertThat(data.resumption).isTrue()
         assertThat(data.song).isEqualTo(SESSION_TITLE)
         assertThat(data.app).isEqualTo(APP_NAME)
-        assertThat(data.actions).hasSize(1)
+        // resume button is a semantic action.
+        assertThat(data.actions).hasSize(0)
         assertThat(data.semanticActions!!.playOrPause).isNotNull()
         assertThat(data.lastActive).isAtLeast(currentTime)
         assertThat(data.isExplicit).isTrue()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
index c0f503d..4cf7de3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
@@ -69,6 +69,8 @@
 import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
 import com.android.systemui.media.controls.domain.resume.MediaResumeListener
 import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
+import com.android.systemui.media.controls.shared.mediaLogger
+import com.android.systemui.media.controls.shared.mockMediaLogger
 import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
 import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC
 import com.android.systemui.media.controls.shared.model.MediaData
@@ -140,7 +142,7 @@
 @RunWith(ParameterizedAndroidJunit4::class)
 @EnableSceneContainer
 class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
-    private val kosmos = testKosmos()
+    private val kosmos = testKosmos().apply { mediaLogger = mockMediaLogger }
     private val testDispatcher = kosmos.testDispatcher
     private val testScope = kosmos.testScope
     private val settings = kosmos.fakeSettings
@@ -257,6 +259,7 @@
                 keyguardUpdateMonitor = keyguardUpdateMonitor,
                 mediaDataRepository = kosmos.mediaDataRepository,
                 mediaDataLoader = { kosmos.mediaDataLoader },
+                mediaLogger = kosmos.mediaLogger,
             )
         mediaDataProcessor.start()
         testScope.runCurrent()
@@ -984,7 +987,8 @@
         assertThat(data.resumption).isTrue()
         assertThat(data.song).isEqualTo(SESSION_TITLE)
         assertThat(data.app).isEqualTo(APP_NAME)
-        assertThat(data.actions).hasSize(1)
+        // resume button is a semantic action.
+        assertThat(data.actions).hasSize(0)
         assertThat(data.semanticActions!!.playOrPause).isNotNull()
         assertThat(data.lastActive).isAtLeast(currentTime)
         verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
@@ -1011,7 +1015,8 @@
         assertThat(data.resumption).isTrue()
         assertThat(data.song).isEqualTo(SESSION_TITLE)
         assertThat(data.app).isEqualTo(APP_NAME)
-        assertThat(data.actions).hasSize(1)
+        // resume button is a semantic action.
+        assertThat(data.actions).hasSize(0)
         assertThat(data.semanticActions!!.playOrPause).isNotNull()
         assertThat(data.lastActive).isAtLeast(currentTime)
         assertThat(data.isExplicit).isTrue()
@@ -2476,6 +2481,55 @@
         assertThat(mediaDataCaptor.value.artwork).isNull()
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
+    fun postDuplicateNotification_doesNotCallListeners() {
+        whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
+        whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
+
+        mediaDataProcessor.addInternalListener(mediaDataFilter)
+        mediaDataFilter.mediaDataProcessor = mediaDataProcessor
+        addNotificationAndLoad()
+        reset(listener)
+        mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
+
+        testScope.assertRunAllReady(foreground = 0, background = 1)
+        verify(listener, never())
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        verify(kosmos.mediaLogger).logDuplicateMediaNotification(eq(KEY))
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
+    fun postDuplicateNotification_callsListeners() {
+        whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
+        whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
+
+        mediaDataProcessor.addInternalListener(mediaDataFilter)
+        mediaDataFilter.mediaDataProcessor = mediaDataProcessor
+        addNotificationAndLoad()
+        reset(listener)
+        mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
+        testScope.assertRunAllReady(foreground = 1, background = 1)
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(KEY),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY))
+    }
+
     private fun TestScope.assertRunAllReady(foreground: Int = 0, background: Int = 0) {
         runCurrent()
         if (Flags.mediaLoadMetadataViaMediaDataLoader()) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
index c1bba4d..680df15 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
@@ -72,7 +72,6 @@
     @Mock private lateinit var mediaController: MediaController
     @Mock private lateinit var logger: MediaTimeoutLogger
     @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
-    private lateinit var executor: FakeExecutor
     @Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
     @Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit
     @Mock private lateinit var sessionCallback: (String) -> Unit
@@ -88,6 +87,9 @@
     private lateinit var resumeData: MediaData
     private lateinit var mediaTimeoutListener: MediaTimeoutListener
     private var clock = FakeSystemClock()
+    private lateinit var mainExecutor: FakeExecutor
+    private lateinit var bgExecutor: FakeExecutor
+    private lateinit var uiExecutor: FakeExecutor
     @Mock private lateinit var mediaFlags: MediaFlags
     @Mock private lateinit var smartspaceData: SmartspaceMediaData
 
@@ -95,11 +97,15 @@
     fun setup() {
         whenever(mediaControllerFactory.create(any())).thenReturn(mediaController)
         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
-        executor = FakeExecutor(clock)
+        mainExecutor = FakeExecutor(clock)
+        bgExecutor = FakeExecutor(clock)
+        uiExecutor = FakeExecutor(clock)
         mediaTimeoutListener =
             MediaTimeoutListener(
                 mediaControllerFactory,
-                executor,
+                bgExecutor,
+                uiExecutor,
+                mainExecutor,
                 logger,
                 statusBarStateController,
                 clock,
@@ -143,30 +149,31 @@
         whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
 
         whenever(mediaController.playbackState).thenReturn(playingState)
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        loadMediaData(KEY, null, mediaData)
         verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
         verify(logger).logPlaybackState(eq(KEY), eq(playingState))
 
         // Ignores if same key
         clearInvocations(mediaController)
-        mediaTimeoutListener.onMediaDataLoaded(KEY, KEY, mediaData)
+        loadMediaData(KEY, KEY, mediaData)
         verify(mediaController, never()).registerCallback(anyObject())
     }
 
     @Test
     fun testOnMediaDataLoaded_registersTimeout_whenPaused() {
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        loadMediaData(KEY, null, mediaData)
         verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
         verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
         verify(logger).logScheduleTimeout(eq(KEY), eq(false), eq(false))
-        assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
+        assertThat(mainExecutor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
     }
 
     @Test
     fun testOnMediaDataRemoved_unregistersPlaybackListener() {
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        loadMediaData(KEY, null, mediaData)
         mediaTimeoutListener.onMediaDataRemoved(KEY, false)
+        assertThat(bgExecutor.runAllReady()).isEqualTo(1)
         verify(mediaController).unregisterCallback(anyObject())
 
         // Ignores duplicate requests
@@ -178,50 +185,50 @@
     @Test
     fun testOnMediaDataRemoved_clearsTimeout() {
         // GIVEN media that is paused
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
-        assertThat(executor.numPending()).isEqualTo(1)
+        loadMediaData(KEY, null, mediaData)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
         // WHEN the media is removed
         mediaTimeoutListener.onMediaDataRemoved(KEY, false)
         // THEN the timeout runnable is cancelled
-        assertThat(executor.numPending()).isEqualTo(0)
+        assertThat(mainExecutor.numPending()).isEqualTo(0)
     }
 
     @Test
     fun testOnMediaDataLoaded_migratesKeys() {
         val newKey = "NEWKEY"
         // From not playing
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        loadMediaData(KEY, null, mediaData)
         clearInvocations(mediaController)
 
         // To playing
         val playingState = mock(android.media.session.PlaybackState::class.java)
         whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
         whenever(mediaController.playbackState).thenReturn(playingState)
-        mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
+        loadMediaData(newKey, KEY, mediaData)
         verify(mediaController).unregisterCallback(anyObject())
         verify(mediaController).registerCallback(anyObject())
         verify(logger).logMigrateListener(eq(KEY), eq(newKey), eq(true))
 
         // Enqueues callback
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
     }
 
     @Test
     fun testOnMediaDataLoaded_migratesKeys_noTimeoutExtension() {
         val newKey = "NEWKEY"
         // From not playing
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        loadMediaData(KEY, null, mediaData)
         clearInvocations(mediaController)
 
         // Migrate, still not playing
         val playingState = mock(android.media.session.PlaybackState::class.java)
         whenever(playingState.state).thenReturn(PlaybackState.STATE_PAUSED)
         whenever(mediaController.playbackState).thenReturn(playingState)
-        mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
+        loadMediaData(newKey, KEY, mediaData)
 
         // The number of queued timeout tasks remains the same. The timeout task isn't cancelled nor
         // is another scheduled
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
         verify(logger).logUpdateListener(eq(newKey), eq(false))
     }
 
@@ -233,8 +240,8 @@
         mediaCallbackCaptor.value.onPlaybackStateChanged(
             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
         )
-        assertThat(executor.numPending()).isEqualTo(1)
-        assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
     }
 
     @Test
@@ -245,7 +252,7 @@
         mediaCallbackCaptor.value.onPlaybackStateChanged(
             PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 0f).build()
         )
-        assertThat(executor.numPending()).isEqualTo(0)
+        assertThat(mainExecutor.numPending()).isEqualTo(0)
         verify(logger).logTimeoutCancelled(eq(KEY), any())
     }
 
@@ -257,7 +264,7 @@
         mediaCallbackCaptor.value.onPlaybackStateChanged(
             PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
         )
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
     }
 
     @Test
@@ -265,7 +272,7 @@
         // Assuming we're have a pending timeout
         testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
 
-        with(executor) {
+        with(mainExecutor) {
             advanceClockToNext()
             runAllReady()
         }
@@ -274,7 +281,7 @@
 
     @Test
     fun testIsTimedOut() {
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        loadMediaData(KEY, null, mediaData)
         assertThat(mediaTimeoutListener.isTimedOut(KEY)).isFalse()
     }
 
@@ -282,16 +289,17 @@
     fun testOnSessionDestroyed_active_clearsTimeout() {
         // GIVEN media that is paused
         val mediaPaused = mediaData.copy(isPlaying = false)
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPaused)
+        loadMediaData(KEY, null, mediaPaused)
         verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
 
         // WHEN the session is destroyed
         mediaCallbackCaptor.value.onSessionDestroyed()
 
         // THEN the controller is unregistered and timeout run
+        assertThat(bgExecutor.runAllReady()).isEqualTo(1)
         verify(mediaController).unregisterCallback(anyObject())
-        assertThat(executor.numPending()).isEqualTo(0)
+        assertThat(mainExecutor.numPending()).isEqualTo(0)
         verify(logger).logSessionDestroyed(eq(KEY))
         verify(sessionCallback).invoke(eq(KEY))
     }
@@ -306,11 +314,11 @@
         whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
         whenever(mediaController.playbackState).thenReturn(playingState)
         val mediaPlaying = mediaData.copy(isPlaying = true)
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPlaying)
+        loadMediaData(KEY, null, mediaPlaying)
 
         // THEN the timeout runnable will update the state
-        assertThat(executor.numPending()).isEqualTo(1)
-        with(executor) {
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
+        with(mainExecutor) {
             advanceClockToNext()
             runAllReady()
         }
@@ -322,31 +330,32 @@
     fun testOnSessionDestroyed_resume_continuesTimeout() {
         // GIVEN resume media with session info
         val resumeWithSession = resumeData.copy(token = session.sessionToken)
-        mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeWithSession)
+        loadMediaData(PACKAGE, null, resumeWithSession)
         verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
 
         // WHEN the session is destroyed
         mediaCallbackCaptor.value.onSessionDestroyed()
 
         // THEN the controller is unregistered, but the timeout is still scheduled
+        assertThat(bgExecutor.runAllReady()).isEqualTo(1)
         verify(mediaController).unregisterCallback(anyObject())
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
         verify(sessionCallback, never()).invoke(eq(KEY))
     }
 
     @Test
     fun testOnMediaDataLoaded_activeToResume_registersTimeout() {
         // WHEN a regular media is loaded
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        loadMediaData(KEY, null, mediaData)
 
         // AND it turns into a resume control
-        mediaTimeoutListener.onMediaDataLoaded(PACKAGE, KEY, resumeData)
+        loadMediaData(PACKAGE, KEY, resumeData)
 
         // THEN we register a timeout
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
         verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
-        assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
+        assertThat(mainExecutor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
     }
 
     @Test
@@ -355,42 +364,42 @@
         val pausedState =
             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
         whenever(mediaController.playbackState).thenReturn(pausedState)
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
-        assertThat(executor.numPending()).isEqualTo(1)
+        loadMediaData(KEY, null, mediaData)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
 
         // AND it turns into a resume control
-        mediaTimeoutListener.onMediaDataLoaded(PACKAGE, KEY, resumeData)
+        loadMediaData(PACKAGE, KEY, resumeData)
 
         // THEN we update the timeout length
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
         verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
-        assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
+        assertThat(mainExecutor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
     }
 
     @Test
     fun testOnMediaDataLoaded_resumption_registersTimeout() {
         // WHEN a resume media is loaded
-        mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData)
+        loadMediaData(PACKAGE, null, resumeData)
 
         // THEN we register a timeout
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
         verify(timeoutCallback, never()).invoke(anyString(), anyBoolean())
-        assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
+        assertThat(mainExecutor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT)
     }
 
     @Test
     fun testOnMediaDataLoaded_resumeToActive_updatesTimeout() {
         // WHEN we have a resume control
-        mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData)
+        loadMediaData(PACKAGE, null, resumeData)
 
         // AND that media is resumed
         val playingState =
             PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
         whenever(mediaController.playbackState).thenReturn(playingState)
-        mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData)
+        loadMediaData(oldKey = PACKAGE, data = mediaData)
 
         // THEN the timeout length is changed to a regular media control
-        assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
+        assertThat(mainExecutor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT)
     }
 
     @Test
@@ -401,7 +410,7 @@
         mediaTimeoutListener.onMediaDataRemoved(PACKAGE, false)
 
         // THEN the timeout runnable is cancelled
-        assertThat(executor.numPending()).isEqualTo(0)
+        assertThat(mainExecutor.numPending()).isEqualTo(0)
     }
 
     @Test
@@ -427,6 +436,7 @@
         // When the playback state changes, and has different actions
         val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
         mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+        assertThat(uiExecutor.runAllReady()).isEqualTo(1)
 
         // Then the callback is invoked
         verify(stateCallback).invoke(eq(KEY), eq(playingState!!))
@@ -463,6 +473,7 @@
                 .addCustomAction(customTwo)
                 .build()
         mediaCallbackCaptor.value.onPlaybackStateChanged(pausedStateTwoActions)
+        assertThat(uiExecutor.runAllReady()).isEqualTo(1)
 
         // Then the callback is invoked
         verify(stateCallback).invoke(eq(KEY), eq(pausedStateTwoActions!!))
@@ -534,6 +545,7 @@
         val playingState =
             PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
         mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+        uiExecutor.runAllReady()
 
         // Then the callback is invoked
         verify(stateCallback).invoke(eq(KEY), eq(playingState!!))
@@ -567,7 +579,7 @@
         // And we doze past the scheduled timeout
         val time = clock.currentTimeMillis()
         clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT)
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
 
         // Then when no longer dozing, the timeout runs immediately
         dozingCallbackCaptor.value.onDozingChanged(false)
@@ -576,7 +588,7 @@
 
         // and cancel any later scheduled timeout
         verify(logger).logTimeoutCancelled(eq(KEY), any())
-        assertThat(executor.numPending()).isEqualTo(0)
+        assertThat(mainExecutor.numPending()).isEqualTo(0)
     }
 
     @Test
@@ -592,12 +604,12 @@
 
         // And we doze, but not past the scheduled timeout
         clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT / 2L)
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
 
         // Then when no longer dozing, the timeout remains scheduled
         dozingCallbackCaptor.value.onDozingChanged(false)
         verify(timeoutCallback, never()).invoke(eq(KEY), eq(true))
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
     }
 
     @Test
@@ -610,8 +622,8 @@
         whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
 
         mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-        assertThat(executor.numPending()).isEqualTo(1)
-        assertThat(executor.advanceClockToNext()).isEqualTo(duration)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.advanceClockToNext()).isEqualTo(duration)
     }
 
     @Test
@@ -619,7 +631,7 @@
         // Given a pending timeout
         testSmartspaceDataLoaded_schedulesTimeout()
 
-        executor.runAllReady()
+        mainExecutor.runAllReady()
         verify(timeoutCallback).invoke(eq(SMARTSPACE_KEY), eq(true))
     }
 
@@ -634,14 +646,14 @@
         whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
 
         mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
 
         val expiryLonger = expireTime + duration
         whenever(smartspaceData.expiryTimeMs).thenReturn(expiryLonger)
         mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
-        assertThat(executor.numPending()).isEqualTo(1)
-        assertThat(executor.advanceClockToNext()).isEqualTo(duration * 2)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.advanceClockToNext()).isEqualTo(duration * 2)
     }
 
     @Test
@@ -649,10 +661,10 @@
         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
 
         mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
 
         mediaTimeoutListener.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
-        assertThat(executor.numPending()).isEqualTo(0)
+        assertThat(mainExecutor.numPending()).isEqualTo(0)
     }
 
     @Test
@@ -667,12 +679,12 @@
         whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
 
         mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
 
         // And we doze past the scheduled timeout
         val time = clock.currentTimeMillis()
         clock.setElapsedRealtime(time + duration * 2)
-        assertThat(executor.numPending()).isEqualTo(1)
+        assertThat(mainExecutor.numPending()).isEqualTo(1)
 
         // Then when no longer dozing, the timeout runs immediately
         dozingCallbackCaptor.value.onDozingChanged(false)
@@ -680,12 +692,18 @@
         verify(logger).logTimeout(eq(SMARTSPACE_KEY))
 
         // and cancel any later scheduled timeout
-        assertThat(executor.numPending()).isEqualTo(0)
+        assertThat(mainExecutor.numPending()).isEqualTo(0)
     }
 
     private fun loadMediaDataWithPlaybackState(state: PlaybackState) {
         whenever(mediaController.playbackState).thenReturn(state)
-        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
+        loadMediaData(data = mediaData)
         verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
     }
+
+    private fun loadMediaData(key: String = KEY, oldKey: String? = null, data: MediaData) {
+        mediaTimeoutListener.onMediaDataLoaded(key, oldKey, data)
+        bgExecutor.runAllReady()
+        uiExecutor.runAllReady()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
index 1260a65..68a5d93 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaControlPanelTest.kt
@@ -79,6 +79,7 @@
 import com.android.systemui.media.controls.shared.model.MediaButton
 import com.android.systemui.media.controls.shared.model.MediaData
 import com.android.systemui.media.controls.shared.model.MediaDeviceData
+import com.android.systemui.media.controls.shared.model.MediaNotificationAction
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
 import com.android.systemui.media.controls.ui.binder.SeekBarObserver
 import com.android.systemui.media.controls.ui.view.GutsViewHolder
@@ -236,6 +237,19 @@
     @Mock private lateinit var recProgressBar3: SeekBar
     @Mock private lateinit var globalSettings: GlobalSettings
 
+    private val intent =
+        Intent().apply {
+            putExtras(Bundle().also { it.putString(KEY_SMARTSPACE_APP_NAME, REC_APP_NAME) })
+            setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        }
+    private val pendingIntent =
+        PendingIntent.getActivity(
+            mContext,
+            0,
+            intent.setPackage(mContext.packageName),
+            PendingIntent.FLAG_MUTABLE
+        )
+
     @JvmField @Rule val mockito = MockitoJUnit.rule()
 
     @Before
@@ -989,14 +1003,13 @@
     @Test
     fun bindNotificationActions() {
         val icon = context.getDrawable(android.R.drawable.ic_media_play)
-        val bg = context.getDrawable(R.drawable.qs_media_round_button_background)
         val actions =
             listOf(
-                MediaAction(icon, Runnable {}, "previous", bg),
-                MediaAction(icon, Runnable {}, "play", bg),
-                MediaAction(icon, null, "next", bg),
-                MediaAction(icon, null, "custom 0", bg),
-                MediaAction(icon, Runnable {}, "custom 1", bg)
+                MediaNotificationAction(true, actionIntent = pendingIntent, icon, "previous"),
+                MediaNotificationAction(true, actionIntent = pendingIntent, icon, "play"),
+                MediaNotificationAction(true, actionIntent = null, icon, "next"),
+                MediaNotificationAction(true, actionIntent = null, icon, "custom 0"),
+                MediaNotificationAction(true, actionIntent = pendingIntent, icon, "custom 1")
             )
         val state =
             mediaData.copy(
@@ -1684,11 +1697,11 @@
     fun actionCustom2Click_isLogged() {
         val actions =
             listOf(
-                MediaAction(null, Runnable {}, "action 0", null),
-                MediaAction(null, Runnable {}, "action 1", null),
-                MediaAction(null, Runnable {}, "action 2", null),
-                MediaAction(null, Runnable {}, "action 3", null),
-                MediaAction(null, Runnable {}, "action 4", null)
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 0"),
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"),
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"),
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"),
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4")
             )
         val data = mediaData.copy(actions = actions)
 
@@ -1703,11 +1716,11 @@
     fun actionCustom3Click_isLogged() {
         val actions =
             listOf(
-                MediaAction(null, Runnable {}, "action 0", null),
-                MediaAction(null, Runnable {}, "action 1", null),
-                MediaAction(null, Runnable {}, "action 2", null),
-                MediaAction(null, Runnable {}, "action 3", null),
-                MediaAction(null, Runnable {}, "action 4", null)
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 0"),
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"),
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"),
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"),
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4")
             )
         val data = mediaData.copy(actions = actions)
 
@@ -1722,11 +1735,11 @@
     fun actionCustom4Click_isLogged() {
         val actions =
             listOf(
-                MediaAction(null, Runnable {}, "action 0", null),
-                MediaAction(null, Runnable {}, "action 1", null),
-                MediaAction(null, Runnable {}, "action 2", null),
-                MediaAction(null, Runnable {}, "action 3", null),
-                MediaAction(null, Runnable {}, "action 4", null)
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 0"),
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 1"),
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 2"),
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 3"),
+                MediaNotificationAction(true, actionIntent = pendingIntent, null, "action 4")
             )
         val data = mediaData.copy(actions = actions)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
index c1cf91d..bc0ec2d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
@@ -22,6 +22,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.systemui.Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX;
+import static com.android.systemui.Flags.FLAG_QS_QUICK_REBIND_ACTIVE_TILES;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -75,6 +76,8 @@
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 
+import com.google.common.truth.Truth;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -95,7 +98,8 @@
 
     @Parameters(name = "{0}")
     public static List<FlagsParameterization> getParams() {
-        return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX);
+        return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX,
+                FLAG_QS_QUICK_REBIND_ACTIVE_TILES);
     }
 
     private final PackageManagerAdapter mMockPackageManagerAdapter =
@@ -154,7 +158,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
     }
 
     @After
@@ -169,12 +174,12 @@
         mStateManager.handleDestroy();
     }
 
-    private void setPackageEnabled(boolean enabled) throws Exception {
+    private void setPackageEnabledAndActive(boolean enabled, boolean active) throws Exception {
         ServiceInfo defaultServiceInfo = null;
         if (enabled) {
             defaultServiceInfo = new ServiceInfo();
             defaultServiceInfo.metaData = new Bundle();
-            defaultServiceInfo.metaData.putBoolean(TileService.META_DATA_ACTIVE_TILE, true);
+            defaultServiceInfo.metaData.putBoolean(TileService.META_DATA_ACTIVE_TILE, active);
             defaultServiceInfo.metaData.putBoolean(TileService.META_DATA_TOGGLEABLE_TILE, true);
         }
         when(mMockPackageManagerAdapter.getServiceInfo(any(), anyInt(), anyInt()))
@@ -186,6 +191,10 @@
                 .thenReturn(defaultPackageInfo);
     }
 
+    private void setPackageEnabled(boolean enabled) throws Exception {
+        setPackageEnabledAndActive(enabled, true);
+    }
+
     private void setPackageInstalledForUser(
             boolean installed,
             boolean active,
@@ -396,13 +405,40 @@
     }
 
     @Test
-    public void testKillProcess() throws Exception {
+    public void testKillProcessWhenTileServiceIsNotActive() throws Exception {
+        setPackageEnabledAndActive(true, false);
         mStateManager.onStartListening();
         mStateManager.executeSetBindService(true);
         mExecutor.runAllReady();
+        verifyBind(1);
+        verify(mMockTileService, times(1)).onStartListening();
+
         mStateManager.onBindingDied(mTileServiceComponentName);
         mExecutor.runAllReady();
-        mClock.advanceTime(5000);
+        mClock.advanceTime(1000);
+        mExecutor.runAllReady();
+
+        // still 4 seconds left because non active tile service rebind time is 5 seconds
+        Truth.assertThat(mContext.isBound(mTileServiceComponentName)).isFalse();
+
+        mClock.advanceTime(4000); // 5 seconds delay for nonActive service rebinding
+        mExecutor.runAllReady();
+        verifyBind(2);
+        verify(mMockTileService, times(2)).onStartListening();
+    }
+
+    @EnableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES)
+    @Test
+    public void testKillProcessWhenTileServiceIsActive_withRebindFlagOn() throws Exception {
+        mStateManager.onStartListening();
+        mStateManager.executeSetBindService(true);
+        mExecutor.runAllReady();
+        verifyBind(1);
+        verify(mMockTileService, times(1)).onStartListening();
+
+        mStateManager.onBindingDied(mTileServiceComponentName);
+        mExecutor.runAllReady();
+        mClock.advanceTime(1000);
         mExecutor.runAllReady();
 
         // Two calls: one for the first bind, one for the restart.
@@ -410,6 +446,86 @@
         verify(mMockTileService, times(2)).onStartListening();
     }
 
+    @DisableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES)
+    @Test
+    public void testKillProcessWhenTileServiceIsActive_withRebindFlagOff() throws Exception {
+        mStateManager.onStartListening();
+        mStateManager.executeSetBindService(true);
+        mExecutor.runAllReady();
+        verifyBind(1);
+        verify(mMockTileService, times(1)).onStartListening();
+
+        mStateManager.onBindingDied(mTileServiceComponentName);
+        mExecutor.runAllReady();
+        mClock.advanceTime(1000);
+        mExecutor.runAllReady();
+        verifyBind(0); // the rebind happens after 4 more seconds
+
+        mClock.advanceTime(4000);
+        mExecutor.runAllReady();
+        verifyBind(1);
+    }
+
+    @EnableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES)
+    @Test
+    public void testKillProcessWhenTileServiceIsActiveTwice_withRebindFlagOn_delaysSecondRebind()
+            throws Exception {
+        mStateManager.onStartListening();
+        mStateManager.executeSetBindService(true);
+        mExecutor.runAllReady();
+        verifyBind(1);
+        verify(mMockTileService, times(1)).onStartListening();
+
+        mStateManager.onBindingDied(mTileServiceComponentName);
+        mExecutor.runAllReady();
+        mClock.advanceTime(1000);
+        mExecutor.runAllReady();
+
+        // Two calls: one for the first bind, one for the restart.
+        verifyBind(2);
+        verify(mMockTileService, times(2)).onStartListening();
+
+        mStateManager.onBindingDied(mTileServiceComponentName);
+        mExecutor.runAllReady();
+        mClock.advanceTime(1000);
+        mExecutor.runAllReady();
+        // because active tile will take 5 seconds to bind the second time, not 1
+        verifyBind(0);
+
+        mClock.advanceTime(4000);
+        mExecutor.runAllReady();
+        verifyBind(1);
+    }
+
+    @DisableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES)
+    @Test
+    public void testKillProcessWhenTileServiceIsActiveTwice_withRebindFlagOff_rebindsFromFirstKill()
+            throws Exception {
+        mStateManager.onStartListening();
+        mStateManager.executeSetBindService(true);
+        mExecutor.runAllReady();
+        verifyBind(1);
+        verify(mMockTileService, times(1)).onStartListening();
+
+        mStateManager.onBindingDied(mTileServiceComponentName); // rebind scheduled for 5 seconds
+        mExecutor.runAllReady();
+        mClock.advanceTime(1000);
+        mExecutor.runAllReady();
+
+        verifyBind(0); // it would bind in 4 more seconds
+
+        mStateManager.onBindingDied(mTileServiceComponentName); // this does not affect the rebind
+        mExecutor.runAllReady();
+        mClock.advanceTime(1000);
+        mExecutor.runAllReady();
+
+        verifyBind(0); // only 2 seconds passed from first kill
+
+        mClock.advanceTime(3000);
+        mExecutor.runAllReady();
+        verifyBind(1); // the rebind scheduled 5 seconds from the first kill should now happen
+    }
+
     @Test
     public void testKillProcessLowMemory() throws Exception {
         doAnswer(invocation -> {
@@ -510,7 +626,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         manager.executeSetBindService(true);
         mExecutor.runAllReady();
@@ -533,7 +650,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         manager.executeSetBindService(true);
         mExecutor.runAllReady();
@@ -556,7 +674,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         manager.executeSetBindService(true);
         mExecutor.runAllReady();
@@ -581,7 +700,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         manager.executeSetBindService(true);
         mExecutor.runAllReady();
@@ -607,7 +727,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         assertThat(manager.isActiveTile()).isTrue();
     }
@@ -626,7 +747,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         assertThat(manager.isActiveTile()).isTrue();
     }
@@ -644,7 +766,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         assertThat(manager.isToggleableTile()).isTrue();
     }
@@ -663,7 +786,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         assertThat(manager.isToggleableTile()).isTrue();
     }
@@ -682,7 +806,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         assertThat(manager.isToggleableTile()).isFalse();
         assertThat(manager.isActiveTile()).isFalse();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt
new file mode 100644
index 0000000..57cfe1b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/IssueRecordingServiceCommandHandlerTest.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.recordissue
+
+import android.app.IActivityManager
+import android.app.NotificationManager
+import android.net.Uri
+import android.os.UserHandle
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.dialogTransitionAnimator
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor
+import com.android.systemui.settings.UserContextProvider
+import com.android.systemui.settings.userFileManager
+import com.android.systemui.settings.userTracker
+import com.android.traceur.TraceConfig
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.any
+import org.mockito.kotlin.isNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class IssueRecordingServiceCommandHandlerTest : SysuiTestCase() {
+
+    private val kosmos = Kosmos().also { it.testCase = this }
+    private val bgExecutor = kosmos.fakeExecutor
+    private val userContextProvider: UserContextProvider = kosmos.userTracker
+    private val dialogTransitionAnimator: DialogTransitionAnimator = kosmos.dialogTransitionAnimator
+    private lateinit var traceurMessageSender: TraceurMessageSender
+    private val issueRecordingState =
+        IssueRecordingState(kosmos.userTracker, kosmos.userFileManager)
+
+    private val iActivityManager = mock<IActivityManager>()
+    private val notificationManager = mock<NotificationManager>()
+    private val panelInteractor = mock<PanelInteractor>()
+
+    private lateinit var underTest: IssueRecordingServiceCommandHandler
+
+    @Before
+    fun setup() {
+        traceurMessageSender = mock<TraceurMessageSender>()
+        underTest =
+            IssueRecordingServiceCommandHandler(
+                bgExecutor,
+                dialogTransitionAnimator,
+                panelInteractor,
+                traceurMessageSender,
+                issueRecordingState,
+                iActivityManager,
+                notificationManager,
+                userContextProvider
+            )
+    }
+
+    @Test
+    fun startsTracing_afterReceivingActionStartCommand() {
+        underTest.handleStartCommand()
+        bgExecutor.runAllReady()
+
+        Truth.assertThat(issueRecordingState.isRecording).isTrue()
+        verify(traceurMessageSender).startTracing(any<TraceConfig>())
+    }
+
+    @Test
+    fun stopsTracing_afterReceivingStopTracingCommand() {
+        underTest.handleStopCommand(mContext.contentResolver)
+        bgExecutor.runAllReady()
+
+        Truth.assertThat(issueRecordingState.isRecording).isFalse()
+        verify(traceurMessageSender).stopTracing()
+    }
+
+    @Test
+    fun cancelsNotification_afterReceivingShareCommand() {
+        underTest.handleShareCommand(0, null, mContext)
+        bgExecutor.runAllReady()
+
+        verify(notificationManager).cancelAsUser(isNull(), anyInt(), any<UserHandle>())
+    }
+
+    @Test
+    fun requestBugreport_afterReceivingShareCommand_withTakeBugreportTrue() {
+        issueRecordingState.takeBugreport = true
+        val uri = mock<Uri>()
+
+        underTest.handleShareCommand(0, uri, mContext)
+        bgExecutor.runAllReady()
+
+        verify(iActivityManager).requestBugReportWithExtraAttachment(uri)
+    }
+
+    @Test
+    fun sharesTracesDirectly_afterReceivingShareCommand_withTakeBugreportFalse() {
+        issueRecordingState.takeBugreport = false
+        val uri = mock<Uri>()
+
+        underTest.handleShareCommand(0, uri, mContext)
+        bgExecutor.runAllReady()
+
+        verify(traceurMessageSender).shareTraces(mContext, uri)
+    }
+
+    @Test
+    fun closesShade_afterReceivingShareCommand() {
+        val uri = mock<Uri>()
+
+        underTest.handleShareCommand(0, uri, mContext)
+        bgExecutor.runAllReady()
+
+        verify(panelInteractor).collapsePanels()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt
index 263b001..78764c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplReceiveTest.kt
@@ -79,21 +79,6 @@
 
     @Test
     fun callsCallbackAndUpdatesProfilesWhenAnIntentReceived() = runTest {
-        tracker =
-            UserTrackerImpl(
-                context,
-                { fakeFeatures },
-                userManager,
-                iActivityManager,
-                dumpManager,
-                this,
-                testDispatcher,
-                handler
-            )
-        tracker.initialize(0)
-        tracker.addCallback(callback, executor)
-        val profileID = tracker.userId + 10
-
         `when`(userManager.getProfiles(anyInt())).thenAnswer { invocation ->
             val id = invocation.getArgument<Int>(0)
             val info = UserInfo(id, "", UserInfo.FLAG_FULL)
@@ -109,6 +94,21 @@
             listOf(info, infoProfile)
         }
 
+        tracker =
+            UserTrackerImpl(
+                context,
+                { fakeFeatures },
+                userManager,
+                iActivityManager,
+                dumpManager,
+                this,
+                testDispatcher,
+                handler
+            )
+        tracker.initialize(0)
+        tracker.addCallback(callback, executor)
+        val profileID = tracker.userId + 10
+
         tracker.onReceive(context, Intent(intentAction))
 
         verify(callback, times(0)).onUserChanged(anyInt(), any())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 01a3d36..1d74331 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -1112,9 +1112,11 @@
     public void testShowBouncerOrKeyguard_showsKeyguardIfShowBouncerReturnsFalse() {
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
+        // Returning false means unable to show the bouncer
         when(mPrimaryBouncerInteractor.show(true)).thenReturn(false);
         when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo())
                 .thenReturn(KeyguardState.LOCKSCREEN);
+        mStatusBarKeyguardViewManager.onStartedWakingUp();
 
         reset(mCentralSurfaces);
         // Advance past reattempts
@@ -1127,6 +1129,23 @@
 
     @Test
     @DisableSceneContainer
+    @EnableFlags(Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART)
+    public void testShowBouncerOrKeyguard_showsKeyguardIfSleeping() {
+        when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo())
+                .thenReturn(KeyguardState.LOCKSCREEN);
+        mStatusBarKeyguardViewManager.onStartedGoingToSleep();
+
+        reset(mCentralSurfaces);
+        reset(mPrimaryBouncerInteractor);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(
+                /* hideBouncerWhenShowing= */true, false);
+        verify(mCentralSurfaces).showKeyguard();
+        verify(mPrimaryBouncerInteractor).hide();
+    }
+
+
+    @Test
+    @DisableSceneContainer
     public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing() {
         boolean isFalsingReset = false;
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt
index 2a0e764..a639463 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeCaptioningRepository.kt
@@ -16,25 +16,31 @@
 
 package com.android.systemui.accessibility.data.repository
 
+import com.android.systemui.accessibility.data.model.CaptioningModel
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 
 class FakeCaptioningRepository : CaptioningRepository {
 
-    private val mutableIsSystemAudioCaptioningEnabled = MutableStateFlow(false)
-    override val isSystemAudioCaptioningEnabled: StateFlow<Boolean>
-        get() = mutableIsSystemAudioCaptioningEnabled.asStateFlow()
-
-    private val mutableIsSystemAudioCaptioningUiEnabled = MutableStateFlow(false)
-    override val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean>
-        get() = mutableIsSystemAudioCaptioningUiEnabled.asStateFlow()
+    private val mutableCaptioningModel = MutableStateFlow<CaptioningModel?>(null)
+    override val captioningModel: StateFlow<CaptioningModel?> = mutableCaptioningModel.asStateFlow()
 
     override suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean) {
-        mutableIsSystemAudioCaptioningEnabled.value = isEnabled
+        mutableCaptioningModel.value =
+            CaptioningModel(
+                isSystemAudioCaptioningEnabled = isEnabled,
+                isSystemAudioCaptioningUiEnabled =
+                    mutableCaptioningModel.value?.isSystemAudioCaptioningUiEnabled == true,
+            )
     }
 
-    fun setIsSystemAudioCaptioningUiEnabled(isSystemAudioCaptioningUiEnabled: Boolean) {
-        mutableIsSystemAudioCaptioningUiEnabled.value = isSystemAudioCaptioningUiEnabled
+    fun setIsSystemAudioCaptioningUiEnabled(isEnabled: Boolean) {
+        mutableCaptioningModel.value =
+            CaptioningModel(
+                isSystemAudioCaptioningEnabled =
+                    mutableCaptioningModel.value?.isSystemAudioCaptioningEnabled == true,
+                isSystemAudioCaptioningUiEnabled = isEnabled,
+            )
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderKosmos.kt
index a5690a0..cb7750f5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoaderKosmos.kt
@@ -24,7 +24,6 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.media.controls.util.fakeMediaControllerFactory
 import com.android.systemui.media.controls.util.mediaFlags
-import com.android.systemui.plugins.activityStarter
 
 val Kosmos.mediaDataLoader by
     Kosmos.Fixture {
@@ -32,7 +31,6 @@
             testableContext,
             testDispatcher,
             testScope,
-            activityStarter,
             fakeMediaControllerFactory,
             mediaFlags,
             imageLoader,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt
index 632436a..174e653 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.media.controls.data.repository.mediaDataRepository
+import com.android.systemui.media.controls.shared.mediaLogger
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider
 import com.android.systemui.media.controls.util.fakeMediaControllerFactory
 import com.android.systemui.media.controls.util.mediaFlags
@@ -60,5 +61,6 @@
             keyguardUpdateMonitor = keyguardUpdateMonitor,
             mediaDataRepository = mediaDataRepository,
             mediaDataLoader = { mediaDataLoader },
+            mediaLogger = mediaLogger,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt
index b7660e0..b33edf9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt
@@ -28,6 +28,8 @@
     Kosmos.Fixture {
         MediaTimeoutListener(
             mediaControllerFactory = fakeMediaControllerFactory,
+            bgExecutor = fakeExecutor,
+            uiExecutor = fakeExecutor,
             mainExecutor = fakeExecutor,
             logger = MediaTimeoutLogger(logcatLogBuffer("MediaTimeoutLogBuffer")),
             statusBarStateController = statusBarStateController,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
index a0fc76b..4978558 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.qs.tiles.impl.custom.packageManagerAdapterFacade
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.time.fakeSystemClock
 
 val Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by
     Kosmos.Fixture {
@@ -39,6 +40,7 @@
                 activityManager,
                 mock(),
                 fakeExecutor,
+                fakeSystemClock,
             )
         }
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
index 55f3ed7..8744638 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
@@ -12,6 +12,8 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.FakeOverlay
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 import kotlinx.coroutines.flow.MutableStateFlow
 
 var Kosmos.sceneKeys by Fixture {
@@ -70,6 +72,8 @@
             sceneInteractor = sceneInteractor,
             falsingInteractor = falsingInteractor,
             powerInteractor = powerInteractor,
+            shadeInteractor = shadeInteractor,
+            splitEdgeDetector = splitEdgeDetector,
             motionEventHandlerReceiver = {},
             logger = sceneLogger
         )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt
new file mode 100644
index 0000000..e0b5292
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/viewmodel/SplitEdgeDetectorKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.viewmodel
+
+import androidx.compose.ui.unit.dp
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+
+var Kosmos.splitEdgeDetector: SplitEdgeDetector by
+    Kosmos.Fixture {
+        SplitEdgeDetector(
+            topEdgeSplitFraction = shadeInteractor::getTopEdgeSplitFraction,
+            edgeSize = 40.dp,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/utils/FakeUserScopedService.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/utils/FakeUserScopedService.kt
new file mode 100644
index 0000000..78763f9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/utils/FakeUserScopedService.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.user.utils
+
+import android.os.UserHandle
+
+class FakeUserScopedService<T>(private val defaultImplementation: T) : UserScopedService<T> {
+
+    private val implementations = mutableMapOf<UserHandle, T>()
+
+    fun addImplementation(user: UserHandle, implementation: T) {
+        implementations[user] = implementation
+    }
+
+    fun removeImplementation(user: UserHandle): T? = implementations.remove(user)
+
+    override fun forUser(user: UserHandle): T =
+        implementations.getOrDefault(user, defaultImplementation)
+}
diff --git a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java
similarity index 82%
copy from ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
copy to ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java
index 4b9cf85..b582ccf 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,7 +15,7 @@
  */
 package android.ravenwood.annotation;
 
-import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.ElementType.METHOD;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -29,8 +29,7 @@
  *
  * @hide
  */
-@Target({TYPE})
+@Target({METHOD})
 @Retention(RetentionPolicy.CLASS)
-public @interface RavenwoodNativeSubstitutionClass {
-    String value();
+public @interface RavenwoodRedirect {
 }
diff --git a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirectionClass.java
similarity index 94%
rename from ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
rename to ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirectionClass.java
index 4b9cf85..bee9222 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirectionClass.java
@@ -31,6 +31,6 @@
  */
 @Target({TYPE})
 @Retention(RetentionPolicy.CLASS)
-public @interface RavenwoodNativeSubstitutionClass {
+public @interface RavenwoodRedirectionClass {
     String value();
 }
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/CursorWindow_host.java b/ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java
similarity index 89%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/CursorWindow_host.java
rename to ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java
index f38d565..e21a9cd 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/CursorWindow_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java
@@ -13,9 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.database;
 
-import android.database.Cursor;
 import android.database.sqlite.SQLiteException;
 import android.os.Parcel;
 import android.util.Base64;
@@ -35,8 +34,8 @@
     private String mName;
     private int mColumnNum;
     private static class Row {
-        String[] fields;
-        int[] types;
+        String[] mFields;
+        int[] mTypes;
     }
 
     private final List<Row> mRows = new ArrayList<>();
@@ -69,9 +68,9 @@
     public static boolean nativeAllocRow(long windowPtr) {
         CursorWindow_host instance = sInstances.get(windowPtr);
         Row row = new Row();
-        row.fields = new String[instance.mColumnNum];
-        row.types = new int[instance.mColumnNum];
-        Arrays.fill(row.types, Cursor.FIELD_TYPE_NULL);
+        row.mFields = new String[instance.mColumnNum];
+        row.mTypes = new int[instance.mColumnNum];
+        Arrays.fill(row.mTypes, Cursor.FIELD_TYPE_NULL);
         instance.mRows.add(row);
         return true;
     }
@@ -82,8 +81,8 @@
             return false;
         }
         Row r = instance.mRows.get(row);
-        r.fields[column] = value;
-        r.types[column] = type;
+        r.mFields[column] = value;
+        r.mTypes[column] = type;
         return true;
     }
 
@@ -93,7 +92,7 @@
             return Cursor.FIELD_TYPE_NULL;
         }
 
-        return instance.mRows.get(row).types[column];
+        return instance.mRows.get(row).mTypes[column];
     }
 
     public static boolean nativePutString(long windowPtr, String value,
@@ -107,7 +106,7 @@
             return null;
         }
 
-        return instance.mRows.get(row).fields[column];
+        return instance.mRows.get(row).mFields[column];
     }
 
     public static boolean nativePutLong(long windowPtr, long value, int row, int column) {
@@ -170,8 +169,8 @@
         parcel.writeInt(window.mColumnNum);
         parcel.writeInt(window.mRows.size());
         for (int row = 0; row < window.mRows.size(); row++) {
-            parcel.writeStringArray(window.mRows.get(row).fields);
-            parcel.writeIntArray(window.mRows.get(row).types);
+            parcel.writeStringArray(window.mRows.get(row).mFields);
+            parcel.writeIntArray(window.mRows.get(row).mTypes);
         }
     }
 
@@ -183,8 +182,8 @@
         int rowCount = parcel.readInt();
         for (int row = 0; row < rowCount; row++) {
             Row r = new Row();
-            r.fields = parcel.createStringArray();
-            r.types = parcel.createIntArray();
+            r.mFields = parcel.createStringArray();
+            r.mTypes = parcel.createIntArray();
             window.mRows.add(r);
         }
         return windowPtr;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/MessageQueue_host.java b/ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java
similarity index 97%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/MessageQueue_host.java
rename to ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java
index 5e81124..1b63adc 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/MessageQueue_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.os;
 
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java b/ravenwood/runtime-helper-src/framework/android/os/Parcel_host.java
similarity index 99%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
rename to ravenwood/runtime-helper-src/framework/android/os/Parcel_host.java
index cb00b3e..720f1d2 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/os/Parcel_host.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.os;
 
 import android.system.ErrnoException;
 import android.system.Os;
@@ -527,4 +527,4 @@
         }
         return false;
     }
-}
\ No newline at end of file
+}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/SystemProperties_host.java b/ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java
similarity index 89%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/SystemProperties_host.java
rename to ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java
index e7479d3..b09bf31 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/SystemProperties_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.os;
 
 import android.util.SparseArray;
 
@@ -36,9 +36,6 @@
     /** Predicate tested to determine if a given key can be written. */
     @GuardedBy("sLock")
     private static Predicate<String> sKeyWritablePredicate;
-    /** Callback to trigger when values are changed */
-    @GuardedBy("sLock")
-    private static Runnable sChangeCallback;
 
     /**
      * Reverse mapping that provides a way back to an original key from the
@@ -48,7 +45,7 @@
     private static SparseArray<String> sKeyHandles = new SparseArray<>();
 
     /**
-     * Basically the same as {@link #native_init$ravenwood}, but it'll only run if no values are
+     * Basically the same as {@link #init$ravenwood}, but it'll only run if no values are
      * set yet.
      */
     public static void initializeIfNeeded(Map<String, String> values,
@@ -57,30 +54,32 @@
             if (sValues != null) {
                 return; // Already initialized.
             }
-            native_init$ravenwood(values, keyReadablePredicate, keyWritablePredicate,
-                    () -> {});
+            init$ravenwood(values, keyReadablePredicate, keyWritablePredicate);
         }
     }
 
-    public static void native_init$ravenwood(Map<String, String> values,
-            Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate,
-            Runnable changeCallback) {
+    public static void init$ravenwood(Map<String, String> values,
+            Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate) {
         synchronized (sLock) {
             sValues = Objects.requireNonNull(values);
             sKeyReadablePredicate = Objects.requireNonNull(keyReadablePredicate);
             sKeyWritablePredicate = Objects.requireNonNull(keyWritablePredicate);
-            sChangeCallback = Objects.requireNonNull(changeCallback);
             sKeyHandles.clear();
+            synchronized (SystemProperties.sChangeCallbacks) {
+                SystemProperties.sChangeCallbacks.clear();
+            }
         }
     }
 
-    public static void native_reset$ravenwood() {
+    public static void reset$ravenwood() {
         synchronized (sLock) {
             sValues = null;
             sKeyReadablePredicate = null;
             sKeyWritablePredicate = null;
-            sChangeCallback = null;
             sKeyHandles.clear();
+            synchronized (SystemProperties.sChangeCallbacks) {
+                SystemProperties.sChangeCallbacks.clear();
+            }
         }
     }
 
@@ -101,7 +100,7 @@
             } else {
                 sValues.put(key, val);
             }
-            sChangeCallback.run();
+            SystemProperties.callChangeCallbacks();
         }
     }
 
@@ -183,7 +182,7 @@
         // Report through callback always registered via init above
         synchronized (sLock) {
             Preconditions.requireNonNullViaRavenwoodRule(sValues);
-            sChangeCallback.run();
+            SystemProperties.callChangeCallbacks();
         }
     }
 
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/EventLog_host.java b/ravenwood/runtime-helper-src/framework/android/util/EventLog_host.java
similarity index 83%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/EventLog_host.java
rename to ravenwood/runtime-helper-src/framework/android/util/EventLog_host.java
index 55d4ffb..878a0ff 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/EventLog_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/util/EventLog_host.java
@@ -13,12 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.util;
 
 import com.android.internal.os.RuntimeInit;
 
 import java.io.PrintStream;
-import java.util.Collection;
 
 public class EventLog_host {
     public static int writeEvent(int tag, int value) {
@@ -58,15 +57,6 @@
         return sb.length();
     }
 
-    public static void readEvents(int[] tags, Collection<android.util.EventLog.Event> output) {
-        throw new UnsupportedOperationException();
-    }
-
-    public static void readEventsOnWrapping(int[] tags, long timestamp,
-            Collection<android.util.EventLog.Event> output) {
-        throw new UnsupportedOperationException();
-    }
-
     /**
      * Return the "real" {@code System.out} if it's been swapped by {@code RavenwoodRuleImpl}, so
      * that we don't end up in a recursive loop.
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Log_host.java b/ravenwood/runtime-helper-src/framework/android/util/Log_host.java
similarity index 95%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Log_host.java
rename to ravenwood/runtime-helper-src/framework/android/util/Log_host.java
index f301b9c..d232ef2 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Log_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/util/Log_host.java
@@ -13,9 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.util;
 
-import android.util.Log;
 import android.util.Log.Level;
 
 import com.android.internal.os.RuntimeInit;
@@ -44,7 +43,7 @@
             case Log.LOG_ID_SYSTEM: buffer = "system"; break;
             case Log.LOG_ID_CRASH: buffer = "crash"; break;
             default: buffer = "buf:" + bufID; break;
-        };
+        }
 
         final String prio;
         switch (priority) {
@@ -55,7 +54,7 @@
             case Log.ERROR: prio = "E"; break;
             case Log.ASSERT: prio = "A"; break;
             default: prio = "prio:" + priority; break;
-        };
+        }
 
         for (String s : msg.split("\\n")) {
             getRealOut().println(String.format("logd: [%s] %s %s: %s", buffer, prio, tag, s));
diff --git a/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayContainer_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayContainer_host.java
new file mode 100644
index 0000000..c18c307
--- /dev/null
+++ b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayContainer_host.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.os;
+
+import java.util.Arrays;
+import java.util.HashMap;
+
+public class LongArrayContainer_host {
+    private static final HashMap<Long, long[]> sInstances = new HashMap<>();
+    private static long sNextId = 1;
+
+    public static long native_init(int arrayLength) {
+        long[] array = new long[arrayLength];
+        long instanceId = sNextId++;
+        sInstances.put(instanceId, array);
+        return instanceId;
+    }
+
+    static long[] getInstance(long instanceId) {
+        return sInstances.get(instanceId);
+    }
+
+    public static void native_setValues(long instanceId, long[] values) {
+        System.arraycopy(values, 0, getInstance(instanceId), 0, values.length);
+    }
+
+    public static void native_getValues(long instanceId, long[] values) {
+        System.arraycopy(getInstance(instanceId), 0, values, 0, values.length);
+    }
+
+    public static boolean native_combineValues(long instanceId, long[] array, int[] indexMap) {
+        long[] values = getInstance(instanceId);
+
+        boolean nonZero = false;
+        Arrays.fill(array, 0);
+
+        for (int i = 0; i < values.length; i++) {
+            int index = indexMap[i];
+            if (index < 0 || index >= array.length) {
+                throw new IndexOutOfBoundsException("Index " + index + " is out of bounds: [0, "
+                        + (array.length - 1) + "]");
+            }
+            if (values[i] != 0) {
+                array[index] += values[i];
+                nonZero = true;
+            }
+        }
+        return nonZero;
+    }
+}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongArrayMultiStateCounter_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java
similarity index 87%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongArrayMultiStateCounter_host.java
rename to ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java
index 0f65544..9ce8ea8 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongArrayMultiStateCounter_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.platform.test.ravenwood.nativesubstitution;
+package com.android.internal.os;
 
 import android.os.BadParcelableException;
 import android.os.Parcel;
@@ -28,7 +28,7 @@
 public class LongArrayMultiStateCounter_host {
 
     /**
-     * A reimplementation of {@link com.android.internal.os.LongArrayMultiStateCounter}, only in
+     * A reimplementation of {@link LongArrayMultiStateCounter}, only in
      * Java instead of native.  The majority of the code (in C++) can be found in
      * /frameworks/native/libs/battery/MultiStateCounter.h
      */
@@ -257,50 +257,6 @@
         }
     }
 
-    public static class LongArrayContainer_host {
-        private static final HashMap<Long, long[]> sInstances = new HashMap<>();
-        private static long sNextId = 1;
-
-        public static long native_init(int arrayLength) {
-            long[] array = new long[arrayLength];
-            long instanceId = sNextId++;
-            sInstances.put(instanceId, array);
-            return instanceId;
-        }
-
-        static long[] getInstance(long instanceId) {
-            return sInstances.get(instanceId);
-        }
-
-        public static void native_setValues(long instanceId, long[] values) {
-            System.arraycopy(values, 0, getInstance(instanceId), 0, values.length);
-        }
-
-        public static void native_getValues(long instanceId, long[] values) {
-            System.arraycopy(getInstance(instanceId), 0, values, 0, values.length);
-        }
-
-        public static boolean native_combineValues(long instanceId, long[] array, int[] indexMap) {
-            long[] values = getInstance(instanceId);
-
-            boolean nonZero = false;
-            Arrays.fill(array, 0);
-
-            for (int i = 0; i < values.length; i++) {
-                int index = indexMap[i];
-                if (index < 0 || index >= array.length) {
-                    throw new IndexOutOfBoundsException("Index " + index + " is out of bounds: [0, "
-                                                        + (array.length - 1) + "]");
-                }
-                if (values[i] != 0) {
-                    array[index] += values[i];
-                    nonZero = true;
-                }
-            }
-            return nonZero;
-        }
-    }
-
     private static final HashMap<Long, LongArrayMultiStateCounterRavenwood> sInstances =
             new HashMap<>();
     private static long sNextId = 1;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongMultiStateCounter_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java
similarity index 98%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongMultiStateCounter_host.java
rename to ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java
index 9486651..1d95aa1 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongMultiStateCounter_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.platform.test.ravenwood.nativesubstitution;
+package com.android.internal.os;
 
 import android.os.BadParcelableException;
 import android.os.Parcel;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/ravenwood/RavenwoodEnvironment_host.java
similarity index 86%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java
rename to ravenwood/runtime-helper-src/framework/com/android/internal/ravenwood/RavenwoodEnvironment_host.java
index 58f6bbb..3bf116d 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/internal/ravenwood/RavenwoodEnvironment_host.java
@@ -13,12 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.platform.test.ravenwood.nativesubstitution;
+package com.android.internal.ravenwood;
 
+import android.os.SystemProperties_host;
 import android.platform.test.ravenwood.RavenwoodSystemProperties;
 import android.util.Log;
 
-import com.android.internal.ravenwood.RavenwoodEnvironment;
 import com.android.ravenwood.common.JvmWorkaround;
 import com.android.ravenwood.common.RavenwoodCommonUtils;
 
@@ -36,7 +36,7 @@
     /**
      * Called from {@link RavenwoodEnvironment#ensureRavenwoodInitialized()}.
      */
-    public static void nativeEnsureRavenwoodInitialized() {
+    public static void ensureRavenwoodInitialized() {
 
         // TODO Unify it with the initialization code in RavenwoodAwareTestRunnerHook.
 
@@ -63,14 +63,14 @@
     /**
      * Called from {@link RavenwoodEnvironment#getRavenwoodRuntimePath()}.
      */
-    public static String nativeGetRavenwoodRuntimePath(RavenwoodEnvironment env) {
+    public static String getRavenwoodRuntimePath(RavenwoodEnvironment env) {
         return RavenwoodCommonUtils.getRavenwoodRuntimePath();
     }
 
     /**
      * Called from {@link RavenwoodEnvironment#fromAddress(long)}.
      */
-    public static <T> T nativeFromAddress(RavenwoodEnvironment env, long address) {
+    public static <T> T fromAddress(RavenwoodEnvironment env, long address) {
         return JvmWorkaround.getInstance().fromAddress(address);
     }
 }
diff --git a/ravenwood/texts/ravenwood-standard-options.txt b/ravenwood/texts/ravenwood-standard-options.txt
index 952ab82..3ec3e3c 100644
--- a/ravenwood/texts/ravenwood-standard-options.txt
+++ b/ravenwood/texts/ravenwood-standard-options.txt
@@ -32,8 +32,11 @@
 --substitute-annotation
     android.ravenwood.annotation.RavenwoodReplace
 
---native-substitute-annotation
-    android.ravenwood.annotation.RavenwoodNativeSubstitutionClass
+--redirect-annotation
+    android.ravenwood.annotation.RavenwoodRedirect
+
+--redirection-class-annotation
+    android.ravenwood.annotation.RavenwoodRedirectionClass
 
 --class-load-hook-annotation
     android.ravenwood.annotation.RavenwoodClassLoadHook
diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
index eba628d..0947238 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/FutureAppSearchSession.java
@@ -16,15 +16,15 @@
 
 package com.android.server.appfunctions;
 
-import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER;
-
-import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.app.appsearch.AppSearchBatchResult;
 import android.app.appsearch.AppSearchManager;
 import android.app.appsearch.AppSearchManager.SearchContext;
 import android.app.appsearch.AppSearchResult;
 import android.app.appsearch.AppSearchSession;
+import android.app.appsearch.BatchResultCallback;
+import android.app.appsearch.GenericDocument;
+import android.app.appsearch.GetByDocumentIdRequest;
 import android.app.appsearch.GetSchemaResponse;
 import android.app.appsearch.PutDocumentsRequest;
 import android.app.appsearch.SearchResult;
@@ -42,10 +42,7 @@
 import java.util.Objects;
 import java.util.concurrent.Executor;
 
-/**
- * A future API wrapper of {@link AppSearchSession} APIs.
- */
-@FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER)
+/** A future API wrapper of {@link AppSearchSession} APIs. */
 public class FutureAppSearchSession implements Closeable {
     private static final String TAG = FutureAppSearchSession.class.getSimpleName();
     private final Executor mExecutor;
@@ -67,14 +64,14 @@
 
     /** Converts a failed app search result codes into an exception. */
     @NonNull
-    private static Exception failedResultToException(@NonNull AppSearchResult<?> appSearchResult) {
+    public static Exception failedResultToException(@NonNull AppSearchResult<?> appSearchResult) {
         return switch (appSearchResult.getResultCode()) {
-            case AppSearchResult.RESULT_INVALID_ARGUMENT -> new IllegalArgumentException(
-                    appSearchResult.getErrorMessage());
-            case AppSearchResult.RESULT_IO_ERROR -> new IOException(
-                    appSearchResult.getErrorMessage());
-            case AppSearchResult.RESULT_SECURITY_ERROR -> new SecurityException(
-                    appSearchResult.getErrorMessage());
+            case AppSearchResult.RESULT_INVALID_ARGUMENT ->
+                    new IllegalArgumentException(appSearchResult.getErrorMessage());
+            case AppSearchResult.RESULT_IO_ERROR ->
+                    new IOException(appSearchResult.getErrorMessage());
+            case AppSearchResult.RESULT_SECURITY_ERROR ->
+                    new SecurityException(appSearchResult.getErrorMessage());
             default -> new IllegalStateException(appSearchResult.getErrorMessage());
         };
     }
@@ -137,14 +134,16 @@
     /** Indexes documents into the AppSearchSession database. */
     public AndroidFuture<AppSearchBatchResult<String, Void>> put(
             @NonNull PutDocumentsRequest putDocumentsRequest) {
-        return getSessionAsync().thenCompose(
-                session -> {
-                    AndroidFuture<AppSearchBatchResult<String, Void>> batchResultFuture =
-                            new AndroidFuture<>();
+        return getSessionAsync()
+                .thenCompose(
+                        session -> {
+                            AndroidFuture<AppSearchBatchResult<String, Void>> batchResultFuture =
+                                    new AndroidFuture<>();
 
-                    session.put(putDocumentsRequest, mExecutor, batchResultFuture::complete);
-                    return batchResultFuture;
-                });
+                            session.put(
+                                    putDocumentsRequest, mExecutor, batchResultFuture::complete);
+                            return batchResultFuture;
+                        });
     }
 
     /**
@@ -152,10 +151,9 @@
      * of search provided.
      */
     public AndroidFuture<FutureSearchResults> search(
-            @NonNull String queryExpression,
-            @NonNull SearchSpec searchSpec) {
-        return getSessionAsync().thenApply(
-                        session -> session.search(queryExpression, searchSpec))
+            @NonNull String queryExpression, @NonNull SearchSpec searchSpec) {
+        return getSessionAsync()
+                .thenApply(session -> session.search(queryExpression, searchSpec))
                 .thenApply(result -> new FutureSearchResults(result, mExecutor));
     }
 
@@ -173,8 +171,8 @@
         private final SearchResults mSearchResults;
         private final Executor mExecutor;
 
-        public FutureSearchResults(@NonNull SearchResults searchResults,
-                @NonNull Executor executor) {
+        public FutureSearchResults(
+                @NonNull SearchResults searchResults, @NonNull Executor executor) {
             mSearchResults = Objects.requireNonNull(searchResults);
             mExecutor = Objects.requireNonNull(executor);
         }
@@ -184,15 +182,68 @@
                     new AndroidFuture<>();
 
             mSearchResults.getNextPage(mExecutor, nextPageFuture::complete);
-            return nextPageFuture.thenApply(result -> {
-                if (result.isSuccess()) {
-                    return result.getResultValue();
-                } else {
-                    throw new RuntimeException(
-                            failedResultToException(result));
-                }
-            });
+            return nextPageFuture.thenApply(
+                    result -> {
+                        if (result.isSuccess()) {
+                            return result.getResultValue();
+                        } else {
+                            throw new RuntimeException(failedResultToException(result));
+                        }
+                    });
+        }
+    }
+
+    /** A future API to retrieve a document by its id from the local AppSearch session. */
+    public AndroidFuture<GenericDocument> getByDocumentId(
+            @NonNull String documentId, @NonNull String namespace) {
+        Objects.requireNonNull(documentId);
+        Objects.requireNonNull(namespace);
+
+        GetByDocumentIdRequest request =
+                new GetByDocumentIdRequest.Builder(namespace)
+                        .addIds(documentId)
+                        .build();
+        return getSessionAsync()
+                .thenCompose(
+                        session -> {
+                            AndroidFuture<AppSearchBatchResult<String, GenericDocument>>
+                                    batchResultFuture = new AndroidFuture<>();
+                            session.getByDocumentId(
+                                    request,
+                                    mExecutor,
+                                    new BatchResultCallbackAdapter<>(batchResultFuture));
+
+                            return batchResultFuture.thenApply(
+                                    batchResult ->
+                                            getGenericDocumentFromBatchResult(
+                                                    batchResult, documentId));
+                        });
+    }
+
+    private static GenericDocument getGenericDocumentFromBatchResult(
+            AppSearchBatchResult<String, GenericDocument> result, String documentId) {
+        if (result.isSuccess()) {
+            return result.getSuccesses().get(documentId);
+        }
+        throw new IllegalArgumentException("No document in the result for id: " + documentId);
+    }
+
+    private static final class BatchResultCallbackAdapter<K, V>
+            implements BatchResultCallback<K, V> {
+        private final AndroidFuture<AppSearchBatchResult<K, V>> mFuture;
+
+        BatchResultCallbackAdapter(AndroidFuture<AppSearchBatchResult<K, V>> future) {
+            mFuture = future;
         }
 
+        @Override
+        public void onResult(@NonNull AppSearchBatchResult<K, V> result) {
+            mFuture.complete(result);
+        }
+
+        @Override
+        public void onSystemError(Throwable t) {
+            mFuture.completeExceptionally(t);
+        }
     }
 }
diff --git a/services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java b/services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java
new file mode 100644
index 0000000..0c22624
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/FutureGlobalSearchSession.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.annotation.NonNull;
+import android.app.appsearch.AppSearchManager;
+import android.app.appsearch.AppSearchResult;
+import android.app.appsearch.GlobalSearchSession;
+import android.app.appsearch.exceptions.AppSearchException;
+import android.app.appsearch.observer.ObserverCallback;
+import android.app.appsearch.observer.ObserverSpec;
+import android.util.Slog;
+
+import com.android.internal.infra.AndroidFuture;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.concurrent.Executor;
+
+/** A wrapper around {@link GlobalSearchSession} that provides a future-based API. */
+public class FutureGlobalSearchSession implements Closeable {
+    private static final String TAG = FutureGlobalSearchSession.class.getSimpleName();
+    private final Executor mExecutor;
+    private final AndroidFuture<AppSearchResult<GlobalSearchSession>> mSettableSessionFuture;
+
+    public FutureGlobalSearchSession(
+            @NonNull AppSearchManager appSearchManager, @NonNull Executor executor) {
+        this.mExecutor = executor;
+        mSettableSessionFuture = new AndroidFuture<>();
+        appSearchManager.createGlobalSearchSession(mExecutor, mSettableSessionFuture::complete);
+    }
+
+    private AndroidFuture<GlobalSearchSession> getSessionAsync() {
+        return mSettableSessionFuture.thenApply(
+                result -> {
+                    if (result.isSuccess()) {
+                        return result.getResultValue();
+                    } else {
+                        throw new RuntimeException(
+                                FutureAppSearchSession.failedResultToException(result));
+                    }
+                });
+    }
+
+    /**
+     * Registers an observer callback for the given target package name.
+     *
+     * @param targetPackageName The package name of the target app.
+     * @param spec The observer spec.
+     * @param executor The executor to run the observer callback on.
+     * @param observer The observer callback to register.
+     * @return A future that completes once the observer is registered.
+     */
+    public AndroidFuture<Void> registerObserverCallbackAsync(
+            String targetPackageName,
+            ObserverSpec spec,
+            Executor executor,
+            ObserverCallback observer) {
+        return getSessionAsync()
+                .thenCompose(
+                        session -> {
+                            try {
+                                session.registerObserverCallback(
+                                        targetPackageName, spec, executor, observer);
+                                return AndroidFuture.completedFuture(null);
+                            } catch (AppSearchException e) {
+                                throw new RuntimeException(e);
+                            }
+                        });
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            getSessionAsync().get().close();
+        } catch (Exception ex) {
+            Slog.e(TAG, "Failed to close global search session", ex);
+        }
+    }
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
new file mode 100644
index 0000000..be5770b
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appfunctions;
+
+import android.annotation.NonNull;
+import android.annotation.WorkerThread;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchSpec;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.appfunctions.FutureAppSearchSession.FutureSearchResults;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+/**
+ * This class implements helper methods for synchronously interacting with AppSearch while
+ * synchronizing AppFunction runtime and static metadata.
+ */
+public class MetadataSyncAdapter {
+    private final FutureAppSearchSession mFutureAppSearchSession;
+    private final Executor mSyncExecutor;
+
+    public MetadataSyncAdapter(
+            @NonNull Executor syncExecutor,
+            @NonNull FutureAppSearchSession futureAppSearchSession) {
+        mSyncExecutor = Objects.requireNonNull(syncExecutor);
+        mFutureAppSearchSession = Objects.requireNonNull(futureAppSearchSession);
+    }
+
+    /**
+     * This method returns a map of package names to a set of function ids that are in the static
+     * metadata but not in the runtime metadata.
+     *
+     * @param staticPackageToFunctionMap A map of package names to a set of function ids from the
+     *     static metadata.
+     * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the
+     *     runtime metadata.
+     * @return A map of package names to a set of function ids that are in the static metadata but
+     *     not in the runtime metadata.
+     */
+    @NonNull
+    @VisibleForTesting
+    static ArrayMap<String, ArraySet<String>> getAddedFunctionsDiffMap(
+            ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap,
+            ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) {
+        return getFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap);
+    }
+
+    /**
+     * This method returns a map of package names to a set of function ids that are in the runtime
+     * metadata but not in the static metadata.
+     *
+     * @param staticPackageToFunctionMap A map of package names to a set of function ids from the
+     *     static metadata.
+     * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the
+     *     runtime metadata.
+     * @return A map of package names to a set of function ids that are in the runtime metadata but
+     *     not in the static metadata.
+     */
+    @NonNull
+    @VisibleForTesting
+    static ArrayMap<String, ArraySet<String>> getRemovedFunctionsDiffMap(
+            ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap,
+            ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) {
+        return getFunctionsDiffMap(runtimePackageToFunctionMap, staticPackageToFunctionMap);
+    }
+
+    @NonNull
+    private static ArrayMap<String, ArraySet<String>> getFunctionsDiffMap(
+            ArrayMap<String, ArraySet<String>> packageToFunctionMapA,
+            ArrayMap<String, ArraySet<String>> packageToFunctionMapB) {
+        ArrayMap<String, ArraySet<String>> diffMap = new ArrayMap<>();
+        for (String packageName : packageToFunctionMapA.keySet()) {
+            if (!packageToFunctionMapB.containsKey(packageName)) {
+                diffMap.put(packageName, packageToFunctionMapA.get(packageName));
+                continue;
+            }
+            ArraySet<String> diffFunctions = new ArraySet<>();
+            for (String functionId :
+                    Objects.requireNonNull(packageToFunctionMapA.get(packageName))) {
+                if (!Objects.requireNonNull(packageToFunctionMapB.get(packageName))
+                        .contains(functionId)) {
+                    diffFunctions.add(functionId);
+                }
+            }
+            if (!diffFunctions.isEmpty()) {
+                diffMap.put(packageName, diffFunctions);
+            }
+        }
+        return diffMap;
+    }
+
+    /**
+     * This method returns a map of package names to a set of function ids.
+     *
+     * @param queryExpression The query expression to use when searching for AppFunction metadata.
+     * @param metadataSearchSpec The search spec to use when searching for AppFunction metadata.
+     * @return A map of package names to a set of function ids.
+     * @throws ExecutionException If the future search results fail to execute.
+     * @throws InterruptedException If the future search results are interrupted.
+     */
+    @NonNull
+    @VisibleForTesting
+    @WorkerThread
+    ArrayMap<String, ArraySet<String>> getPackageToFunctionIdMap(
+            @NonNull String queryExpression,
+            @NonNull SearchSpec metadataSearchSpec,
+            @NonNull String propertyFunctionId,
+            @NonNull String propertyPackageName)
+            throws ExecutionException, InterruptedException {
+        ArrayMap<String, ArraySet<String>> packageToFunctionIds = new ArrayMap<>();
+        FutureSearchResults futureSearchResults =
+                mFutureAppSearchSession.search(queryExpression, metadataSearchSpec).get();
+        List<SearchResult> searchResultsList = futureSearchResults.getNextPage().get();
+        // TODO(b/357551503): This could be expensive if we have more functions
+        while (!searchResultsList.isEmpty()) {
+            for (SearchResult searchResult : searchResultsList) {
+                String packageName =
+                        searchResult.getGenericDocument().getPropertyString(propertyPackageName);
+                String functionId =
+                        searchResult.getGenericDocument().getPropertyString(propertyFunctionId);
+                packageToFunctionIds
+                        .computeIfAbsent(packageName, k -> new ArraySet<>())
+                        .add(functionId);
+            }
+            searchResultsList = futureSearchResults.getNextPage().get();
+        }
+        return packageToFunctionIds;
+    }
+}
diff --git a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java
index 98903ae..58597c3 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCaller.java
@@ -25,7 +25,6 @@
  * services are properly unbound after the operation completes or a timeout occurs.
  *
  * @param <T> Class of wrapped service.
- * @hide
  */
 public interface RemoteServiceCaller<T> {
 
diff --git a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java
index 0e18705..eea17ee 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/RemoteServiceCallerImpl.java
@@ -34,7 +34,6 @@
  * Context#bindService}.
  *
  * @param <T> Class of wrapped service.
- * @hide
  */
 public class RemoteServiceCallerImpl<T> implements RemoteServiceCaller<T> {
     private static final String TAG = "AppFunctionsServiceCall";
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 2937307..99c3eca 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -139,6 +139,7 @@
     static final String[] sDeviceConfigAconfigScopes = new String[] {
         "accessibility",
         "android_core_networking",
+        "android_health_services",
         "android_sdk",
         "android_stylus",
         "aoc",
@@ -235,7 +236,6 @@
         "wear_connectivity",
         "wear_esim_carriers",
         "wear_frameworks",
-        "wear_health_services",
         "wear_media",
         "wear_offload",
         "wear_security",
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 780eda6..6daf0d0 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -12712,11 +12712,6 @@
             if (mController == null)
                 return;
             try {
-                // TODO: remove this when deprecating STREAM_BLUETOOTH_SCO
-                if (isStreamBluetoothSco(streamType)) {
-                    // TODO: notify both sco and voice_call about volume changes
-                    streamType = AudioSystem.STREAM_BLUETOOTH_SCO;
-                }
                 mController.volumeChanged(streamType, flags);
             } catch (RemoteException e) {
                 Log.w(TAG, "Error calling volumeChanged", e);
@@ -14727,6 +14722,7 @@
     @Override
     /** @see AudioManager#permissionUpdateBarrier() */
     public void permissionUpdateBarrier() {
+        if (!audioserverPermissions()) return;
         mAudioSystem.triggerSystemPropertyUpdate(mSysPropListenerNativeHandle);
         List<Future> snapshot;
         synchronized (mScheduledPermissionTasks) {
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 4b85217..101596d 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -1150,6 +1150,13 @@
             return Constants.ABORT_REFUSED;
         }
 
+        if (mArcEstablished) {
+            HdmiLogger.debug("ARC is already established.");
+            HdmiCecMessage command = HdmiCecMessageBuilder.buildReportArcInitiated(
+                getDeviceInfo().getLogicalAddress(), message.getSource());
+            mService.sendCecCommand(command);
+            return Constants.HANDLED;
+        }
         // In case where <Initiate Arc> is started by <Request ARC Initiation>, this message is
         // handled in RequestArcInitiationAction as well.
         SetArcTransmissionStateAction action = new SetArcTransmissionStateAction(this,
diff --git a/services/core/java/com/android/server/hdmi/RequestSadAction.java b/services/core/java/com/android/server/hdmi/RequestSadAction.java
index 0188e96..2566300 100644
--- a/services/core/java/com/android/server/hdmi/RequestSadAction.java
+++ b/services/core/java/com/android/server/hdmi/RequestSadAction.java
@@ -19,6 +19,8 @@
 import android.hardware.hdmi.HdmiControlManager;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -36,7 +38,8 @@
     // State in which the action is waiting for <Report Short Audio Descriptor>.
     private static final int STATE_WAITING_FOR_REPORT_SAD = 1;
     private static final int MAX_SAD_PER_REQUEST = 4;
-    private static final int RETRY_COUNTER_MAX = 1;
+    @VisibleForTesting
+    public static final int RETRY_COUNTER_MAX = 3;
     private final int mTargetAddress;
     private final RequestSadCallback mCallback;
     private final List<Integer> mCecCodecsToQuery = new ArrayList<>();
diff --git a/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java b/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java
index 5ab22e1..e6abcb9 100644
--- a/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java
+++ b/services/core/java/com/android/server/hdmi/SetArcTransmissionStateAction.java
@@ -60,6 +60,12 @@
     boolean start() {
         // Seq #37.
         if (mEnabled) {
+            // Avoid triggering duplicate RequestSadAction events.
+            // This could lead to unexpected responses from the AVR and cause the TV to receive data
+            // out of order. The SAD report does not provide information about the order of events.
+            if ((tv().hasAction(RequestSadAction.class))) {
+                return true;
+            }
             // Request SADs before enabling ARC
             RequestSadAction action = new RequestSadAction(
                     localDevice(), Constants.ADDR_AUDIO_SYSTEM,
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 84cee7e..1285a61 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -2269,13 +2269,15 @@
     // Native callback.
     @SuppressWarnings("unused")
     private void notifyTouchpadHardwareState(TouchpadHardwareState hardwareStates, int deviceId) {
-        // TODO(b/286551975): sent the touchpad hardware state data here to TouchpadDebugActivity
         Slog.d(TAG, "notifyTouchpadHardwareState: Time: "
                 + hardwareStates.getTimestamp() + ", No. Buttons: "
                 + hardwareStates.getButtonsDown() + ", No. Fingers: "
                 + hardwareStates.getFingerCount() + ", No. Touch: "
                 + hardwareStates.getTouchCount() + ", Id: "
                 + deviceId);
+        if (mTouchpadDebugViewController != null) {
+            mTouchpadDebugViewController.updateTouchpadHardwareState(hardwareStates);
+        }
     }
 
     // Native callback.
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
index 7785ffb..ba56ad0 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
@@ -30,6 +30,9 @@
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import com.android.server.input.TouchpadFingerState;
+import com.android.server.input.TouchpadHardwareState;
+
 import java.util.Objects;
 
 public class TouchpadDebugView extends LinearLayout {
@@ -52,6 +55,10 @@
     private int mScreenHeight;
     private int mWindowLocationBeforeDragX;
     private int mWindowLocationBeforeDragY;
+    @NonNull
+    private TouchpadHardwareState mLastTouchpadState =
+            new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0,
+                    new TouchpadFingerState[0]);
 
     public TouchpadDebugView(Context context, int touchpadId) {
         super(context);
@@ -83,14 +90,14 @@
 
     private void init(Context context) {
         setOrientation(VERTICAL);
-        setLayoutParams(new LinearLayout.LayoutParams(
-                LinearLayout.LayoutParams.WRAP_CONTENT,
-                LinearLayout.LayoutParams.WRAP_CONTENT));
-        setBackgroundColor(Color.TRANSPARENT);
+        setLayoutParams(new LayoutParams(
+                LayoutParams.WRAP_CONTENT,
+                LayoutParams.WRAP_CONTENT));
+        setBackgroundColor(Color.RED);
 
         // TODO(b/286551975): Replace this content with the touchpad debug view.
         TextView textView1 = new TextView(context);
-        textView1.setBackgroundColor(Color.parseColor("#FFFF0000"));
+        textView1.setBackgroundColor(Color.TRANSPARENT);
         textView1.setTextSize(20);
         textView1.setText("Touchpad Debug View 1");
         textView1.setGravity(Gravity.CENTER);
@@ -98,7 +105,7 @@
         textView1.setLayoutParams(new LayoutParams(1000, 200));
 
         TextView textView2 = new TextView(context);
-        textView2.setBackgroundColor(Color.BLUE);
+        textView2.setBackgroundColor(Color.TRANSPARENT);
         textView2.setTextSize(20);
         textView2.setText("Touchpad Debug View 2");
         textView2.setGravity(Gravity.CENTER);
@@ -126,9 +133,7 @@
             case MotionEvent.ACTION_MOVE:
                 deltaX = event.getRawX() - mWindowLayoutParams.x - mTouchDownX;
                 deltaY = event.getRawY() - mWindowLayoutParams.y - mTouchDownY;
-                Slog.d("TouchpadDebugView", "Slop = " + mTouchSlop);
                 if (isSlopExceeded(deltaX, deltaY)) {
-                    Slog.d("TouchpadDebugView", "Slop exceeded");
                     mWindowLayoutParams.x =
                             Math.max(0, Math.min((int) (event.getRawX() - mTouchDownX),
                                     mScreenWidth - this.getWidth()));
@@ -136,9 +141,6 @@
                             Math.max(0, Math.min((int) (event.getRawY() - mTouchDownY),
                                     mScreenHeight - this.getHeight()));
 
-                    Slog.d("TouchpadDebugView", "New position X: "
-                            + mWindowLayoutParams.x + ", Y: " + mWindowLayoutParams.y);
-
                     mWindowManager.updateViewLayout(this, mWindowLayoutParams);
                 }
                 return true;
@@ -166,7 +168,7 @@
     @Override
     public boolean performClick() {
         super.performClick();
-        Slog.d("TouchpadDebugView", "You clicked me!");
+        Slog.d("TouchpadDebugView", "You tapped the window!");
         return true;
     }
 
@@ -201,4 +203,34 @@
     public WindowManager.LayoutParams getWindowLayoutParams() {
         return mWindowLayoutParams;
     }
+
+    public void updateHardwareState(TouchpadHardwareState touchpadHardwareState) {
+        if (mLastTouchpadState.getButtonsDown() == 0) {
+            if (touchpadHardwareState.getButtonsDown() > 0) {
+                onTouchpadButtonPress();
+            }
+        } else {
+            if (touchpadHardwareState.getButtonsDown() == 0) {
+                onTouchpadButtonRelease();
+            }
+        }
+        mLastTouchpadState = touchpadHardwareState;
+    }
+
+    private void onTouchpadButtonPress() {
+        Slog.d("TouchpadDebugView", "You clicked me!");
+
+        // Iterate through all child views
+        // Temporary demonstration for testing
+        for (int i = 0; i < getChildCount(); i++) {
+            getChildAt(i).setBackgroundColor(Color.BLUE);
+        }
+    }
+
+    private void onTouchpadButtonRelease() {
+        Slog.d("TouchpadDebugView", "You released the click");
+        for (int i = 0; i < getChildCount(); i++) {
+            getChildAt(i).setBackgroundColor(Color.RED);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
index c28e74a..bc53c49 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
@@ -27,6 +27,7 @@
 
 import com.android.server.input.InputManagerService;
 import com.android.server.input.TouchpadHardwareProperties;
+import com.android.server.input.TouchpadHardwareState;
 
 import java.util.Objects;
 
@@ -132,4 +133,10 @@
         mTouchpadDebugView = null;
         Slog.d(TAG, "Touchpad debug view removed.");
     }
+
+    public void updateTouchpadHardwareState(TouchpadHardwareState touchpadHardwareState) {
+        if (mTouchpadDebugView != null) {
+            mTouchpadDebugView.updateHardwareState(touchpadHardwareState);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 4fcf27d..38ef5b8 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -3403,8 +3403,13 @@
             // It's OK to dump the credential type since anyone with physical access can just
             // observe it from the keyguard directly.
             pw.println("Quality: " + getKeyguardStoredQuality(userId));
-            pw.println("CredentialType: " + LockPatternUtils.credentialTypeToString(
-                    getCredentialTypeInternal(userId)));
+            final int credentialType = getCredentialTypeInternal(userId);
+            pw.println("CredentialType: "
+                    + LockPatternUtils.credentialTypeToString(credentialType));
+            if (credentialType == CREDENTIAL_TYPE_NONE) {
+                pw.println("IsLockScreenDisabled: "
+                        + getBoolean(LockPatternUtils.DISABLE_LOCKSCREEN_KEY, false, userId));
+            }
             pw.println("SeparateChallenge: " + getSeparateProfileChallengeEnabledInternal(userId));
             pw.println(TextUtils.formatSimple("Metrics: %s",
                     getUserPasswordMetrics(userId) != null ? "known" : "unknown"));
diff --git a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java
index bad959a..925ba17 100644
--- a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java
+++ b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java
@@ -22,6 +22,7 @@
 import static com.android.server.notification.ZenLog.traceApplyDeviceEffect;
 import static com.android.server.notification.ZenLog.traceScheduleApplyDeviceEffect;
 
+import android.app.KeyguardManager;
 import android.app.UiModeManager;
 import android.app.WallpaperManager;
 import android.content.BroadcastReceiver;
@@ -53,6 +54,7 @@
 
     private final Context mContext;
     private final ColorDisplayManager mColorDisplayManager;
+    private final KeyguardManager mKeyguardManager;
     private final PowerManager mPowerManager;
     private final UiModeManager mUiModeManager;
     private final WallpaperManager mWallpaperManager;
@@ -67,6 +69,7 @@
     DefaultDeviceEffectsApplier(Context context) {
         mContext = context;
         mColorDisplayManager = context.getSystemService(ColorDisplayManager.class);
+        mKeyguardManager = context.getSystemService(KeyguardManager.class);
         mPowerManager = context.getSystemService(PowerManager.class);
         mUiModeManager = context.getSystemService(UiModeManager.class);
         WallpaperManager wallpaperManager = context.getSystemService(WallpaperManager.class);
@@ -133,12 +136,14 @@
 
         // Changing the theme can be disruptive for the user (Activities are likely recreated, may
         // lose some state). Therefore we only apply the change immediately if the rule was
-        // activated manually, or we are initializing, or the screen is currently off/dreaming.
+        // activated manually, or we are initializing, or the screen is currently off/dreaming,
+        // or if the device is locked.
         if (origin == ZenModeConfig.ORIGIN_INIT
                 || origin == ZenModeConfig.ORIGIN_INIT_USER
                 || origin == ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI
                 || origin == ZenModeConfig.ORIGIN_USER_IN_APP
-                || !mPowerManager.isInteractive()) {
+                || !mPowerManager.isInteractive()
+                || (android.app.Flags.modesUi() && mKeyguardManager.isKeyguardLocked())) {
             unregisterScreenOffReceiver();
             updateNightModeImmediately(useNightMode);
         } else {
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 43a285c..2856eb4 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -1019,7 +1019,9 @@
                     && scanInstallPackages(requests, createdAppId, versionInfos)) {
                 List<ReconciledPackage> reconciledPackages =
                         reconcileInstallPackages(requests, versionInfos);
-                if (reconciledPackages != null && commitInstallPackages(reconciledPackages)) {
+                if (reconciledPackages != null
+                        && renameAndUpdatePaths(requests)
+                        && commitInstallPackages(reconciledPackages)) {
                     success = true;
                 }
             }
@@ -1029,24 +1031,49 @@
         }
     }
 
-    private boolean prepareInstallPackages(List<InstallRequest> requests) {
-        // TODO: will remove the locking after doRename is moved out of prepare
+    private boolean renameAndUpdatePaths(List<InstallRequest> requests) {
         try (PackageManagerTracedLock installLock = mPm.mInstallLock.acquireLock()) {
             for (InstallRequest request : requests) {
-                try {
-                    Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "preparePackage");
-                    request.onPrepareStarted();
-                    preparePackageLI(request);
-                } catch (PrepareFailure prepareFailure) {
-                    request.setError(prepareFailure.error,
-                            prepareFailure.getMessage());
-                    request.setOriginPackage(prepareFailure.mConflictingPackage);
-                    request.setOriginPermission(prepareFailure.mConflictingPermission);
-                    return false;
-                } finally {
-                    request.onPrepareFinished();
-                    Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+                ParsedPackage parsedPackage = request.getParsedPackage();
+                final boolean isApex = (request.getScanFlags() & SCAN_AS_APEX) != 0;
+                if (isApex) {
+                    continue;
                 }
+                try {
+                    doRenameLI(request, parsedPackage);
+                    setUpFsVerity(parsedPackage);
+                } catch (Installer.InstallerException | IOException | DigestException
+                         | NoSuchAlgorithmException | PrepareFailure e) {
+                    request.setError(PackageManagerException.INTERNAL_ERROR_VERITY_SETUP,
+                            "Failed to set up verity: " + e);
+                    return false;
+                }
+
+                // update paths that are set before renaming
+                PackageSetting scannedPackageSetting = request.getScannedPackageSetting();
+                scannedPackageSetting.setPath(new File(parsedPackage.getPath()));
+                scannedPackageSetting.setLegacyNativeLibraryPath(
+                        parsedPackage.getNativeLibraryRootDir());
+            }
+            return true;
+        }
+    }
+
+    private boolean prepareInstallPackages(List<InstallRequest> requests) {
+        for (InstallRequest request : requests) {
+            try {
+                Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "preparePackage");
+                request.onPrepareStarted();
+                preparePackage(request);
+            } catch (PrepareFailure prepareFailure) {
+                request.setError(prepareFailure.error,
+                        prepareFailure.getMessage());
+                request.setOriginPackage(prepareFailure.mConflictingPackage);
+                request.setOriginPermission(prepareFailure.mConflictingPermission);
+                return false;
+            } finally {
+                request.onPrepareFinished();
+                Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
             }
         }
         return true;
@@ -1231,8 +1258,7 @@
         return newProp != null && newProp.getBoolean();
     }
 
-    @GuardedBy("mPm.mInstallLock")
-    private void preparePackageLI(InstallRequest request) throws PrepareFailure {
+    private void preparePackage(InstallRequest request) throws PrepareFailure {
         final int[] allUsers =  mPm.mUserManager.getUserIds();
         final int installFlags = request.getInstallFlags();
         final boolean onExternal = request.getVolumeUuid() != null;
@@ -1739,18 +1765,7 @@
             }
         }
 
-        if (!isApex) {
-            doRenameLI(request, parsedPackage);
-
-            try {
-                setUpFsVerity(parsedPackage);
-            } catch (Installer.InstallerException | IOException | DigestException
-                    | NoSuchAlgorithmException e) {
-                throw PrepareFailure.ofInternalError(
-                        "Failed to set up verity: " + e,
-                        PackageManagerException.INTERNAL_ERROR_VERITY_SETUP);
-            }
-        } else {
+        if (isApex) {
             // Use the path returned by apexd
             parsedPackage.setPath(request.getApexInfo().modulePath);
             parsedPackage.setBaseApkPath(request.getApexInfo().modulePath);
@@ -1882,10 +1897,16 @@
                     }
 
                     if (!oldSharedUid.equals(newSharedUid)) {
-                        throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED,
-                                "Package " + parsedPackage.getPackageName()
-                                        + " shared user changed from "
-                                        + oldSharedUid + " to " + newSharedUid);
+                        if (!(oldSharedUid.equals("<nothing>") && ps.getPkg() == null
+                                && ps.isArchivedOnAnyUser(allUsers))) {
+                            // Only allow changing sharedUserId if unarchiving
+                            // TODO(b/361558423): remove this check after pre-archiving installs
+                            // accept a sharedUserId param in the API
+                            throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED,
+                                    "Package " + parsedPackage.getPackageName()
+                                            + " shared user changed from "
+                                            + oldSharedUid + " to " + newSharedUid);
+                        }
                     }
 
                     // APK should not re-join shared UID
@@ -2086,7 +2107,21 @@
 
         // Reflect the rename in scanned details
         try {
-            parsedPackage.setPath(afterCodeFile.getCanonicalPath());
+            String afterCanonicalPath = afterCodeFile.getCanonicalPath();
+            String beforeCanonicalPath = beforeCodeFile.getCanonicalPath();
+            parsedPackage.setPath(afterCanonicalPath);
+
+            parsedPackage.setNativeLibraryDir(
+                    parsedPackage.getNativeLibraryDir()
+                            .replace(beforeCanonicalPath, afterCanonicalPath));
+            parsedPackage.setNativeLibraryRootDir(
+                    parsedPackage.getNativeLibraryRootDir()
+                            .replace(beforeCanonicalPath, afterCanonicalPath));
+            String secondaryNativeLibraryDir = parsedPackage.getSecondaryNativeLibraryDir();
+            if (secondaryNativeLibraryDir != null) {
+                parsedPackage.setSecondaryNativeLibraryDir(
+                        secondaryNativeLibraryDir.replace(beforeCanonicalPath, afterCanonicalPath));
+            }
         } catch (IOException e) {
             Slog.e(TAG, "Failed to get path: " + afterCodeFile, e);
             throw new PrepareFailure(PackageManager.INSTALL_FAILED_MEDIA_UNAVAILABLE,
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 9fb9e71..9428de7 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -925,6 +925,18 @@
         return PackageArchiver.isArchived(readUserState(userId));
     }
 
+    /**
+     * @return if the package is archived in any of the users
+     */
+    boolean isArchivedOnAnyUser(int[] userIds) {
+        for (int user : userIds) {
+            if (isArchived(user)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     int getInstallReason(int userId) {
         return readUserState(userId).getInstallReason();
     }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 1fcd7f1..ed9dcfa 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -535,7 +535,7 @@
     volatile boolean mRequestedOrSleepingDefaultDisplay;
 
     /**
-     * This is used to check whether to invoke {@link #updateScreenOffSleepToken} when screen is
+     * This is used to check whether to acquire screen-off sleep token when screen is
      * turned off. E.g. if it is false when screen is turned off and the display is swapping, it
      * is expected that the screen will be on in a short time. Then it is unnecessary to acquire
      * screen-off-sleep-token, so it can avoid intermediate visibility or lifecycle changes.
@@ -610,7 +610,6 @@
     private boolean mPendingKeyguardOccluded;
     private boolean mKeyguardOccludedChanged;
 
-    private ActivityTaskManagerInternal.SleepTokenAcquirer mScreenOffSleepTokenAcquirer;
     Intent mHomeIntent;
     Intent mCarDockIntent;
     Intent mDeskDockIntent;
@@ -2220,9 +2219,6 @@
         mLockPatternUtils = new LockPatternUtils(mContext);
         mLogger = new MetricsLogger();
 
-        mScreenOffSleepTokenAcquirer = mActivityTaskManagerInternal
-                .createSleepTokenAcquirer("ScreenOff");
-
         Resources res = mContext.getResources();
         mWakeOnDpadKeyPress =
                 res.getBoolean(com.android.internal.R.bool.config_wakeOnDpadKeyPress);
@@ -5521,13 +5517,6 @@
         mRequestedOrSleepingDefaultDisplay = true;
         mIsGoingToSleepDefaultDisplay = true;
 
-        // In case startedGoingToSleep is called after screenTurnedOff (the source caller is in
-        // order but the methods run on different threads) and updateScreenOffSleepToken was
-        // skipped. Then acquire sleep token if screen was off.
-        if (!mDefaultDisplayPolicy.isScreenOnFully() && !mDefaultDisplayPolicy.isScreenOnEarly()) {
-            updateScreenOffSleepToken(true /* acquire */);
-        }
-
         if (mKeyguardDelegate != null) {
             mKeyguardDelegate.onStartedGoingToSleep(pmSleepReason);
         }
@@ -5688,11 +5677,9 @@
         if (DEBUG_WAKEUP) Slog.i(TAG, "Display" + displayId + " turned off...");
 
         if (displayId == DEFAULT_DISPLAY) {
-            if (!isSwappingDisplay || mIsGoingToSleepDefaultDisplay) {
-                updateScreenOffSleepToken(true /* acquire */);
-            }
+            final boolean acquireSleepToken = !isSwappingDisplay || mIsGoingToSleepDefaultDisplay;
             mRequestedOrSleepingDefaultDisplay = false;
-            mDefaultDisplayPolicy.screenTurnedOff();
+            mDefaultDisplayPolicy.screenTurnedOff(acquireSleepToken);
             synchronized (mLock) {
                 if (mKeyguardDelegate != null) {
                     mKeyguardDelegate.onScreenTurnedOff();
@@ -5748,7 +5735,6 @@
         if (displayId == DEFAULT_DISPLAY) {
             Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "screenTurningOn",
                     0 /* cookie */);
-            updateScreenOffSleepToken(false /* acquire */);
             mDefaultDisplayPolicy.screenTurningOn(screenOnListener);
             mBootAnimationDismissable = false;
 
@@ -6255,15 +6241,6 @@
         }
     }
 
-    // TODO (multidisplay): Support multiple displays in WindowManagerPolicy.
-    private void updateScreenOffSleepToken(boolean acquire) {
-        if (acquire) {
-            mScreenOffSleepTokenAcquirer.acquire(DEFAULT_DISPLAY);
-        } else {
-            mScreenOffSleepTokenAcquirer.release(DEFAULT_DISPLAY);
-        }
-    }
-
     /** {@inheritDoc} */
     @Override
     public void enableScreenAfterBoot() {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index f53dda6..4dcc6e1 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -3169,7 +3169,8 @@
                 final WallpaperDestinationChangeHandler
                         liveSync = new WallpaperDestinationChangeHandler(
                         newWallpaper);
-                boolean same = changingToSame(name, newWallpaper);
+                boolean same = changingToSame(name, newWallpaper.connection,
+                        newWallpaper.wallpaperComponent);
 
                 /*
                  * If we have a shared system+lock wallpaper, and we reapply the same wallpaper
@@ -3257,14 +3258,15 @@
         return name == null || name.equals(mDefaultWallpaperComponent);
     }
 
-    private boolean changingToSame(ComponentName componentName, WallpaperData wallpaper) {
-        if (wallpaper.connection != null) {
-            final ComponentName wallpaperName = wallpaper.wallpaperComponent;
-            if (isDefaultComponent(componentName) && isDefaultComponent(wallpaperName)) {
+    private boolean changingToSame(ComponentName newComponentName,
+            WallpaperConnection currentConnection, ComponentName currentComponentName) {
+        if (currentConnection != null) {
+            if (isDefaultComponent(newComponentName) && isDefaultComponent(currentComponentName)) {
                 if (DEBUG) Slog.v(TAG, "changingToSame: still using default");
                 // Still using default wallpaper.
                 return true;
-            } else if (wallpaperName != null && wallpaperName.equals(componentName)) {
+            } else if (currentComponentName != null && currentComponentName.equals(
+                    newComponentName)) {
                 // Changing to same wallpaper.
                 if (DEBUG) Slog.v(TAG, "same wallpaper");
                 return true;
@@ -3279,7 +3281,8 @@
             Slog.v(TAG, "bindWallpaperComponentLocked: componentName=" + componentName);
         }
         // Has the component changed?
-        if (!force && changingToSame(componentName, wallpaper)) {
+        if (!force && changingToSame(componentName, wallpaper.connection,
+                wallpaper.wallpaperComponent)) {
             try {
                 if (DEBUG_LIVE) {
                     Slog.v(TAG, "Changing to the same component, ignoring");
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 99747e0..0be6471 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8149,7 +8149,8 @@
      */
     @Override
     protected int getOverrideOrientation() {
-        if (mWmService.mConstants.mIgnoreActivityOrientationRequest) {
+        if (mWmService.mConstants.mIgnoreActivityOrientationRequest
+                && info.applicationInfo.category != ApplicationInfo.CATEGORY_GAME) {
             return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
         }
         return mAppCompatController.getOrientationPolicy()
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
index b8ce02ed..3d6b64b 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
@@ -130,35 +130,6 @@
     }
 
     /**
-     * Sleep tokens cause the activity manager to put the top activity to sleep.
-     * They are used by components such as dreams that may hide and block interaction
-     * with underlying activities.
-     * The Acquirer provides an interface that encapsulates the underlying work, so the user does
-     * not need to handle the token by him/herself.
-     */
-    public interface SleepTokenAcquirer {
-
-        /**
-         * Acquires a sleep token.
-         * @param displayId The display to apply to.
-         */
-        void acquire(int displayId);
-
-        /**
-         * Releases the sleep token.
-         * @param displayId The display to apply to.
-         */
-        void release(int displayId);
-    }
-
-    /**
-     * Creates a sleep token acquirer for the specified display with the specified tag.
-     *
-     * @param tag A string identifying the purpose (eg. "Dream").
-     */
-    public abstract SleepTokenAcquirer createSleepTokenAcquirer(@NonNull String tag);
-
-    /**
      * Returns home activity for the specified user.
      *
      * @param userId ID of the user or {@link android.os.UserHandle#USER_ALL}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index e25d940d..49ca698 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -4356,6 +4356,7 @@
             mTaskOrganizerController.dump(pw, "  ");
             mVisibleActivityProcessTracker.dump(pw, "  ");
             mActiveUids.dump(pw, "  ");
+            pw.println("  SleepTokens=" + mRootWindowContainer.mSleepTokens);
             if (mDemoteTopAppReasons != 0) {
                 pw.println("  mDemoteTopAppReasons=" + mDemoteTopAppReasons);
             }
@@ -5071,17 +5072,16 @@
         EventLogTags.writeWmSetResumedActivity(r.mUserId, r.shortComponentName, reason);
     }
 
-    final class SleepTokenAcquirerImpl implements ActivityTaskManagerInternal.SleepTokenAcquirer {
+    final class SleepTokenAcquirer {
         private final String mTag;
         private final SparseArray<RootWindowContainer.SleepToken> mSleepTokens =
                 new SparseArray<>();
 
-        SleepTokenAcquirerImpl(@NonNull String tag) {
+        SleepTokenAcquirer(@NonNull String tag) {
             mTag = tag;
         }
 
-        @Override
-        public void acquire(int displayId) {
+        void acquire(int displayId) {
             synchronized (mGlobalLock) {
                 if (!mSleepTokens.contains(displayId)) {
                     mSleepTokens.append(displayId,
@@ -5091,8 +5091,7 @@
             }
         }
 
-        @Override
-        public void release(int displayId) {
+        void release(int displayId) {
             synchronized (mGlobalLock) {
                 final RootWindowContainer.SleepToken token = mSleepTokens.get(displayId);
                 if (token != null) {
@@ -5955,11 +5954,6 @@
     }
 
     final class LocalService extends ActivityTaskManagerInternal {
-        @Override
-        public SleepTokenAcquirer createSleepTokenAcquirer(@NonNull String tag) {
-            Objects.requireNonNull(tag);
-            return new SleepTokenAcquirerImpl(tag);
-        }
 
         @Override
         public ComponentName getHomeActivityForUser(int userId) {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index e8a3951..10e0641 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -735,8 +735,6 @@
 
     /** All tokens used to put activities on this root task to sleep (including mOffToken) */
     final ArrayList<RootWindowContainer.SleepToken> mAllSleepTokens = new ArrayList<>();
-    /** The token acquirer to put root tasks on the display to sleep */
-    private final ActivityTaskManagerInternal.SleepTokenAcquirer mOffTokenAcquirer;
 
     private boolean mSleeping;
 
@@ -1131,7 +1129,6 @@
         mDisplay = display;
         mDisplayId = display.getDisplayId();
         mCurrentUniqueDisplayId = display.getUniqueId();
-        mOffTokenAcquirer = mRootWindowContainer.mDisplayOffTokenAcquirer;
         mWallpaperController = new WallpaperController(mWmService, this);
         mWallpaperController.resetLargestDisplay(display);
         display.getDisplayInfo(mDisplayInfo);
@@ -6157,9 +6154,9 @@
         final int displayState = mDisplayInfo.state;
         if (displayId != DEFAULT_DISPLAY) {
             if (displayState == Display.STATE_OFF) {
-                mOffTokenAcquirer.acquire(mDisplayId);
+                mRootWindowContainer.mDisplayOffTokenAcquirer.acquire(mDisplayId);
             } else if (displayState == Display.STATE_ON) {
-                mOffTokenAcquirer.release(mDisplayId);
+                mRootWindowContainer.mDisplayOffTokenAcquirer.release(mDisplayId);
             }
             ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
                     "Content Recording: Display %d state was (%d), is now (%d), so update "
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 5c62120..107d31e 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -804,6 +804,14 @@
                     mAwake /* waiting */);
             if (!awake) {
                 onDisplaySwitchFinished();
+                // In case PhoneWindowManager's startedGoingToSleep is called after screenTurnedOff
+                // (the source caller is in order but the methods run on different threads) and
+                // updateScreenOffSleepToken was skipped by mIsGoingToSleepDefaultDisplay. Then
+                // acquire sleep token if screen is off.
+                if (!mScreenOnEarly && !mScreenOnFully && !mDisplayContent.isSleeping()) {
+                    Slog.w(TAG, "Late acquire sleep token for " + mDisplayContent);
+                    mService.mRoot.mDisplayOffTokenAcquirer.acquire(mDisplayContent.mDisplayId);
+                }
             }
         }
     }
@@ -851,6 +859,7 @@
     public void screenTurningOn(ScreenOnListener screenOnListener) {
         WindowProcessController visibleDozeUiProcess = null;
         synchronized (mLock) {
+            mService.mRoot.mDisplayOffTokenAcquirer.release(mDisplayContent.mDisplayId);
             mScreenOnEarly = true;
             mScreenOnFully = false;
             mKeyguardDrawComplete = false;
@@ -875,8 +884,12 @@
         onDisplaySwitchFinished();
     }
 
-    public void screenTurnedOff() {
+    /** It is called after {@link #screenTurningOn}. This runs on PowerManager's thread. */
+    public void screenTurnedOff(boolean acquireSleepToken) {
         synchronized (mLock) {
+            if (acquireSleepToken) {
+                mService.mRoot.mDisplayOffTokenAcquirer.acquire(mDisplayContent.mDisplayId);
+            }
             mScreenOnEarly = false;
             mScreenOnFully = false;
             mKeyguardDrawComplete = false;
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index 5d8a96c..0c489d6 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -87,7 +87,7 @@
     private final SparseArray<KeyguardDisplayState> mDisplayStates = new SparseArray<>();
     private final ActivityTaskManagerService mService;
     private RootWindowContainer mRootWindowContainer;
-    private final ActivityTaskManagerInternal.SleepTokenAcquirer mSleepTokenAcquirer;
+    private final ActivityTaskManagerService.SleepTokenAcquirer mSleepTokenAcquirer;
     private boolean mWaitingForWakeTransition;
     private Transition.ReadyCondition mWaitAodHide = null;
 
@@ -95,7 +95,7 @@
             ActivityTaskSupervisor taskSupervisor) {
         mService = service;
         mTaskSupervisor = taskSupervisor;
-        mSleepTokenAcquirer = mService.new SleepTokenAcquirerImpl(KEYGUARD_SLEEP_TOKEN_TAG);
+        mSleepTokenAcquirer = mService.new SleepTokenAcquirer(KEYGUARD_SLEEP_TOKEN_TAG);
     }
 
     void setWindowManager(WindowManagerService windowManager) {
@@ -658,10 +658,10 @@
 
         private boolean mRequestDismissKeyguard;
         private final ActivityTaskManagerService mService;
-        private final ActivityTaskManagerInternal.SleepTokenAcquirer mSleepTokenAcquirer;
+        private final ActivityTaskManagerService.SleepTokenAcquirer mSleepTokenAcquirer;
 
         KeyguardDisplayState(ActivityTaskManagerService service, int displayId,
-                ActivityTaskManagerInternal.SleepTokenAcquirer acquirer) {
+                ActivityTaskManagerService.SleepTokenAcquirer acquirer) {
             mService = service;
             mDisplayId = displayId;
             mSleepTokenAcquirer = acquirer;
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 866dcd5..8f5612c 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -215,7 +215,7 @@
     private static final String DISPLAY_OFF_SLEEP_TOKEN_TAG = "Display-off";
 
     /** The token acquirer to put root tasks on the displays to sleep */
-    final ActivityTaskManagerInternal.SleepTokenAcquirer mDisplayOffTokenAcquirer;
+    final ActivityTaskManagerService.SleepTokenAcquirer mDisplayOffTokenAcquirer;
 
     /**
      * The modes which affect which tasks are returned when calling
@@ -450,7 +450,7 @@
         mService = service.mAtmService;
         mTaskSupervisor = mService.mTaskSupervisor;
         mTaskSupervisor.mRootWindowContainer = this;
-        mDisplayOffTokenAcquirer = mService.new SleepTokenAcquirerImpl(DISPLAY_OFF_SLEEP_TOKEN_TAG);
+        mDisplayOffTokenAcquirer = mService.new SleepTokenAcquirer(DISPLAY_OFF_SLEEP_TOKEN_TAG);
         mDeviceStateController = new DeviceStateController(service.mContext, service.mGlobalLock);
         mDisplayRotationCoordinator = new DisplayRotationCoordinator();
     }
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 5aa34d2..92953e5 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -17,6 +17,8 @@
 package com.android.server.wm;
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENTS_INFO;
+import static android.window.TaskFragmentOrganizer.KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO;
 import static android.window.TaskFragmentOrganizer.putErrorInfoInBundle;
 import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
 import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
@@ -206,7 +208,13 @@
             mOrganizerPid = pid;
             mAppThread = getAppThread(pid, mOrganizerUid);
             for (int i = mOrganizedTaskFragments.size() - 1; i >= 0; i--) {
-                mOrganizedTaskFragments.get(i).onTaskFragmentOrganizerRestarted(organizer);
+                final TaskFragment taskFragment = mOrganizedTaskFragments.get(i);
+                if (taskFragment.isAttached()
+                        && taskFragment.getTopNonFinishingActivity() != null) {
+                    taskFragment.onTaskFragmentOrganizerRestarted(organizer);
+                } else {
+                    mOrganizedTaskFragments.remove(taskFragment);
+                }
             }
             try {
                 mOrganizer.asBinder().linkToDeath(this, 0 /*flags*/);
@@ -575,8 +583,29 @@
         }
 
         mCachedTaskFragmentOrganizerStates.remove(cachedState);
-        outSavedState.putAll(cachedState.mSavedState);
         cachedState.restore(organizer, pid);
+        outSavedState.putAll(cachedState.mSavedState);
+
+        // Collect the organized TfInfo and TfParentInfo in the system.
+        final ArrayList<TaskFragmentInfo> infos = new ArrayList<>();
+        final ArrayMap<Integer, Task> tasks = new ArrayMap<>();
+        final int fragmentCount = cachedState.mOrganizedTaskFragments.size();
+        for (int j = 0; j < fragmentCount; j++) {
+            final TaskFragment tf = cachedState.mOrganizedTaskFragments.get(j);
+            infos.add(tf.getTaskFragmentInfo());
+            if (!tasks.containsKey(tf.getTask().mTaskId)) {
+                tasks.put(tf.getTask().mTaskId, tf.getTask());
+            }
+        }
+        outSavedState.putParcelableArrayList(KEY_RESTORE_TASK_FRAGMENTS_INFO, infos);
+
+        final ArrayList<TaskFragmentParentInfo> parentInfos = new ArrayList<>();
+        for (int j = tasks.size() - 1; j >= 0; j--) {
+            parentInfos.add(tasks.valueAt(j).getTaskFragmentParentInfo());
+        }
+        outSavedState.putParcelableArrayList(KEY_RESTORE_TASK_FRAGMENT_PARENT_INFO,
+                parentInfos);
+
         mTaskFragmentOrganizerState.put(organizer.asBinder(), cachedState);
         mPendingTaskFragmentEvents.put(organizer.asBinder(), new ArrayList<>());
         return true;
diff --git a/services/core/java/com/android/server/wm/WindowManagerConstants.java b/services/core/java/com/android/server/wm/WindowManagerConstants.java
index 47c42f4..e0f24d8 100644
--- a/services/core/java/com/android/server/wm/WindowManagerConstants.java
+++ b/services/core/java/com/android/server/wm/WindowManagerConstants.java
@@ -34,7 +34,7 @@
  */
 final class WindowManagerConstants {
 
-    /** The orientation of activity will be always "unspecified". */
+    /** The orientation of activity will be always "unspecified" except for game apps. */
     private static final String KEY_IGNORE_ACTIVITY_ORIENTATION_REQUEST =
             "ignore_activity_orientation_request";
 
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt
index 5233f19..a0f1a55 100644
--- a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureAppSearchSessionTest.kt
@@ -16,6 +16,7 @@
 package com.android.server.appfunctions
 
 import android.app.appfunctions.AppFunctionRuntimeMetadata
+import android.app.appfunctions.AppFunctionRuntimeMetadata.APP_FUNCTION_RUNTIME_NAMESPACE
 import android.app.appfunctions.AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema
 import android.app.appfunctions.AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema
 import android.app.appsearch.AppSearchManager
@@ -42,7 +43,7 @@
     fun clearData() {
         val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build()
         FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
-            val setSchemaRequest = SetSchemaRequest.Builder().build()
+            val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build()
             it.setSchema(setSchemaRequest)
         }
     }
@@ -123,6 +124,38 @@
         }
     }
 
+    @Test
+    fun getByDocumentId() {
+        val searchContext = AppSearchManager.SearchContext.Builder(TEST_DB).build()
+        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session ->
+            val setSchemaRequest =
+                SetSchemaRequest.Builder()
+                    .addSchemas(
+                        createParentAppFunctionRuntimeSchema(),
+                        createAppFunctionRuntimeSchema(TEST_PACKAGE_NAME)
+                    )
+                    .build()
+            val schema = session.setSchema(setSchemaRequest)
+            val appFunctionRuntimeMetadata =
+                AppFunctionRuntimeMetadata.Builder(TEST_PACKAGE_NAME, TEST_FUNCTION_ID, "").build()
+            val putDocumentsRequest: PutDocumentsRequest =
+                PutDocumentsRequest.Builder()
+                    .addGenericDocuments(appFunctionRuntimeMetadata)
+                    .build()
+            val putResult = session.put(putDocumentsRequest)
+
+            val genricDocument = session
+                .getByDocumentId(
+                    /* documentId= */ "${TEST_PACKAGE_NAME}/${TEST_FUNCTION_ID}",
+                    APP_FUNCTION_RUNTIME_NAMESPACE
+                )
+                .get()
+
+            val foundAppFunctionRuntimeMetadata = AppFunctionRuntimeMetadata(genricDocument)
+            assertThat(foundAppFunctionRuntimeMetadata.functionId).isEqualTo(TEST_FUNCTION_ID)
+        }
+    }
+
     private companion object {
         const val TEST_DB: String = "test_db"
         const val TEST_PACKAGE_NAME: String = "test_pkg"
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt
new file mode 100644
index 0000000..1fa55c7
--- /dev/null
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/FutureGlobalSearchSessionTest.kt
@@ -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.appfunctions
+
+import android.app.appfunctions.AppFunctionRuntimeMetadata
+import android.app.appfunctions.AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema
+import android.app.appfunctions.AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema
+import android.app.appsearch.AppSearchManager
+import android.app.appsearch.AppSearchManager.SearchContext
+import android.app.appsearch.PutDocumentsRequest
+import android.app.appsearch.SetSchemaRequest
+import android.app.appsearch.observer.DocumentChangeInfo
+import android.app.appsearch.observer.ObserverCallback
+import android.app.appsearch.observer.ObserverSpec
+import android.app.appsearch.observer.SchemaChangeInfo
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.internal.infra.AndroidFuture
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class FutureGlobalSearchSessionTest {
+    private val context = InstrumentationRegistry.getInstrumentation().targetContext
+    private val appSearchManager = context.getSystemService(AppSearchManager::class.java)
+    private val testExecutor = MoreExecutors.directExecutor()
+
+    @Before
+    @After
+    fun clearData() {
+        val searchContext = SearchContext.Builder(TEST_DB).build()
+        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+            val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build()
+            it.setSchema(setSchemaRequest)
+        }
+    }
+
+    @Test
+    fun registerDocumentChangeObserverCallback() {
+        val packageObserverSpec: ObserverSpec =
+            ObserverSpec.Builder()
+                .addFilterSchemas(
+                    AppFunctionRuntimeMetadata.getRuntimeSchemaNameForPackage(TEST_TARGET_PKG_NAME)
+                )
+                .build()
+        val settableDocumentChangeInfo: AndroidFuture<DocumentChangeInfo> = AndroidFuture()
+        val observer: ObserverCallback =
+            object : ObserverCallback {
+                override fun onSchemaChanged(changeInfo: SchemaChangeInfo) {}
+
+                override fun onDocumentChanged(changeInfo: DocumentChangeInfo) {
+                    settableDocumentChangeInfo.complete(changeInfo)
+                }
+            }
+        val futureGlobalSearchSession = FutureGlobalSearchSession(appSearchManager, testExecutor)
+
+        val registerPackageObserver: Void? =
+            futureGlobalSearchSession
+                .registerObserverCallbackAsync(
+                    TEST_TARGET_PKG_NAME,
+                    packageObserverSpec,
+                    testExecutor,
+                    observer,
+                )
+                .get()
+        assertThat(registerPackageObserver).isNull()
+        // Trigger document change
+        val searchContext = SearchContext.Builder(TEST_DB).build()
+        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use { session ->
+            val setSchemaRequest =
+                SetSchemaRequest.Builder()
+                    .addSchemas(
+                        createParentAppFunctionRuntimeSchema(),
+                        createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME),
+                    )
+                    .build()
+            val schema = session.setSchema(setSchemaRequest)
+            assertThat(schema.get()).isNotNull()
+            val appFunctionRuntimeMetadata =
+                AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, TEST_FUNCTION_ID, "")
+                    .build()
+            val putDocumentsRequest: PutDocumentsRequest =
+                PutDocumentsRequest.Builder()
+                    .addGenericDocuments(appFunctionRuntimeMetadata)
+                    .build()
+            val putResult = session.put(putDocumentsRequest).get()
+            assertThat(putResult.isSuccess).isTrue()
+        }
+        assertThat(
+                settableDocumentChangeInfo
+                    .get()
+                    .changedDocumentIds
+                    .contains(
+                        AppFunctionRuntimeMetadata.getDocumentIdForAppFunction(
+                            TEST_TARGET_PKG_NAME,
+                            TEST_FUNCTION_ID,
+                        )
+                    )
+            )
+            .isTrue()
+    }
+
+    private companion object {
+        const val TEST_DB: String = "test_db"
+        const val TEST_TARGET_PKG_NAME = "com.android.frameworks.appfunctionstests"
+        const val TEST_FUNCTION_ID: String = "print"
+    }
+}
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
new file mode 100644
index 0000000..1061da2
--- /dev/null
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
@@ -0,0 +1,296 @@
+/*
+ * 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.appfunctions
+
+import android.app.appfunctions.AppFunctionRuntimeMetadata
+import android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID
+import android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME
+import android.app.appsearch.AppSearchManager
+import android.app.appsearch.AppSearchManager.SearchContext
+import android.app.appsearch.PutDocumentsRequest
+import android.app.appsearch.SearchSpec
+import android.app.appsearch.SetSchemaRequest
+import android.util.ArrayMap
+import android.util.ArraySet
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class MetadataSyncAdapterTest {
+    private val context = InstrumentationRegistry.getInstrumentation().targetContext
+    private val appSearchManager = context.getSystemService(AppSearchManager::class.java)
+    private val testExecutor = MoreExecutors.directExecutor()
+
+    @Before
+    @After
+    fun clearData() {
+        val searchContext = SearchContext.Builder(TEST_DB).build()
+        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+            val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build()
+            it.setSchema(setSchemaRequest)
+        }
+    }
+
+    @Test
+    fun getPackageToFunctionIdMap() {
+        val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build()
+        val functionRuntimeMetadata =
+            AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId", "").build()
+        val setSchemaRequest =
+            SetSchemaRequest.Builder()
+                .addSchemas(AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema())
+                .addSchemas(
+                    AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
+                )
+                .build()
+        val putDocumentsRequest: PutDocumentsRequest =
+            PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build()
+        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+            val setSchemaResponse = it.setSchema(setSchemaRequest).get()
+            assertThat(setSchemaResponse).isNotNull()
+            val appSearchBatchResult = it.put(putDocumentsRequest).get()
+            assertThat(appSearchBatchResult.isSuccess).isTrue()
+        }
+
+        val metadataSyncAdapter =
+            MetadataSyncAdapter(
+                testExecutor,
+                FutureAppSearchSession(appSearchManager, testExecutor, searchContext),
+            )
+        val searchSpec: SearchSpec =
+            SearchSpec.Builder()
+                .addFilterSchemas(
+                    AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE,
+                    AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
+                        .schemaType,
+                )
+                .build()
+        val packageToFunctionIdMap =
+            metadataSyncAdapter.getPackageToFunctionIdMap(
+                "",
+                searchSpec,
+                PROPERTY_FUNCTION_ID,
+                PROPERTY_PACKAGE_NAME,
+            )
+
+        assertThat(packageToFunctionIdMap).isNotNull()
+        assertThat(packageToFunctionIdMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunctionId")
+    }
+
+    @Test
+    fun getPackageToFunctionIdMap_multipleDocuments() {
+        val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build()
+        val functionRuntimeMetadata =
+            AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId", "").build()
+        val functionRuntimeMetadata1 =
+            AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId1", "").build()
+        val functionRuntimeMetadata2 =
+            AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId2", "").build()
+        val functionRuntimeMetadata3 =
+            AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId3", "").build()
+        val setSchemaRequest =
+            SetSchemaRequest.Builder()
+                .addSchemas(AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema())
+                .addSchemas(
+                    AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
+                )
+                .build()
+        val putDocumentsRequest: PutDocumentsRequest =
+            PutDocumentsRequest.Builder()
+                .addGenericDocuments(
+                    functionRuntimeMetadata,
+                    functionRuntimeMetadata1,
+                    functionRuntimeMetadata2,
+                    functionRuntimeMetadata3,
+                )
+                .build()
+        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+            val setSchemaResponse = it.setSchema(setSchemaRequest).get()
+            assertThat(setSchemaResponse).isNotNull()
+            val appSearchBatchResult = it.put(putDocumentsRequest).get()
+            assertThat(appSearchBatchResult.isSuccess).isTrue()
+        }
+
+        val metadataSyncAdapter =
+            MetadataSyncAdapter(
+                testExecutor,
+                FutureAppSearchSession(appSearchManager, testExecutor, searchContext),
+            )
+        val searchSpec: SearchSpec =
+            SearchSpec.Builder()
+                .setResultCountPerPage(1)
+                .addFilterSchemas(
+                    AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE,
+                    AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
+                        .schemaType,
+                )
+                .build()
+        val packageToFunctionIdMap =
+            metadataSyncAdapter.getPackageToFunctionIdMap(
+                "",
+                searchSpec,
+                PROPERTY_FUNCTION_ID,
+                PROPERTY_PACKAGE_NAME,
+            )
+
+        assertThat(packageToFunctionIdMap).isNotNull()
+        assertThat(packageToFunctionIdMap[TEST_TARGET_PKG_NAME])
+            .containsExactly(
+                "testFunctionId",
+                "testFunctionId1",
+                "testFunctionId2",
+                "testFunctionId3",
+            )
+    }
+
+    @Test
+    fun getAddedFunctionsDiffMap_noDiff() {
+        val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        staticPackageToFunctionMap.putAll(
+            mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+        )
+        val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> =
+            ArrayMap(staticPackageToFunctionMap)
+
+        val addedFunctionsDiffMap =
+            MetadataSyncAdapter.getAddedFunctionsDiffMap(
+                staticPackageToFunctionMap,
+                runtimePackageToFunctionMap,
+            )
+
+        assertThat(addedFunctionsDiffMap.isEmpty()).isEqualTo(true)
+    }
+
+    @Test
+    fun getAddedFunctionsDiffMap_addedFunction() {
+        val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        staticPackageToFunctionMap.putAll(
+            mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1", "testFunction2")))
+        )
+        val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        runtimePackageToFunctionMap.putAll(
+            mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+        )
+
+        val addedFunctionsDiffMap =
+            MetadataSyncAdapter.getAddedFunctionsDiffMap(
+                staticPackageToFunctionMap,
+                runtimePackageToFunctionMap,
+            )
+
+        assertThat(addedFunctionsDiffMap.size).isEqualTo(1)
+        assertThat(addedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction2")
+    }
+
+    @Test
+    fun getAddedFunctionsDiffMap_addedFunctionNewPackage() {
+        val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        staticPackageToFunctionMap.putAll(
+            mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+        )
+        val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+
+        val addedFunctionsDiffMap =
+            MetadataSyncAdapter.getAddedFunctionsDiffMap(
+                staticPackageToFunctionMap,
+                runtimePackageToFunctionMap,
+            )
+
+        assertThat(addedFunctionsDiffMap.size).isEqualTo(1)
+        assertThat(addedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction1")
+    }
+
+    @Test
+    fun getAddedFunctionsDiffMap_removedFunction() {
+        val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        runtimePackageToFunctionMap.putAll(
+            mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+        )
+
+        val addedFunctionsDiffMap =
+            MetadataSyncAdapter.getAddedFunctionsDiffMap(
+                staticPackageToFunctionMap,
+                runtimePackageToFunctionMap,
+            )
+
+        assertThat(addedFunctionsDiffMap.isEmpty()).isEqualTo(true)
+    }
+
+    @Test
+    fun getRemovedFunctionsDiffMap_noDiff() {
+        val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        staticPackageToFunctionMap.putAll(
+            mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+        )
+        val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> =
+            ArrayMap(staticPackageToFunctionMap)
+
+        val removedFunctionsDiffMap =
+            MetadataSyncAdapter.getRemovedFunctionsDiffMap(
+                staticPackageToFunctionMap,
+                runtimePackageToFunctionMap,
+            )
+
+        assertThat(removedFunctionsDiffMap.isEmpty()).isEqualTo(true)
+    }
+
+    @Test
+    fun getRemovedFunctionsDiffMap_removedFunction() {
+        val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        runtimePackageToFunctionMap.putAll(
+            mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+        )
+
+        val removedFunctionsDiffMap =
+            MetadataSyncAdapter.getRemovedFunctionsDiffMap(
+                staticPackageToFunctionMap,
+                runtimePackageToFunctionMap,
+            )
+
+        assertThat(removedFunctionsDiffMap.size).isEqualTo(1)
+        assertThat(removedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction1")
+    }
+
+    @Test
+    fun getRemovedFunctionsDiffMap_addedFunction() {
+        val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        staticPackageToFunctionMap.putAll(
+            mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+        )
+        val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+
+        val removedFunctionsDiffMap =
+            MetadataSyncAdapter.getRemovedFunctionsDiffMap(
+                staticPackageToFunctionMap,
+                runtimePackageToFunctionMap,
+            )
+
+        assertThat(removedFunctionsDiffMap.isEmpty()).isEqualTo(true)
+    }
+
+    private companion object {
+        const val TEST_DB: String = "test_db"
+        const val TEST_TARGET_PKG_NAME = "com.android.frameworks.appfunctionstests"
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java
new file mode 100644
index 0000000..c8e4f89
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobParametersTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.job;
+
+import static android.app.job.Flags.FLAG_CLEANUP_EMPTY_JOBS;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+
+import android.app.job.IJobCallback;
+import android.app.job.JobParameters;
+import android.net.Uri;
+import android.os.Parcel;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+public class JobParametersTest {
+    private static final String TAG = JobParametersTest.class.getSimpleName();
+    private static final int TEST_JOB_ID_1 = 123;
+    private static final String TEST_NAMESPACE = "TEST_NAMESPACE";
+    private static final String TEST_DEBUG_STOP_REASON = "TEST_DEBUG_STOP_REASON";
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    private MockitoSession mMockingSession;
+    @Mock private Parcel mMockParcel;
+    @Mock private IJobCallback.Stub mMockJobCallbackStub;
+
+    @Before
+    public void setUp() throws Exception {
+        mMockingSession =
+                mockitoSession().initMocks(this).strictness(Strictness.LENIENT).startMocking();
+    }
+
+    @After
+    public void tearDown() {
+        if (mMockingSession != null) {
+            mMockingSession.finishMocking();
+        }
+
+        when(mMockParcel.readInt())
+                .thenReturn(TEST_JOB_ID_1) // Job ID
+                .thenReturn(0) // No clip data
+                .thenReturn(0) // No deadline expired
+                .thenReturn(0) // No network
+                .thenReturn(0) // No stop reason
+                .thenReturn(0); // Internal stop reason
+        when(mMockParcel.readString())
+                .thenReturn(TEST_NAMESPACE) // Job namespace
+                .thenReturn(TEST_DEBUG_STOP_REASON); // Debug stop reason
+        when(mMockParcel.readPersistableBundle()).thenReturn(null);
+        when(mMockParcel.readBundle()).thenReturn(null);
+        when(mMockParcel.readStrongBinder()).thenReturn(mMockJobCallbackStub);
+        when(mMockParcel.readBoolean())
+                .thenReturn(false) // expedited
+                .thenReturn(false); // user initiated
+        when(mMockParcel.createTypedArray(any())).thenReturn(new Uri[0]);
+        when(mMockParcel.createStringArray()).thenReturn(new String[0]);
+    }
+
+    /**
+     * Test to verify that the JobParameters created using Non-Parcelable constructor has not
+     * cleaner attached
+     */
+    @Test
+    public void testJobParametersNonParcelableConstructor_noCleaner() {
+        JobParameters jobParameters =
+                new JobParameters(
+                        null,
+                        TEST_NAMESPACE,
+                        TEST_JOB_ID_1,
+                        null,
+                        null,
+                        null,
+                        0,
+                        false,
+                        false,
+                        false,
+                        null,
+                        null,
+                        null);
+
+        // Verify that cleaner is not registered
+        assertThat(jobParameters.getCleanable()).isNull();
+        assertThat(jobParameters.getJobCleanupCallback()).isNull();
+    }
+
+    /**
+     * Test to verify that the JobParameters created using Parcelable constructor has not cleaner
+     * attached
+     */
+    @Test
+    public void testJobParametersParcelableConstructor_noCleaner() {
+        JobParameters jobParameters = JobParameters.CREATOR.createFromParcel(mMockParcel);
+
+        // Verify that cleaner is not registered
+        assertThat(jobParameters.getCleanable()).isNull();
+        assertThat(jobParameters.getJobCleanupCallback()).isNull();
+    }
+
+    /** Test to verify that the JobParameters Cleaner is disabled */
+    @RequiresFlagsEnabled(FLAG_CLEANUP_EMPTY_JOBS)
+    @Test
+    public void testCleanerWithLeakedJobCleanerDisabled_flagCleanupEmptyJobsEnabled() {
+        // Inject real JobCallbackCleanup
+        JobParameters jobParameters = JobParameters.CREATOR.createFromParcel(mMockParcel);
+
+        // Enable the cleaner
+        jobParameters.enableCleaner();
+
+        // Verify the cleaner is enabled
+        assertThat(jobParameters.getCleanable()).isNotNull();
+        assertThat(jobParameters.getJobCleanupCallback()).isNotNull();
+        assertThat(jobParameters.getJobCleanupCallback().isCleanerEnabled()).isTrue();
+
+        // Disable the cleaner
+        jobParameters.disableCleaner();
+
+        // Verify the cleaner is disabled
+        assertThat(jobParameters.getCleanable()).isNull();
+        assertThat(jobParameters.getJobCleanupCallback()).isNull();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index a5f1fcd..2d95740 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -30,6 +30,7 @@
 import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON;
 import static com.android.server.hdmi.RequestActiveSourceAction.TIMEOUT_WAIT_FOR_LAUNCHERX_API_CALL_MS;
 import static com.android.server.hdmi.RoutingControlAction.TIMEOUT_ROUTING_INFORMATION_MS;
+import static com.android.server.hdmi.RequestSadAction.RETRY_COUNTER_MAX;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -273,13 +274,12 @@
         assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated);
 
         // Finish querying SADs
-        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
-        mNativeWrapper.clearResultMessages();
-        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
-        mTestLooper.dispatchAll();
-        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
-        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
-        mTestLooper.dispatchAll();
+        for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) {
+            assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+            mNativeWrapper.clearResultMessages();
+            mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+            mTestLooper.dispatchAll();
+        }
 
         assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
         mNativeWrapper.clearResultMessages();
@@ -685,18 +685,46 @@
         assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated);
 
         // Finish querying SADs
-        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
-        mNativeWrapper.clearResultMessages();
-        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
-        mTestLooper.dispatchAll();
-        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
-        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
-        mTestLooper.dispatchAll();
+        for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) {
+            assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+            mNativeWrapper.clearResultMessages();
+            mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+            mTestLooper.dispatchAll();
+        }
 
         assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
     }
 
     @Test
+    public void handleInitiateArc_arcAlreadyEstablished_noRequestSad() {
+        // Emulate Audio device on port 0x2000 (supports ARC)
+        mNativeWrapper.setPortConnectionStatus(2, true);
+        HdmiCecMessage reportPhysicalAddress =
+                HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
+                        ADDR_AUDIO_SYSTEM, 0x2000, HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+        mNativeWrapper.onCecMessage(reportPhysicalAddress);
+        mTestLooper.dispatchAll();
+
+        assertThat(mHdmiCecLocalDeviceTv.isArcEstablished()).isFalse();
+
+        HdmiCecMessage requestArcInitiation = HdmiCecMessageBuilder.buildInitiateArc(
+                ADDR_AUDIO_SYSTEM,
+                ADDR_TV);
+        mNativeWrapper.onCecMessage(requestArcInitiation);
+        mTestLooper.dispatchAll();
+
+        // Finish querying SADs
+        for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) {
+            assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+            mNativeWrapper.clearResultMessages();
+            mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+            mTestLooper.dispatchAll();
+        }
+
+        assertThat(mHdmiCecLocalDeviceTv.isArcEstablished()).isTrue();
+    }
+
+    @Test
     public void handleTerminateArc_noAudioDevice() {
         HdmiCecMessage terminateArc = HdmiCecMessageBuilder.buildTerminateArc(
                 ADDR_AUDIO_SYSTEM,
@@ -970,13 +998,12 @@
         // <Report ARC Initiated> should only be sent after SAD querying is done
         assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated);
         // Finish querying SADs
-        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
-        mNativeWrapper.clearResultMessages();
-        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
-        mTestLooper.dispatchAll();
-        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
-        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
-        mTestLooper.dispatchAll();
+        for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) {
+            assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+            mNativeWrapper.clearResultMessages();
+            mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+            mTestLooper.dispatchAll();
+        }
 
         assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
         mNativeWrapper.clearResultMessages();
@@ -1171,13 +1198,12 @@
         mTestLooper.dispatchAll();
 
         // Finish querying SADs
-        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
-        mNativeWrapper.clearResultMessages();
-        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
-        mTestLooper.dispatchAll();
-        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
-        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
-        mTestLooper.dispatchAll();
+        for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) {
+            assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+            mNativeWrapper.clearResultMessages();
+            mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+            mTestLooper.dispatchAll();
+        }
 
         // ARC should be established after RequestSadAction is finished
         assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
@@ -1327,13 +1353,12 @@
         assertThat(mNativeWrapper.getResultMessages()).doesNotContain(reportArcInitiated);
 
         // Finish querying SADs
-        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
-        mNativeWrapper.clearResultMessages();
-        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
-        mTestLooper.dispatchAll();
-        assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
-        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
-        mTestLooper.dispatchAll();
+        for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) {
+            assertThat(mNativeWrapper.getResultMessages()).contains(SAD_QUERY);
+            mNativeWrapper.clearResultMessages();
+            mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+            mTestLooper.dispatchAll();
+        }
 
         assertThat(mNativeWrapper.getResultMessages()).contains(reportArcInitiated);
     }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
index f8e465c..4cf2937 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
@@ -18,6 +18,7 @@
 
 import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
+import static com.android.server.hdmi.RequestSadAction.RETRY_COUNTER_MAX;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -144,7 +145,7 @@
     }
 
     @Test
-    public void noResponse_queryAgainOnce_emptyResult() {
+    public void noResponse_queryAgain_emptyResult() {
         RequestSadAction action = new RequestSadAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM,
                 mCallback);
         action.start();
@@ -154,13 +155,13 @@
         HdmiCecMessage expected1 = HdmiCecMessageBuilder.buildRequestShortAudioDescriptor(
                 mTvLogicalAddress, Constants.ADDR_AUDIO_SYSTEM,
                 CODECS_TO_QUERY_1.stream().mapToInt(i -> i).toArray());
-        assertThat(mNativeWrapper.getResultMessages()).contains(expected1);
-        mNativeWrapper.clearResultMessages();
-        mTestLooper.moveTimeForward(TIMEOUT_MS);
-        mTestLooper.dispatchAll();
-        assertThat(mNativeWrapper.getResultMessages()).contains(expected1);
-        mTestLooper.moveTimeForward(TIMEOUT_MS);
-        mTestLooper.dispatchAll();
+
+        for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) {
+            assertThat(mNativeWrapper.getResultMessages()).contains(expected1);
+            mNativeWrapper.clearResultMessages();
+            mTestLooper.moveTimeForward(TIMEOUT_MS);
+            mTestLooper.dispatchAll();
+        }
 
         assertThat(mSupportedSads).isNotNull();
         assertThat(mSupportedSads.size()).isEqualTo(0);
@@ -507,7 +508,7 @@
     }
 
     @Test
-    public void invalidMessageLength_queryAgainOnce() {
+    public void invalidMessageLength_queryAgain() {
         RequestSadAction action = new RequestSadAction(mHdmiCecLocalDeviceTv, ADDR_AUDIO_SYSTEM,
                 mCallback);
         action.start();
@@ -524,16 +525,13 @@
                 0x27, 0x20, 0x0A};
         HdmiCecMessage response1 = HdmiCecMessageBuilder.buildReportShortAudioDescriptor(
                 Constants.ADDR_AUDIO_SYSTEM, mTvLogicalAddress, sadsToRespond_1);
-        assertThat(mNativeWrapper.getResultMessages()).contains(expected1);
-        mNativeWrapper.clearResultMessages();
-        action.processCommand(response1);
-        mTestLooper.dispatchAll();
-        mTestLooper.moveTimeForward(TIMEOUT_MS);
-        mTestLooper.dispatchAll();
-        assertThat(mNativeWrapper.getResultMessages()).contains(expected1);
-        mNativeWrapper.clearResultMessages();
-        mTestLooper.moveTimeForward(TIMEOUT_MS);
-        mTestLooper.dispatchAll();
+
+        for (int i = 0; i <= RETRY_COUNTER_MAX; ++i) {
+            assertThat(mNativeWrapper.getResultMessages()).contains(expected1);
+            mNativeWrapper.clearResultMessages();
+            mTestLooper.moveTimeForward(TIMEOUT_MS);
+            mTestLooper.dispatchAll();
+        }
 
         assertThat(mSupportedSads).isNotNull();
         assertThat(mSupportedSads.size()).isEqualTo(0);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
index 4a19973..1890879 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java
@@ -39,6 +39,7 @@
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
+import android.app.KeyguardManager;
 import android.app.UiModeManager;
 import android.app.WallpaperManager;
 import android.content.BroadcastReceiver;
@@ -78,6 +79,7 @@
     private DefaultDeviceEffectsApplier mApplier;
     @Mock PowerManager mPowerManager;
     @Mock ColorDisplayManager mColorDisplayManager;
+    @Mock KeyguardManager mKeyguardManager;
     @Mock UiModeManager mUiModeManager;
     @Mock WallpaperManager mWallpaperManager;
 
@@ -87,6 +89,7 @@
         mContext = spy(new TestableContext(InstrumentationRegistry.getContext(), null));
         mContext.addMockSystemService(PowerManager.class, mPowerManager);
         mContext.addMockSystemService(ColorDisplayManager.class, mColorDisplayManager);
+        mContext.addMockSystemService(KeyguardManager.class, mKeyguardManager);
         mContext.addMockSystemService(UiModeManager.class, mUiModeManager);
         mContext.addMockSystemService(WallpaperManager.class, mWallpaperManager);
         when(mWallpaperManager.isWallpaperSupported()).thenReturn(true);
@@ -311,6 +314,22 @@
     }
 
     @Test
+    @EnableFlags({android.app.Flags.FLAG_MODES_API, android.app.Flags.FLAG_MODES_UI})
+    public void apply_nightModeWithScreenOnAndKeyguardShowing_appliedImmediately(
+            @TestParameter ZenChangeOrigin origin) {
+
+        when(mPowerManager.isInteractive()).thenReturn(true);
+        when(mKeyguardManager.isKeyguardLocked()).thenReturn(true);
+
+        mApplier.apply(new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(),
+                origin.value());
+
+        // Effect was applied, and no broadcast receiver was registered.
+        verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_NIGHT));
+        verify(mContext, never()).registerReceiver(any(), any(), anyInt());
+    }
+
+    @Test
     @TestParameters({"{origin: ORIGIN_USER_IN_SYSTEMUI}", "{origin: ORIGIN_USER_IN_APP}",
             "{origin: ORIGIN_INIT}", "{origin: ORIGIN_INIT_USER}"})
     public void apply_nightModeWithScreenOn_appliedImmediatelyBasedOnOrigin(
diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
index e694c0b..536dcfb 100644
--- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java
@@ -42,7 +42,6 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.clearInvocations;
 
 import android.app.ActivityManager;
 import android.app.AppOpsManager;
@@ -135,15 +134,13 @@
         doNothing().when(mPhoneWindowManager).initializeHdmiState();
         final boolean[] isScreenTurnedOff = { false };
         final DisplayPolicy displayPolicy = mock(DisplayPolicy.class);
-        doAnswer(invocation -> isScreenTurnedOff[0] = true).when(displayPolicy).screenTurnedOff();
+        doAnswer(invocation -> isScreenTurnedOff[0] = true).when(displayPolicy).screenTurnedOff(
+                anyBoolean());
         doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnEarly();
         doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnFully();
 
         mPhoneWindowManager.mDefaultDisplayPolicy = displayPolicy;
         mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class);
-        final ActivityTaskManagerInternal.SleepTokenAcquirer tokenAcquirer =
-                mock(ActivityTaskManagerInternal.SleepTokenAcquirer.class);
-        doReturn(tokenAcquirer).when(mAtmInternal).createSleepTokenAcquirer(anyString());
         final PowerManager pm = mock(PowerManager.class);
         doReturn(true).when(pm).isInteractive();
         doReturn(pm).when(mContext).getSystemService(eq(Context.POWER_SERVICE));
@@ -155,9 +152,8 @@
         assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse();
 
         // Skip sleep-token for non-sleep-screen-off.
-        clearInvocations(tokenAcquirer);
         mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
-        verify(tokenAcquirer, never()).acquire(anyInt());
+        verify(displayPolicy).screenTurnedOff(false /* acquireSleepToken */);
         assertThat(isScreenTurnedOff[0]).isTrue();
 
         // Apply sleep-token for sleep-screen-off.
@@ -165,21 +161,10 @@
         mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
         assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isTrue();
         mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
-        verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY));
+        verify(displayPolicy).screenTurnedOff(true /* acquireSleepToken */);
 
         mPhoneWindowManager.finishedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
         assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse();
-
-        // Simulate unexpected reversed order: screenTurnedOff -> startedGoingToSleep. The sleep
-        // token can still be acquired.
-        isScreenTurnedOff[0] = false;
-        clearInvocations(tokenAcquirer);
-        mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */);
-        verify(tokenAcquirer, never()).acquire(anyInt());
-        assertThat(displayPolicy.isScreenOnEarly()).isFalse();
-        assertThat(displayPolicy.isScreenOnFully()).isFalse();
-        mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */);
-        verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY));
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 1e035da..e2e76d6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3231,7 +3231,7 @@
         mDisplayContent.mOpeningApps.remove(activity);
         mDisplayContent.mClosingApps.remove(activity);
         activity.commitVisibility(false /* visible */, false /* performLayout */);
-        mDisplayContent.getDisplayPolicy().screenTurnedOff();
+        mDisplayContent.getDisplayPolicy().screenTurnedOff(false /* acquireSleepToken */);
         final KeyguardController controller = mSupervisor.getKeyguardController();
         doReturn(true).when(controller).isKeyguardGoingAway(anyInt());
         activity.setVisibility(true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index caeb41c..f32a234 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -284,11 +284,11 @@
         final DisplayPolicy policy = mDisplayContent.getDisplayPolicy();
         policy.addWindowLw(mNotificationShadeWindow, mNotificationShadeWindow.mAttrs);
 
-        policy.screenTurnedOff();
+        policy.screenTurnedOff(false /* acquireSleepToken */);
         policy.setAwake(false);
         policy.screenTurningOn(null /* screenOnListener */);
         assertTrue(wpc.isShowingUiWhileDozing());
-        policy.screenTurnedOff();
+        policy.screenTurnedOff(false /* acquireSleepToken */);
         assertFalse(wpc.isShowingUiWhileDozing());
 
         policy.screenTurningOn(null /* screenOnListener */);
@@ -393,7 +393,7 @@
                 info.logicalWidth, info.logicalHeight).mConfigFrame);
 
         // If screen is not fully turned on, then the cache should be preserved.
-        displayPolicy.screenTurnedOff();
+        displayPolicy.screenTurnedOff(false /* acquireSleepToken */);
         final TransitionController transitionController = mDisplayContent.mTransitionController;
         spyOn(transitionController);
         doReturn(true).when(transitionController).isCollecting();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index cc1805a..fd959b9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -242,7 +242,7 @@
 
         final Rect relStartBounds = new Rect(mTaskFragment.getRelativeEmbeddedBounds());
         final DisplayPolicy displayPolicy = mDisplayContent.getDisplayPolicy();
-        displayPolicy.screenTurnedOff();
+        displayPolicy.screenTurnedOff(false /* acquireSleepToken */);
 
         assertFalse(mTaskFragment.okToAnimate());
 
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 3944b8e..284e2bd 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -420,6 +420,14 @@
     @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
     public static final int SATELLITE_RESULT_DISABLE_IN_PROGRESS = 28;
 
+    /**
+     * Enabling satellite is in progress.
+     *
+     * @hide
+     */
+    @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+    public static final int SATELLITE_RESULT_ENABLE_IN_PROGRESS = 29;
+
     /** @hide */
     @IntDef(prefix = {"SATELLITE_RESULT_"}, value = {
             SATELLITE_RESULT_SUCCESS,
@@ -450,7 +458,8 @@
             SATELLITE_RESULT_LOCATION_DISABLED,
             SATELLITE_RESULT_LOCATION_NOT_AVAILABLE,
             SATELLITE_RESULT_EMERGENCY_CALL_IN_PROGRESS,
-            SATELLITE_RESULT_DISABLE_IN_PROGRESS
+            SATELLITE_RESULT_DISABLE_IN_PROGRESS,
+            SATELLITE_RESULT_ENABLE_IN_PROGRESS
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface SatelliteResult {}
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index e852e6b..e57c207 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3394,4 +3394,19 @@
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission("
             + "android.Manifest.permission.SATELLITE_COMMUNICATION)")
     void provisionSatellite(in List<SatelliteSubscriberInfo> list, in ResultReceiver result);
+
+    /**
+     * This API can be used by only CTS to override the cached value for the device overlay config
+     * value :
+     * config_satellite_gateway_service_package and
+     * config_satellite_carrier_roaming_esos_provisioned_class.
+     * These values are set before sending an intent to broadcast there are any change to list of
+     * subscriber informations.
+     *
+     * @param name the name is one of the following that constitute an intent.
+     * Component package name, or component class name.
+     * @return {@code true} if the setting is successful, {@code false} otherwise.
+     * @hide
+     */
+    boolean setSatelliteSubscriberIdListChangedIntentComponent(in String name);
 }
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
index ad0ef1b..0f08be2 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
@@ -26,7 +26,9 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.graphics.Color;
 import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
 import android.testing.TestableContext;
 import android.view.MotionEvent;
 import android.view.View;
@@ -40,6 +42,8 @@
 
 import com.android.cts.input.MotionEventBuilder;
 import com.android.cts.input.PointerBuilder;
+import com.android.server.input.TouchpadFingerState;
+import com.android.server.input.TouchpadHardwareState;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -289,4 +293,36 @@
         assertEquals(initialX, mWindowLayoutParamsCaptor.getValue().x);
         assertEquals(initialY, mWindowLayoutParamsCaptor.getValue().y);
     }
+
+    @Test
+    public void testTouchpadClick() {
+        View child;
+
+        mTouchpadDebugView.updateHardwareState(
+                new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0,
+                        new TouchpadFingerState[0]));
+
+        for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) {
+            child = mTouchpadDebugView.getChildAt(i);
+            assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.BLUE);
+        }
+
+        mTouchpadDebugView.updateHardwareState(
+                new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0,
+                        new TouchpadFingerState[0]));
+
+        for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) {
+            child = mTouchpadDebugView.getChildAt(i);
+            assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.RED);
+        }
+
+        mTouchpadDebugView.updateHardwareState(
+                new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0,
+                        new TouchpadFingerState[0]));
+
+        for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) {
+            child = mTouchpadDebugView.getChildAt(i);
+            assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.BLUE);
+        }
+    }
 }
diff --git a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java b/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirect.java
similarity index 76%
copy from ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
copy to tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirect.java
index 4b9cf85..bc9471b 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
+++ b/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirect.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.ravenwood.annotation;
+package android.hosttest.annotation;
 
-import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.ElementType.METHOD;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -24,13 +24,9 @@
 /**
  * THIS ANNOTATION IS EXPERIMENTAL. REACH OUT TO g/ravenwood BEFORE USING IT, OR YOU HAVE ANY
  * QUESTIONS ABOUT IT.
- *
- * TODO: Javadoc
- *
  * @hide
  */
-@Target({TYPE})
+@Target({METHOD})
 @Retention(RetentionPolicy.CLASS)
-public @interface RavenwoodNativeSubstitutionClass {
-    String value();
+public @interface HostSideTestRedirect {
 }
diff --git a/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestNativeSubstitutionClass.java b/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirectionClass.java
similarity index 95%
rename from tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestNativeSubstitutionClass.java
rename to tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirectionClass.java
index 9c81383..28ad236 100644
--- a/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestNativeSubstitutionClass.java
+++ b/tools/hoststubgen/hoststubgen/annotations-src/android/hosttest/annotation/HostSideTestRedirectionClass.java
@@ -30,6 +30,6 @@
  */
 @Target({TYPE})
 @Retention(RetentionPolicy.CLASS)
-public @interface HostSideTestNativeSubstitutionClass {
+public @interface HostSideTestRedirectionClass {
     String value();
 }
diff --git a/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt b/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt
index e72c9a4..eba8e62 100644
--- a/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt
+++ b/tools/hoststubgen/hoststubgen/hoststubgen-standard-options.txt
@@ -27,8 +27,11 @@
 --substitute-annotation
     android.hosttest.annotation.HostSideTestSubstitute
 
---native-substitute-annotation
-    android.hosttest.annotation.HostSideTestNativeSubstitutionClass
+--redirect-annotation
+    android.hosttest.annotation.HostSideTestRedirect
+
+--redirection-class-annotation
+    android.hosttest.annotation.HostSideTestRedirectionClass
 
 --class-load-hook-annotation
     android.hosttest.annotation.HostSideTestClassLoadHook
diff --git a/tools/hoststubgen/hoststubgen/invoketest/hoststubgen-invoke-test.sh b/tools/hoststubgen/hoststubgen/invoketest/hoststubgen-invoke-test.sh
index 5f0368a..084448d 100755
--- a/tools/hoststubgen/hoststubgen/invoketest/hoststubgen-invoke-test.sh
+++ b/tools/hoststubgen/hoststubgen/invoketest/hoststubgen-invoke-test.sh
@@ -100,8 +100,10 @@
           android.hosttest.annotation.HostSideTestRemove \
       --substitute-annotation \
           android.hosttest.annotation.HostSideTestSubstitute \
-      --native-substitute-annotation \
-          android.hosttest.annotation.HostSideTestNativeSubstitutionClass \
+      --redirect-annotation \
+          android.hosttest.annotation.HostSideTestRedirect \
+      --redirection-class-annotation \
+          android.hosttest.annotation.HostSideTestRedirectionClass \
       --class-load-hook-annotation \
           android.hosttest.annotation.HostSideTestClassLoadHook \
       --keep-static-initializer-annotation \
@@ -223,4 +225,4 @@
 
 
 echo "All tests passed"
-exit 0
\ No newline at end of file
+exit 0
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 0f38fe7..34aaaa9 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -24,8 +24,9 @@
 import com.android.hoststubgen.filters.FilterPolicy
 import com.android.hoststubgen.filters.FilterRemapper
 import com.android.hoststubgen.filters.ImplicitOutputFilter
-import com.android.hoststubgen.filters.NativeFilter
+import com.android.hoststubgen.filters.KeepNativeFilter
 import com.android.hoststubgen.filters.OutputFilter
+import com.android.hoststubgen.filters.SanitizationFilter
 import com.android.hoststubgen.filters.createFilterFromTextPolicyFile
 import com.android.hoststubgen.filters.printAsTextPolicy
 import com.android.hoststubgen.utils.ClassFilter
@@ -134,7 +135,7 @@
         var filter: OutputFilter = ConstantFilter(options.defaultPolicy.get, "default-by-options")
 
         // Next, we build a filter that preserves all native methods by default
-        filter = NativeFilter(allClasses, filter)
+        filter = KeepNativeFilter(allClasses, filter)
 
         // Next, we need a filter that resolves "class-wide" policies.
         // This is used when a member (methods, fields, nested classes) don't get any polices
@@ -166,11 +167,12 @@
             options.throwAnnotations,
             options.removeAnnotations,
             options.substituteAnnotations,
-            options.nativeSubstituteAnnotations,
+            options.redirectAnnotations,
+            options.redirectionClassAnnotations,
             options.classLoadHookAnnotations,
             options.keepStaticInitializerAnnotations,
             annotationAllowedClassesFilter,
-            filter,
+            filter
         )
 
         // Next, "text based" filter, which allows to override polices without touching
@@ -182,6 +184,9 @@
         // Apply the implicit filter.
         filter = ImplicitOutputFilter(errors, allClasses, filter)
 
+        // Add a final sanitization step.
+        filter = SanitizationFilter(errors, allClasses, filter)
+
         return filter
     }
 
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index 1cedcc3..057a52c 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -85,9 +85,10 @@
         var throwAnnotations: MutableSet<String> = mutableSetOf(),
         var removeAnnotations: MutableSet<String> = mutableSetOf(),
         var keepClassAnnotations: MutableSet<String> = mutableSetOf(),
+        var redirectAnnotations: MutableSet<String> = mutableSetOf(),
 
         var substituteAnnotations: MutableSet<String> = mutableSetOf(),
-        var nativeSubstituteAnnotations: MutableSet<String> = mutableSetOf(),
+        var redirectionClassAnnotations: MutableSet<String> = mutableSetOf(),
         var classLoadHookAnnotations: MutableSet<String> = mutableSetOf(),
         var keepStaticInitializerAnnotations: MutableSet<String> = mutableSetOf(),
 
@@ -186,8 +187,11 @@
                         "--substitute-annotation" ->
                             ret.substituteAnnotations.addUniqueAnnotationArg()
 
-                        "--native-substitute-annotation" ->
-                            ret.nativeSubstituteAnnotations.addUniqueAnnotationArg()
+                        "--redirect-annotation" ->
+                            ret.redirectAnnotations.addUniqueAnnotationArg()
+
+                        "--redirection-class-annotation" ->
+                            ret.redirectionClassAnnotations.addUniqueAnnotationArg()
 
                         "--class-load-hook-annotation" ->
                             ret.classLoadHookAnnotations.addUniqueAnnotationArg()
@@ -275,7 +279,7 @@
               removeAnnotations=$removeAnnotations,
               keepClassAnnotations=$keepClassAnnotations,
               substituteAnnotations=$substituteAnnotations,
-              nativeSubstituteAnnotations=$nativeSubstituteAnnotations,
+              nativeSubstituteAnnotations=$redirectionClassAnnotations,
               classLoadHookAnnotations=$classLoadHookAnnotations,
               keepStaticInitializerAnnotations=$keepStaticInitializerAnnotations,
               packageRedirects=$packageRedirects,
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt
index 7197e0e..a02082d 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt
@@ -29,33 +29,24 @@
 
 
 /** Name of the class initializer method. */
-val CLASS_INITIALIZER_NAME = "<clinit>"
+const val CLASS_INITIALIZER_NAME = "<clinit>"
 
 /** Descriptor of the class initializer method. */
-val CLASS_INITIALIZER_DESC = "()V"
+const val CLASS_INITIALIZER_DESC = "()V"
 
 /** Name of constructors. */
-val CTOR_NAME = "<init>"
+const val CTOR_NAME = "<init>"
 
 /**
- * Find any of [anyAnnotations] from the list of visible / invisible annotations.
+ * Find any of [set] from the list of visible / invisible annotations.
  */
 fun findAnyAnnotation(
-        anyAnnotations: Set<String>,
-        visibleAnnotations: List<AnnotationNode>?,
-        invisibleAnnotations: List<AnnotationNode>?,
-    ): AnnotationNode? {
-    for (an in visibleAnnotations ?: emptyList()) {
-        if (anyAnnotations.contains(an.desc)) {
-            return an
-        }
-    }
-    for (an in invisibleAnnotations ?: emptyList()) {
-        if (anyAnnotations.contains(an.desc)) {
-            return an
-        }
-    }
-    return null
+    set: Set<String>,
+    visibleAnnotations: List<AnnotationNode>?,
+    invisibleAnnotations: List<AnnotationNode>?,
+): AnnotationNode? {
+    return visibleAnnotations?.find { it.desc in set }
+        ?: invisibleAnnotations?.find { it.desc in set }
 }
 
 fun ClassNode.findAnyAnnotation(set: Set<String>): AnnotationNode? {
@@ -70,6 +61,27 @@
     return findAnyAnnotation(set, this.visibleAnnotations, this.invisibleAnnotations)
 }
 
+fun findAllAnnotations(
+    set: Set<String>,
+    visibleAnnotations: List<AnnotationNode>?,
+    invisibleAnnotations: List<AnnotationNode>?
+): List<AnnotationNode> {
+    return (visibleAnnotations ?: emptyList()).filter { it.desc in set } +
+            (invisibleAnnotations ?: emptyList()).filter { it.desc in set }
+}
+
+fun ClassNode.findAllAnnotations(set: Set<String>): List<AnnotationNode> {
+    return findAllAnnotations(set, this.visibleAnnotations, this.invisibleAnnotations)
+}
+
+fun MethodNode.findAllAnnotations(set: Set<String>): List<AnnotationNode> {
+    return findAllAnnotations(set, this.visibleAnnotations, this.invisibleAnnotations)
+}
+
+fun FieldNode.findAllAnnotations(set: Set<String>): List<AnnotationNode> {
+    return findAllAnnotations(set, this.visibleAnnotations, this.invisibleAnnotations)
+}
+
 fun <T> findAnnotationValueAsObject(
     an: AnnotationNode,
     propertyName: String,
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
index 38a41b2..a6b8cdb 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
@@ -18,12 +18,15 @@
 import com.android.hoststubgen.ClassParseException
 import com.android.hoststubgen.HostStubGenErrors
 import com.android.hoststubgen.InvalidAnnotationException
-import com.android.hoststubgen.addNonNullElement
+import com.android.hoststubgen.addLists
 import com.android.hoststubgen.asm.CLASS_INITIALIZER_DESC
 import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME
 import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.findAllAnnotations
 import com.android.hoststubgen.asm.findAnnotationValueAsString
 import com.android.hoststubgen.asm.findAnyAnnotation
+import com.android.hoststubgen.asm.getPackageNameFromFullClassName
+import com.android.hoststubgen.asm.resolveClassNameWithDefaultPackage
 import com.android.hoststubgen.asm.toHumanReadableClassName
 import com.android.hoststubgen.asm.toHumanReadableMethodName
 import com.android.hoststubgen.asm.toJvmClassName
@@ -46,7 +49,8 @@
     throwAnnotations_: Set<String>,
     removeAnnotations_: Set<String>,
     substituteAnnotations_: Set<String>,
-    nativeSubstituteAnnotations_: Set<String>,
+    redirectAnnotations_: Set<String>,
+    redirectionClassAnnotations_: Set<String>,
     classLoadHookAnnotations_: Set<String>,
     keepStaticInitializerAnnotations_: Set<String>,
     private val annotationAllowedClassesFilter: ClassFilter,
@@ -56,8 +60,10 @@
     private val keepClassAnnotations = convertToInternalNames(keepClassAnnotations_)
     private val throwAnnotations = convertToInternalNames(throwAnnotations_)
     private val removeAnnotations = convertToInternalNames(removeAnnotations_)
+    private val redirectAnnotations = convertToInternalNames(redirectAnnotations_)
     private val substituteAnnotations = convertToInternalNames(substituteAnnotations_)
-    private val nativeSubstituteAnnotations = convertToInternalNames(nativeSubstituteAnnotations_)
+    private val redirectionClassAnnotations =
+        convertToInternalNames(redirectionClassAnnotations_)
     private val classLoadHookAnnotations = convertToInternalNames(classLoadHookAnnotations_)
     private val keepStaticInitializerAnnotations =
         convertToInternalNames(keepStaticInitializerAnnotations_)
@@ -67,11 +73,12 @@
             keepClassAnnotations +
             throwAnnotations +
             removeAnnotations +
+            redirectAnnotations +
             substituteAnnotations
 
     /** All the annotations we use. */
     private val allAnnotations = visibilityAnnotations +
-            nativeSubstituteAnnotations +
+            redirectionClassAnnotations +
             classLoadHookAnnotations +
             keepStaticInitializerAnnotations
 
@@ -84,8 +91,9 @@
                 keepClassAnnotations_ +
                 throwAnnotations_ +
                 removeAnnotations_ +
+                redirectAnnotations_ +
                 substituteAnnotations_ +
-                nativeSubstituteAnnotations_ +
+                redirectionClassAnnotations_ +
                 classLoadHookAnnotations_ +
                 keepStaticInitializerAnnotations_
     )
@@ -99,6 +107,7 @@
             in substituteAnnotations -> FilterPolicy.Substitute.withReason(REASON_ANNOTATION)
             in throwAnnotations -> FilterPolicy.Throw.withReason(REASON_ANNOTATION)
             in removeAnnotations -> FilterPolicy.Remove.withReason(REASON_ANNOTATION)
+            in redirectAnnotations -> FilterPolicy.Redirect.withReason(REASON_ANNOTATION)
             else -> null
         }
     }
@@ -129,13 +138,6 @@
         descriptor: String
     ): FilterPolicyWithReason {
         val cn = classes.getClass(className)
-
-        if (methodName == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC) {
-            if (cn.findAnyAnnotation(keepStaticInitializerAnnotations) != null) {
-                return FilterPolicy.Keep.withReason(REASON_ANNOTATION)
-            }
-        }
-
         return getAnnotationPolicy(cn).methodPolicies[MethodKey(methodName, descriptor)]
             ?: super.getPolicyForMethod(className, methodName, descriptor)
     }
@@ -150,22 +152,14 @@
             ?: super.getRenameTo(className, methodName, descriptor)
     }
 
-    override fun getNativeSubstitutionClass(className: String): String? {
-        classes.getClass(className).let { cn ->
-            cn.findAnyAnnotation(nativeSubstituteAnnotations)?.let { an ->
-                return getAnnotationField(an, "value")?.toJvmClassName()
-            }
-        }
-        return null
+    override fun getRedirectionClass(className: String): String? {
+        val cn = classes.getClass(className)
+        return getAnnotationPolicy(cn).redirectionClass
     }
 
     override fun getClassLoadHooks(className: String): List<String> {
-        val e = classes.getClass(className).let { cn ->
-            cn.findAnyAnnotation(classLoadHookAnnotations)?.let { an ->
-                getAnnotationField(an, "value")?.toHumanReadableMethodName()
-            }
-        }
-        return addNonNullElement(super.getClassLoadHooks(className), e)
+        val cn = classes.getClass(className)
+        return addLists(super.getClassLoadHooks(className), getAnnotationPolicy(cn).classLoadHooks)
     }
 
     private data class MethodKey(val name: String, val desc: String)
@@ -195,6 +189,8 @@
         val fieldPolicies = mutableMapOf<String, FilterPolicyWithReason>()
         val methodPolicies = mutableMapOf<MethodKey, FilterPolicyWithReason>()
         val renamedMethods = mutableMapOf<MethodKey, String>()
+        val redirectionClass: String?
+        val classLoadHooks: List<String>
 
         init {
             val allowAnnotation = annotationAllowedClassesFilter.matches(cn.name)
@@ -204,6 +200,16 @@
                 "class", cn.name
             )
             classPolicy = cn.findAnyAnnotation(visibilityAnnotations)?.policy
+            redirectionClass = cn.findAnyAnnotation(redirectionClassAnnotations)?.let { an ->
+                getAnnotationField(an, "value")?.let { resolveRelativeClass(cn, it) }
+            }
+            classLoadHooks = cn.findAllAnnotations(classLoadHookAnnotations).mapNotNull { an ->
+                getAnnotationField(an, "value")?.toHumanReadableMethodName()
+            }
+            if (cn.findAnyAnnotation(keepStaticInitializerAnnotations) != null) {
+                methodPolicies[MethodKey(CLASS_INITIALIZER_NAME, CLASS_INITIALIZER_DESC)] =
+                    FilterPolicy.Keep.withReason(REASON_ANNOTATION)
+            }
 
             for (fn in cn.fields ?: emptyList()) {
                 detectInvalidAnnotations(
@@ -297,25 +303,36 @@
                 )
             }
         }
-    }
 
-    /**
-     * Return the (String) value of 'value' parameter from an annotation.
-     */
-    private fun getAnnotationField(
-        an: AnnotationNode,
-        name: String,
-        required: Boolean = true
-    ): String? {
-        try {
-            val suffix = findAnnotationValueAsString(an, name)
-            if (suffix == null && required) {
-                errors.onErrorFound("Annotation \"${an.desc}\" must have field $name")
+        /**
+         * Return the (String) value of 'value' parameter from an annotation.
+         */
+        private fun getAnnotationField(
+            an: AnnotationNode,
+            name: String,
+            required: Boolean = true
+        ): String? {
+            try {
+                val suffix = findAnnotationValueAsString(an, name)
+                if (suffix == null && required) {
+                    errors.onErrorFound("Annotation \"${an.desc}\" must have field $name")
+                }
+                return suffix
+            } catch (e: ClassParseException) {
+                errors.onErrorFound(e.message!!)
+                return null
             }
-            return suffix
-        } catch (e: ClassParseException) {
-            errors.onErrorFound(e.message!!)
-            return null
+        }
+
+        /**
+         * Resolve the full class name if the class is relative
+         */
+        private fun resolveRelativeClass(
+            cn: ClassNode,
+            name: String
+        ): String {
+            val packageName = getPackageNameFromFullClassName(cn.name)
+            return resolveClassNameWithDefaultPackage(name, packageName).toJvmClassName()
         }
     }
 
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ClassWidePolicyPropagatingFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ClassWidePolicyPropagatingFilter.kt
index 8ee3a94..f8bb526 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ClassWidePolicyPropagatingFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ClassWidePolicyPropagatingFilter.kt
@@ -16,7 +16,6 @@
 package com.android.hoststubgen.filters
 
 import com.android.hoststubgen.asm.ClassNodes
-import com.android.hoststubgen.asm.isNative
 
 /**
  * This is used as the second last fallback filter. This filter propagates the class-wide policy
@@ -88,16 +87,7 @@
         methodName: String,
         descriptor: String
     ): FilterPolicyWithReason {
-        return outermostFilter.getNativeSubstitutionClass(className)?.let {
-            // First check native substitution
-            classes.findMethod(className, methodName, descriptor)?.let { mn ->
-                if (mn.isNative()) {
-                    FilterPolicy.NativeSubstitute.withReason("class-wide in $className")
-                } else {
-                    null
-                }
-            }
-        } ?: getClassWidePolicy(className, resolve = true)
-        ?: super.getPolicyForMethod(className, methodName, descriptor)
+        return getClassWidePolicy(className, resolve = true)
+            ?: super.getPolicyForMethod(className, methodName, descriptor)
     }
-}
\ No newline at end of file
+}
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 6fcffb8..b8b0d8a 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/DelegatingFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/DelegatingFilter.kt
@@ -72,8 +72,8 @@
         return fallback.getRenameTo(className, methodName, descriptor)
     }
 
-    override fun getNativeSubstitutionClass(className: String): String? {
-        return fallback.getNativeSubstitutionClass(className)
+    override fun getRedirectionClass(className: String): String? {
+        return fallback.getRedirectionClass(className)
     }
 
     override fun getClassLoadHooks(className: String): List<String> {
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
index ab03874..2f2f81b 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
@@ -33,9 +33,9 @@
     Substitute,
 
     /**
-     * Only usable with methods. Replace a native method with a "substitution" method,
+     * Only usable with methods. Redirect a method to a method in the substitution class.
      */
-    NativeSubstitute,
+    Redirect,
 
     /**
      * Only usable with methods. The item will be kept in the impl jar file, but when called,
@@ -102,8 +102,7 @@
     val isSupported: Boolean
         get() {
             return when (this) {
-                // TODO: handle native method with no substitution as being unsupported
-                Keep, KeepClass, Substitute, NativeSubstitute -> true
+                Keep, KeepClass, Substitute, Redirect -> true
                 else -> false
             }
         }
@@ -111,7 +110,7 @@
     val isMethodRewriteBody: Boolean
         get() {
             return when (this) {
-                NativeSubstitute, Throw, Ignore -> true
+                Redirect, Throw, Ignore -> true
                 else -> false
             }
         }
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
index 2e144f5..59fa464 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
@@ -19,6 +19,7 @@
 import com.android.hoststubgen.asm.ClassNodes
 import com.android.hoststubgen.asm.toHumanReadableClassName
 import com.android.hoststubgen.asm.toHumanReadableMethodName
+import com.android.hoststubgen.asm.toJvmClassName
 import com.android.hoststubgen.log
 
 // TODO: Validate all input names.
@@ -29,7 +30,7 @@
 ) : DelegatingFilter(fallback) {
     private val mPolicies: MutableMap<String, FilterPolicyWithReason> = mutableMapOf()
     private val mRenames: MutableMap<String, String> = mutableMapOf()
-    private val mNativeSubstitutionClasses: MutableMap<String, String> = mutableMapOf()
+    private val mRedirectionClasses: MutableMap<String, String> = mutableMapOf()
     private val mClassLoadHooks: MutableMap<String, String> = mutableMapOf()
 
     private fun getClassKey(className: String): String {
@@ -115,17 +116,17 @@
         mRenames[getMethodKey(className, methodName, descriptor)] = toName
     }
 
-    override fun getNativeSubstitutionClass(className: String): String? {
-        return mNativeSubstitutionClasses[getClassKey(className)]
-                ?: super.getNativeSubstitutionClass(className)
+    override fun getRedirectionClass(className: String): String? {
+        return mRedirectionClasses[getClassKey(className)]
+                ?: super.getRedirectionClass(className)
     }
 
-    fun setNativeSubstitutionClass(from: String, to: String) {
+    fun setRedirectionClass(from: String, to: String) {
         checkClass(from)
 
-        // Native substitute classes may be provided from other jars, so we can't do this check.
+        // Redirection classes may be provided from other jars, so we can't do this check.
         // ensureClassExists(to)
-        mNativeSubstitutionClasses[getClassKey(from)] = to.toHumanReadableClassName()
+        mRedirectionClasses[getClassKey(from)] = to.toJvmClassName()
     }
 
     override fun getClassLoadHooks(className: String): List<String> {
@@ -136,4 +137,4 @@
     fun setClassLoadHook(className: String, methodName: String) {
         mClassLoadHooks[getClassKey(className)] = methodName.toHumanReadableMethodName()
     }
-}
\ No newline at end of file
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/NativeFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/KeepNativeFilter.kt
similarity index 82%
rename from tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/NativeFilter.kt
rename to tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/KeepNativeFilter.kt
index bd71931..00e7d77 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/NativeFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/KeepNativeFilter.kt
@@ -18,7 +18,12 @@
 import com.android.hoststubgen.asm.ClassNodes
 import com.android.hoststubgen.asm.isNative
 
-class NativeFilter(
+/**
+ *  For native methods that weren't handled by outer filters, we keep it so that
+ *  native method registration will not crash at runtime. Ideally we shouldn't need
+ *  this, but in practice unsupported native method registrations do occur.
+ */
+class KeepNativeFilter(
     private val classes: ClassNodes,
     fallback: OutputFilter
 ) : DelegatingFilter(fallback) {
@@ -28,8 +33,6 @@
         descriptor: String,
     ): FilterPolicyWithReason {
         return classes.findMethod(className, methodName, descriptor)?.let { mn ->
-            // For native methods that weren't handled by outer filters,
-            // we keep it so that native method registration will not crash.
             if (mn.isNative()) {
                 FilterPolicy.Keep.withReason("native-preserve")
             } else {
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/OutputFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/OutputFilter.kt
index 1049e2b..f99ce90 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/OutputFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/OutputFilter.kt
@@ -35,10 +35,6 @@
      * using it.
      */
     open var outermostFilter: OutputFilter = this
-        get() = field
-        set(value) {
-            field = value
-        }
 
     abstract fun getPolicyForClass(className: String): FilterPolicyWithReason
 
@@ -60,13 +56,13 @@
     }
 
     /**
-     * Return a "native substitution class" name for a given class.
+     * Return a "redirection class" name for a given class.
      *
-     * The result will be in a "human readable" form. (e.g. uses '.'s instead of '/'s)
+     * The result will be in a JVM internal form. (e.g. uses '/'s instead of '.'s)
      *
-     * (which corresponds to @HostSideTestNativeSubstitutionClass of the standard annotations.)
+     * (which corresponds to @HostSideTestRedirectClass of the standard annotations.)
      */
-    open fun getNativeSubstitutionClass(className: String): String? {
+    open fun getRedirectionClass(className: String): String? {
         return null
     }
 
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/SanitizationFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/SanitizationFilter.kt
new file mode 100644
index 0000000..18a1e16
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/SanitizationFilter.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.hoststubgen.filters
+
+import com.android.hoststubgen.HostStubGenErrors
+import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.toHumanReadableClassName
+import com.android.hoststubgen.log
+
+/**
+ * Check whether the policies in the inner layers make sense, and sanitize the results.
+ */
+class SanitizationFilter(
+    private val errors: HostStubGenErrors,
+    private val classes: ClassNodes,
+    fallback: OutputFilter
+) : DelegatingFilter(fallback) {
+    override fun getPolicyForMethod(
+        className: String,
+        methodName: String,
+        descriptor: String
+    ): FilterPolicyWithReason {
+        val policy = super.getPolicyForMethod(className, methodName, descriptor)
+        if (policy.policy == FilterPolicy.Redirect) {
+            // Check whether the hosting class has a redirection class
+            if (getRedirectionClass(className) == null) {
+                errors.onErrorFound("Method $methodName$descriptor requires a redirection " +
+                        "class set on ${className.toHumanReadableClassName()}")
+            }
+        }
+        return policy
+    }
+
+    override fun getRedirectionClass(className: String): String? {
+        return super.getRedirectionClass(className)?.also { clazz ->
+            if (classes.findClass(clazz) == null) {
+                log.w("Redirection class $clazz not found. Class must be available at runtime.")
+            } else if (outermostFilter.getPolicyForClass(clazz).policy != FilterPolicy.KeepClass) {
+                // If the class exists, it must have a KeepClass policy.
+                errors.onErrorFound("Redirection class $clazz must have @KeepWholeClass.")
+            }
+        }
+    }
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
index 14fd82b..073b503 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -142,9 +142,9 @@
                                     throw ParseException(
                                             "Special class can't have a substitution")
                                 }
-                                // It's a native-substitution.
+                                // It's a redirection class.
                                 val toClass = fields[2].substring(1)
-                                imf.setNativeSubstitutionClass(className, toClass)
+                                imf.setRedirectionClass(className, toClass)
                             } else if (fields[2].startsWith("~")) {
                                 if (classType != SpecialClass.NotSpecial) {
                                     // We could support it, but not needed at least for now.
@@ -350,6 +350,7 @@
         "r", "remove" -> FilterPolicy.Remove
         "kc", "keepclass" -> FilterPolicy.KeepClass
         "i", "ignore" -> FilterPolicy.Ignore
+        "rdr", "redirect" -> FilterPolicy.Redirect
         else -> {
             if (s.startsWith("@")) {
                 FilterPolicy.Substitute
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
index 41ba928..261ef59c 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
@@ -21,8 +21,6 @@
 import com.android.hoststubgen.asm.ClassNodes
 import com.android.hoststubgen.asm.UnifiedVisitor
 import com.android.hoststubgen.asm.getPackageNameFromFullClassName
-import com.android.hoststubgen.asm.resolveClassNameWithDefaultPackage
-import com.android.hoststubgen.asm.toJvmClassName
 import com.android.hoststubgen.filters.FilterPolicy
 import com.android.hoststubgen.filters.FilterPolicyWithReason
 import com.android.hoststubgen.filters.OutputFilter
@@ -57,7 +55,7 @@
 
     protected lateinit var currentPackageName: String
     protected lateinit var currentClassName: String
-    protected var nativeSubstitutionClass: String? = null
+    protected var redirectionClass: String? = null
     protected lateinit var classPolicy: FilterPolicyWithReason
 
     override fun visit(
@@ -72,34 +70,13 @@
         currentClassName = name
         currentPackageName = getPackageNameFromFullClassName(name)
         classPolicy = filter.getPolicyForClass(currentClassName)
+        redirectionClass = filter.getRedirectionClass(currentClassName)
 
         log.d("[%s] visit: %s (package: %s)", this.javaClass.simpleName, name, currentPackageName)
         log.indent()
         log.v("Emitting class: %s", name)
         log.indent()
 
-        filter.getNativeSubstitutionClass(currentClassName)?.let { className ->
-            val fullClassName = resolveClassNameWithDefaultPackage(className, currentPackageName)
-                .toJvmClassName()
-            log.d("  NativeSubstitutionClass: $fullClassName")
-            if (classes.findClass(fullClassName) == null) {
-                log.w(
-                    "Native substitution class $fullClassName not found. Class must be " +
-                            "available at runtime."
-                )
-            } else {
-                // If the class exists, it must have a KeepClass policy.
-                if (filter.getPolicyForClass(fullClassName).policy != FilterPolicy.KeepClass) {
-                    // TODO: Use real annotation name.
-                    options.errors.onErrorFound(
-                        "Native substitution class $fullClassName should have @Keep."
-                    )
-                }
-            }
-
-            nativeSubstitutionClass = fullClassName
-        }
-
         // Inject annotations to generated classes.
         UnifiedVisitor.on(this).visitAnnotation(HostStubGenProcessedAsKeep.CLASS_DESCRIPTOR, true)
     }
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
index 057d653..567a69e 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
@@ -184,9 +184,12 @@
                     return IgnoreMethodAdapter(descriptor, forceCreateBody, innerVisitor)
                         .withAnnotation(HostStubGenProcessedAsIgnore.CLASS_DESCRIPTOR)
                 }
-                FilterPolicy.NativeSubstitute -> {
-                    log.v("Rewriting native method...")
-                    return NativeSubstitutingMethodAdapter(access, name, descriptor, innerVisitor)
+                FilterPolicy.Redirect -> {
+                    log.v("Redirecting method...")
+                    return RedirectMethodAdapter(
+                        access, name, descriptor,
+                        forceCreateBody, innerVisitor
+                    )
                         .withAnnotation(HostStubGenProcessedAsSubstitute.CLASS_DESCRIPTOR)
                 }
                 else -> {}
@@ -274,15 +277,16 @@
     }
 
     /**
-     * A method adapter that rewrite a native method body with a
-     * call to a method in the "native substitution" class.
+     * A method adapter that rewrite a method body with a
+     * call to a method in the redirection class.
      */
-    private inner class NativeSubstitutingMethodAdapter(
+    private inner class RedirectMethodAdapter(
         access: Int,
         private val name: String,
         private val descriptor: String,
+        createBody: Boolean,
         next: MethodVisitor?
-    ) : BodyReplacingMethodVisitor(true, next) {
+    ) : BodyReplacingMethodVisitor(createBody, next) {
 
         private val isStatic = (access and Opcodes.ACC_STATIC) != 0
 
@@ -290,7 +294,7 @@
             var targetDescriptor = descriptor
             var argOffset = 0
 
-            // For non-static native method, we need to tweak it a bit.
+            // For non-static method, we need to tweak it a bit.
             if (!isStatic) {
                 // Push `this` as the first argument.
                 this.visitVarInsn(Opcodes.ALOAD, 0)
@@ -310,7 +314,7 @@
 
             visitMethodInsn(
                 INVOKESTATIC,
-                nativeSubstitutionClass,
+                redirectionClass,
                 name,
                 targetDescriptor,
                 false
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
index 5fde14f..82586bb 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
@@ -41,20 +41,40 @@
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
-## Class: android/hosttest/annotation/HostSideTestNativeSubstitutionClass.class
-  Compiled from "HostSideTestNativeSubstitutionClass.java"
-public interface android.hosttest.annotation.HostSideTestNativeSubstitutionClass extends java.lang.annotation.Annotation
+## Class: android/hosttest/annotation/HostSideTestRedirect.class
+  Compiled from "HostSideTestRedirect.java"
+public interface android.hosttest.annotation.HostSideTestRedirect extends java.lang.annotation.Annotation
   minor version: 0
   major version: 61
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
-  this_class: #x                          // android/hosttest/annotation/HostSideTestNativeSubstitutionClass
+  this_class: #x                          // android/hosttest/annotation/HostSideTestRedirect
+  super_class: #x                         // java/lang/Object
+  interfaces: 1, fields: 0, methods: 0, attributes: 2
+}
+SourceFile: "HostSideTestRedirect.java"
+RuntimeVisibleAnnotations:
+  x: #x(#x=[e#x.#x])
+    java.lang.annotation.Target(
+      value=[Ljava/lang/annotation/ElementType;.METHOD]
+    )
+  x: #x(#x=e#x.#x)
+    java.lang.annotation.Retention(
+      value=Ljava/lang/annotation/RetentionPolicy;.CLASS
+    )
+## Class: android/hosttest/annotation/HostSideTestRedirectionClass.class
+  Compiled from "HostSideTestRedirectionClass.java"
+public interface android.hosttest.annotation.HostSideTestRedirectionClass extends java.lang.annotation.Annotation
+  minor version: 0
+  major version: 61
+  flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
+  this_class: #x                          // android/hosttest/annotation/HostSideTestRedirectionClass
   super_class: #x                         // java/lang/Object
   interfaces: 1, fields: 0, methods: 1, attributes: 2
   public abstract java.lang.String value();
     descriptor: ()Ljava/lang/String;
     flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
 }
-SourceFile: "HostSideTestNativeSubstitutionClass.java"
+SourceFile: "HostSideTestRedirectionClass.java"
 RuntimeVisibleAnnotations:
   x: #x(#x=[e#x.#x])
     java.lang.annotation.Target(
@@ -1925,7 +1945,7 @@
   flags: (0x0021) ACC_PUBLIC, ACC_SUPER
   this_class: #x                         // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
   super_class: #x                         // java/lang/Object
-  interfaces: 0, fields: 1, methods: 12, attributes: 2
+  interfaces: 0, fields: 1, methods: 14, attributes: 2
   int value;
     descriptor: I
     flags: (0x0000)
@@ -1946,6 +1966,9 @@
   public static native int nativeAddTwo(int);
     descriptor: (I)I
     flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
 
   public static int nativeAddTwo_should_be_like_this(int);
     descriptor: (I)I
@@ -1963,6 +1986,9 @@
   public static native long nativeLongPlus(long, long);
     descriptor: (JJ)J
     flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
 
   public static long nativeLongPlus_should_be_like_this(long, long);
     descriptor: (JJ)J
@@ -1997,6 +2023,9 @@
   public native int nativeNonStaticAddToValue(int);
     descriptor: (I)I
     flags: (0x0101) ACC_PUBLIC, ACC_NATIVE
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
 
   public int nativeNonStaticAddToValue_should_be_like_this(int);
     descriptor: (I)I
@@ -2023,9 +2052,6 @@
   public static native void nativeStillKeep();
     descriptor: ()V
     flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE
-    RuntimeInvisibleAnnotations:
-      x: #x()
-        android.hosttest.annotation.HostSideTestKeep
 
   public static void nativeStillNotSupported_should_be_like_this();
     descriptor: ()V
@@ -2041,13 +2067,47 @@
   public static native byte nativeBytePlus(byte, byte);
     descriptor: (BB)B
     flags: (0x0109) ACC_PUBLIC, ACC_STATIC, ACC_NATIVE
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
+
+  public void notNativeRedirected();
+    descriptor: ()V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=2, locals=1, args_size=1
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":()V
+         x: athrow
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0       8     0  this   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
+
+  public static void notNativeStaticRedirected();
+    descriptor: ()V
+    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=2, locals=0, args_size=0
+         x: new           #x                 // class java/lang/RuntimeException
+         x: dup
+         x: invokespecial #x                 // Method java/lang/RuntimeException."<init>":()V
+         x: athrow
+      LineNumberTable:
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
 }
 SourceFile: "TinyFrameworkNative.java"
 RuntimeInvisibleAnnotations:
   x: #x()
     android.hosttest.annotation.HostSideTestWholeClassKeep
   x: #x(#x=s#x)
-    android.hosttest.annotation.HostSideTestNativeSubstitutionClass(
+    android.hosttest.annotation.HostSideTestRedirectionClass(
       value="TinyFrameworkNative_host"
     )
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.class
@@ -2058,7 +2118,7 @@
   flags: (0x0021) ACC_PUBLIC, ACC_SUPER
   this_class: #x                         // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
   super_class: #x                         // java/lang/Object
-  interfaces: 0, fields: 0, methods: 5, attributes: 2
+  interfaces: 0, fields: 0, methods: 7, attributes: 2
   public com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host();
     descriptor: ()V
     flags: (0x0001) ACC_PUBLIC
@@ -2132,6 +2192,25 @@
         Start  Length  Slot  Name   Signature
             0       5     0  arg1   B
             0       5     1  arg2   B
+
+  public static void notNativeRedirected(com.android.hoststubgen.test.tinyframework.TinyFrameworkNative);
+    descriptor: (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V
+    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=0, locals=1, args_size=1
+         x: return
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0       1     0 source   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+
+  public static void notNativeStaticRedirected();
+    descriptor: ()V
+    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=0, locals=0, args_size=0
+         x: return
+      LineNumberTable:
 }
 SourceFile: "TinyFrameworkNative_host.java"
 RuntimeInvisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt
index e41d46d..31bbcc5 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-dump.txt
@@ -48,13 +48,35 @@
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
-## Class: android/hosttest/annotation/HostSideTestNativeSubstitutionClass.class
-  Compiled from "HostSideTestNativeSubstitutionClass.java"
-public interface android.hosttest.annotation.HostSideTestNativeSubstitutionClass extends java.lang.annotation.Annotation
+## Class: android/hosttest/annotation/HostSideTestRedirect.class
+  Compiled from "HostSideTestRedirect.java"
+public interface android.hosttest.annotation.HostSideTestRedirect extends java.lang.annotation.Annotation
   minor version: 0
   major version: 61
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
-  this_class: #x                          // android/hosttest/annotation/HostSideTestNativeSubstitutionClass
+  this_class: #x                          // android/hosttest/annotation/HostSideTestRedirect
+  super_class: #x                         // java/lang/Object
+  interfaces: 1, fields: 0, methods: 0, attributes: 2
+}
+SourceFile: "HostSideTestRedirect.java"
+RuntimeVisibleAnnotations:
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+  x: #x(#x=[e#x.#x])
+    java.lang.annotation.Target(
+      value=[Ljava/lang/annotation/ElementType;.METHOD]
+    )
+  x: #x(#x=e#x.#x)
+    java.lang.annotation.Retention(
+      value=Ljava/lang/annotation/RetentionPolicy;.CLASS
+    )
+## Class: android/hosttest/annotation/HostSideTestRedirectionClass.class
+  Compiled from "HostSideTestRedirectionClass.java"
+public interface android.hosttest.annotation.HostSideTestRedirectionClass extends java.lang.annotation.Annotation
+  minor version: 0
+  major version: 61
+  flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
+  this_class: #x                          // android/hosttest/annotation/HostSideTestRedirectionClass
   super_class: #x                         // java/lang/Object
   interfaces: 1, fields: 0, methods: 1, attributes: 2
   public abstract java.lang.String value();
@@ -64,7 +86,7 @@
       x: #x()
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
 }
-SourceFile: "HostSideTestNativeSubstitutionClass.java"
+SourceFile: "HostSideTestRedirectionClass.java"
 RuntimeVisibleAnnotations:
   x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
@@ -2076,7 +2098,7 @@
   flags: (0x0021) ACC_PUBLIC, ACC_SUPER
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
   super_class: #x                         // java/lang/Object
-  interfaces: 0, fields: 1, methods: 12, attributes: 3
+  interfaces: 0, fields: 1, methods: 14, attributes: 3
   int value;
     descriptor: I
     flags: (0x0000)
@@ -2113,6 +2135,9 @@
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
       x: #x()
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
 
   public static int nativeAddTwo_should_be_like_this(int);
     descriptor: (I)I
@@ -2144,6 +2169,9 @@
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
       x: #x()
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
 
   public static long nativeLongPlus_should_be_like_this(long, long);
     descriptor: (JJ)J
@@ -2195,6 +2223,9 @@
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
       x: #x()
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
 
   public int nativeNonStaticAddToValue_should_be_like_this(int);
     descriptor: (I)I
@@ -2240,9 +2271,6 @@
     RuntimeVisibleAnnotations:
       x: #x()
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
-    RuntimeInvisibleAnnotations:
-      x: #x()
-        android.hosttest.annotation.HostSideTestKeep
 
   public static void nativeStillNotSupported_should_be_like_this();
     descriptor: ()V
@@ -2272,6 +2300,42 @@
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
       x: #x()
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
+
+  public void notNativeRedirected();
+    descriptor: ()V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=1, locals=1, args_size=1
+         x: aload_0
+         x: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.notNativeRedirected:(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V
+         x: return
+    RuntimeVisibleAnnotations:
+      x: #x()
+        com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
+      x: #x()
+        com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
+
+  public static void notNativeStaticRedirected();
+    descriptor: ()V
+    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=0, locals=0, args_size=0
+         x: invokestatic  #x                 // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.notNativeStaticRedirected:()V
+         x: return
+    RuntimeVisibleAnnotations:
+      x: #x()
+        com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
+      x: #x()
+        com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
 }
 SourceFile: "TinyFrameworkNative.java"
 RuntimeVisibleAnnotations:
@@ -2281,7 +2345,7 @@
   x: #x()
     android.hosttest.annotation.HostSideTestWholeClassKeep
   x: #x(#x=s#x)
-    android.hosttest.annotation.HostSideTestNativeSubstitutionClass(
+    android.hosttest.annotation.HostSideTestRedirectionClass(
       value="TinyFrameworkNative_host"
     )
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.class
@@ -2292,7 +2356,7 @@
   flags: (0x0021) ACC_PUBLIC, ACC_SUPER
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
   super_class: #x                         // java/lang/Object
-  interfaces: 0, fields: 0, methods: 5, attributes: 3
+  interfaces: 0, fields: 0, methods: 7, attributes: 3
   public com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host();
     descriptor: ()V
     flags: (0x0001) ACC_PUBLIC
@@ -2381,6 +2445,31 @@
     RuntimeVisibleAnnotations:
       x: #x()
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+
+  public static void notNativeRedirected(com.android.hoststubgen.test.tinyframework.TinyFrameworkNative);
+    descriptor: (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V
+    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=0, locals=1, args_size=1
+         x: return
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0       1     0 source   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+    RuntimeVisibleAnnotations:
+      x: #x()
+        com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+
+  public static void notNativeStaticRedirected();
+    descriptor: ()V
+    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=0, locals=0, args_size=0
+         x: return
+      LineNumberTable:
+    RuntimeVisibleAnnotations:
+      x: #x()
+        com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
 }
 SourceFile: "TinyFrameworkNative_host.java"
 RuntimeVisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
index 2ca723b..41f459a 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-dump.txt
@@ -67,13 +67,44 @@
     java.lang.annotation.Retention(
       value=Ljava/lang/annotation/RetentionPolicy;.CLASS
     )
-## Class: android/hosttest/annotation/HostSideTestNativeSubstitutionClass.class
-  Compiled from "HostSideTestNativeSubstitutionClass.java"
-public interface android.hosttest.annotation.HostSideTestNativeSubstitutionClass extends java.lang.annotation.Annotation
+## Class: android/hosttest/annotation/HostSideTestRedirect.class
+  Compiled from "HostSideTestRedirect.java"
+public interface android.hosttest.annotation.HostSideTestRedirect extends java.lang.annotation.Annotation
   minor version: 0
   major version: 61
   flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
-  this_class: #x                          // android/hosttest/annotation/HostSideTestNativeSubstitutionClass
+  this_class: #x                          // android/hosttest/annotation/HostSideTestRedirect
+  super_class: #x                         // java/lang/Object
+  interfaces: 1, fields: 0, methods: 1, attributes: 2
+  private static {};
+    descriptor: ()V
+    flags: (0x000a) ACC_PRIVATE, ACC_STATIC
+    Code:
+      stack=2, locals=0, args_size=0
+         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestRedirect
+         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
+         x: return
+}
+SourceFile: "HostSideTestRedirect.java"
+RuntimeVisibleAnnotations:
+  x: #x()
+    com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+  x: #x(#x=[e#x.#x])
+    java.lang.annotation.Target(
+      value=[Ljava/lang/annotation/ElementType;.METHOD]
+    )
+  x: #x(#x=e#x.#x)
+    java.lang.annotation.Retention(
+      value=Ljava/lang/annotation/RetentionPolicy;.CLASS
+    )
+## Class: android/hosttest/annotation/HostSideTestRedirectionClass.class
+  Compiled from "HostSideTestRedirectionClass.java"
+public interface android.hosttest.annotation.HostSideTestRedirectionClass extends java.lang.annotation.Annotation
+  minor version: 0
+  major version: 61
+  flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
+  this_class: #x                          // android/hosttest/annotation/HostSideTestRedirectionClass
   super_class: #x                         // java/lang/Object
   interfaces: 1, fields: 0, methods: 2, attributes: 2
   private static {};
@@ -81,7 +112,7 @@
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
     Code:
       stack=2, locals=0, args_size=0
-         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestNativeSubstitutionClass
+         x: ldc           #x                  // class android/hosttest/annotation/HostSideTestRedirectionClass
          x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
          x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.onClassLoaded:(Ljava/lang/Class;Ljava/lang/String;)V
          x: return
@@ -93,7 +124,7 @@
       x: #x()
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
 }
-SourceFile: "HostSideTestNativeSubstitutionClass.java"
+SourceFile: "HostSideTestRedirectionClass.java"
 RuntimeVisibleAnnotations:
   x: #x()
     com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
@@ -2612,7 +2643,7 @@
   flags: (0x0021) ACC_PUBLIC, ACC_SUPER
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
   super_class: #x                         // java/lang/Object
-  interfaces: 0, fields: 1, methods: 13, attributes: 3
+  interfaces: 0, fields: 1, methods: 15, attributes: 3
   int value;
     descriptor: I
     flags: (0x0000)
@@ -2669,6 +2700,9 @@
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
       x: #x()
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
 
   public static int nativeAddTwo_should_be_like_this(int);
     descriptor: (I)I
@@ -2710,6 +2744,9 @@
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
       x: #x()
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
 
   public static long nativeLongPlus_should_be_like_this(long, long);
     descriptor: (JJ)J
@@ -2776,6 +2813,9 @@
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
       x: #x()
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
 
   public int nativeNonStaticAddToValue_should_be_like_this(int);
     descriptor: (I)I
@@ -2831,9 +2871,6 @@
     RuntimeVisibleAnnotations:
       x: #x()
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
-    RuntimeInvisibleAnnotations:
-      x: #x()
-        android.hosttest.annotation.HostSideTestKeep
 
   public static void nativeStillNotSupported_should_be_like_this();
     descriptor: ()V
@@ -2873,6 +2910,52 @@
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
       x: #x()
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
+
+  public void notNativeRedirected();
+    descriptor: ()V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=4, locals=1, args_size=1
+         x: ldc           #x                  // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
+         x: ldc           #x                 // String notNativeRedirected
+         x: ldc           #x                 // String ()V
+         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+        x: aload_0
+        x: invokestatic  #x                // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.notNativeRedirected:(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V
+        x: return
+    RuntimeVisibleAnnotations:
+      x: #x()
+        com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
+      x: #x()
+        com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
+
+  public static void notNativeStaticRedirected();
+    descriptor: ()V
+    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=4, locals=0, args_size=0
+         x: ldc           #x                  // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
+         x: ldc           #x                // String notNativeStaticRedirected
+         x: ldc           #x                 // String ()V
+         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+        x: invokestatic  #x                // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.notNativeStaticRedirected:()V
+        x: return
+    RuntimeVisibleAnnotations:
+      x: #x()
+        com.android.hoststubgen.hosthelper.HostStubGenProcessedAsSubstitute
+      x: #x()
+        com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+    RuntimeInvisibleAnnotations:
+      x: #x()
+        android.hosttest.annotation.HostSideTestRedirect
 }
 SourceFile: "TinyFrameworkNative.java"
 RuntimeVisibleAnnotations:
@@ -2882,7 +2965,7 @@
   x: #x()
     android.hosttest.annotation.HostSideTestWholeClassKeep
   x: #x(#x=s#x)
-    android.hosttest.annotation.HostSideTestNativeSubstitutionClass(
+    android.hosttest.annotation.HostSideTestRedirectionClass(
       value="TinyFrameworkNative_host"
     )
 ## Class: com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.class
@@ -2893,7 +2976,7 @@
   flags: (0x0021) ACC_PUBLIC, ACC_SUPER
   this_class: #x                          // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
   super_class: #x                         // java/lang/Object
-  interfaces: 0, fields: 0, methods: 6, attributes: 3
+  interfaces: 0, fields: 0, methods: 8, attributes: 3
   private static {};
     descriptor: ()V
     flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -3017,6 +3100,41 @@
     RuntimeVisibleAnnotations:
       x: #x()
         com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+
+  public static void notNativeRedirected(com.android.hoststubgen.test.tinyframework.TinyFrameworkNative);
+    descriptor: (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V
+    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=4, locals=1, args_size=1
+         x: ldc           #x                  // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
+         x: ldc           #x                 // String notNativeRedirected
+         x: ldc           #x                 // String (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;)V
+         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+        x: return
+      LineNumberTable:
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+           11       1     0 source   Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+    RuntimeVisibleAnnotations:
+      x: #x()
+        com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
+
+  public static void notNativeStaticRedirected();
+    descriptor: ()V
+    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=4, locals=0, args_size=0
+         x: ldc           #x                  // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
+         x: ldc           #x                 // String notNativeStaticRedirected
+         x: ldc           #x                 // String ()V
+         x: ldc           #x                 // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+         x: invokestatic  #x                 // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+        x: return
+      LineNumberTable:
+    RuntimeVisibleAnnotations:
+      x: #x()
+        com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
 }
 SourceFile: "TinyFrameworkNative_host.java"
 RuntimeVisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java
index 73b5e2f..04a551c 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java
@@ -15,20 +15,23 @@
  */
 package com.android.hoststubgen.test.tinyframework;
 
-import android.hosttest.annotation.HostSideTestKeep;
-import android.hosttest.annotation.HostSideTestNativeSubstitutionClass;
+import android.hosttest.annotation.HostSideTestRedirect;
+import android.hosttest.annotation.HostSideTestRedirectionClass;
 import android.hosttest.annotation.HostSideTestThrow;
 import android.hosttest.annotation.HostSideTestWholeClassKeep;
 
 @HostSideTestWholeClassKeep
-@HostSideTestNativeSubstitutionClass("TinyFrameworkNative_host")
+@HostSideTestRedirectionClass("TinyFrameworkNative_host")
 public class TinyFrameworkNative {
+
+    @HostSideTestRedirect
     public static native int nativeAddTwo(int arg);
 
     public static int nativeAddTwo_should_be_like_this(int arg) {
         return TinyFrameworkNative_host.nativeAddTwo(arg);
     }
 
+    @HostSideTestRedirect
     public static native long nativeLongPlus(long arg1, long arg2);
 
     public static long nativeLongPlus_should_be_like_this(long arg1, long arg2) {
@@ -41,6 +44,7 @@
         this.value = v;
     }
 
+    @HostSideTestRedirect
     public native int nativeNonStaticAddToValue(int arg);
 
     public int nativeNonStaticAddToValue_should_be_like_this(int arg) {
@@ -50,12 +54,22 @@
     @HostSideTestThrow
     public static native void nativeStillNotSupported();
 
-    @HostSideTestKeep
     public static native void nativeStillKeep();
 
     public static void nativeStillNotSupported_should_be_like_this() {
         throw new RuntimeException();
     }
 
+    @HostSideTestRedirect
     public static native byte nativeBytePlus(byte arg1, byte arg2);
+
+    @HostSideTestRedirect
+    public void notNativeRedirected() {
+        throw new RuntimeException();
+    }
+
+    @HostSideTestRedirect
+    public static void notNativeStaticRedirected() {
+        throw new RuntimeException();
+    }
 }
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java
index b23c216..c7a29a1 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java
@@ -17,8 +17,6 @@
 
 import android.hosttest.annotation.HostSideTestWholeClassKeep;
 
-// TODO: This annotation shouldn't be needed.
-// We should infer it from HostSideTestNativeSubstitutionClass.
 @HostSideTestWholeClassKeep
 public class TinyFrameworkNative_host {
     public static int nativeAddTwo(int arg) {
@@ -38,4 +36,10 @@
     public static byte nativeBytePlus(byte arg1, byte arg2) {
         return (byte) (arg1 + arg2);
     }
+
+    public static void notNativeRedirected(TinyFrameworkNative source) {
+    }
+
+    public static void notNativeStaticRedirected() {
+    }
 }
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
index 14229a0..68673dc 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
@@ -164,6 +164,12 @@
     }
 
     @Test
+    public void testNotNativeRedirect() {
+        TinyFrameworkNative.notNativeStaticRedirected();
+        new TinyFrameworkNative().notNativeRedirected();
+    }
+
+    @Test
     public void testExitLog() {
         thrown.expect(RuntimeException.class);
         thrown.expectMessage("Outer exception");
diff --git a/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt b/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
index 24d203f..f5af99e 100644
--- a/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
+++ b/tools/lint/common/src/main/java/com/google/android/lint/aidl/EnforcePermissionUtils.kt
@@ -24,20 +24,31 @@
 import org.jetbrains.uast.UMethod
 
 /**
- * Given a UMethod, determine if this method is the entrypoint to an interface
- * generated by AIDL, returning the interface name if so, otherwise returning
- * null
+ * Given a UMethod, determine if this method is the entrypoint to an interface generated by AIDL,
+ * returning the interface name if so, otherwise returning null.
  */
 fun getContainingAidlInterface(context: JavaContext, node: UMethod): String? {
+    return containingAidlInterfacePsiClass(context, node)?.name
+}
+
+/**
+ * Given a UMethod, determine if this method is the entrypoint to an interface generated by AIDL,
+ * returning the fully qualified interface name if so, otherwise returning null.
+ */
+fun getContainingAidlInterfaceQualified(context: JavaContext, node: UMethod): String? {
+    return containingAidlInterfacePsiClass(context, node)?.qualifiedName
+}
+
+private fun containingAidlInterfacePsiClass(context: JavaContext, node: UMethod): PsiClass? {
     val containingStub = containingStub(context, node) ?: return null
     val superMethod = node.findSuperMethods(containingStub)
     if (superMethod.isEmpty()) return null
-    return containingStub.containingClass?.name
+    return containingStub.containingClass
 }
 
-/* Returns the containing Stub class if any. This is not sufficient to infer
- * that the method itself extends an AIDL generated method. See
- * getContainingAidlInterface for that purpose.
+/**
+ * Returns the containing Stub class if any. This is not sufficient to infer that the method itself
+ * extends an AIDL generated method. See getContainingAidlInterface for that purpose.
  */
 fun containingStub(context: JavaContext, node: UMethod?): PsiClass? {
     var superClass = node?.containingClass?.superClass
@@ -48,7 +59,7 @@
     return null
 }
 
-private fun isStub(context: JavaContext, psiClass: PsiClass?): Boolean {
+fun isStub(context: JavaContext, psiClass: PsiClass?): Boolean {
     if (psiClass == null) return false
     if (psiClass.name != "Stub") return false
     if (!context.evaluator.isStatic(psiClass)) return false
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt
new file mode 100644
index 0000000..8777712
--- /dev/null
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfaces.kt
@@ -0,0 +1,774 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.lint.aidl
+
+/**
+ * The exemptAidlInterfaces set was generated by running ExemptAidlInterfacesGenerator on the
+ * entire source tree. To reproduce the results, run generate-exempt-aidl-interfaces.sh
+ * located in tools/lint/utils.
+ *
+ * TODO: b/363248121 - Use the exemptAidlInterfaces set inside PermissionAnnotationDetector when it
+ * gets migrated to a global lint check.
+ */
+val exemptAidlInterfaces = setOf(
+    "android.accessibilityservice.IAccessibilityServiceConnection",
+    "android.accessibilityservice.IBrailleDisplayConnection",
+    "android.accounts.IAccountAuthenticatorResponse",
+    "android.accounts.IAccountManager",
+    "android.accounts.IAccountManagerResponse",
+    "android.adservices.adid.IAdIdProviderService",
+    "android.adservices.adid.IAdIdService",
+    "android.adservices.adid.IGetAdIdCallback",
+    "android.adservices.adid.IGetAdIdProviderCallback",
+    "android.adservices.adselection.AdSelectionCallback",
+    "android.adservices.adselection.AdSelectionOverrideCallback",
+    "android.adservices.adselection.AdSelectionService",
+    "android.adservices.adselection.GetAdSelectionDataCallback",
+    "android.adservices.adselection.PersistAdSelectionResultCallback",
+    "android.adservices.adselection.ReportImpressionCallback",
+    "android.adservices.adselection.ReportInteractionCallback",
+    "android.adservices.adselection.SetAppInstallAdvertisersCallback",
+    "android.adservices.adselection.UpdateAdCounterHistogramCallback",
+    "android.adservices.appsetid.IAppSetIdProviderService",
+    "android.adservices.appsetid.IAppSetIdService",
+    "android.adservices.appsetid.IGetAppSetIdCallback",
+    "android.adservices.appsetid.IGetAppSetIdProviderCallback",
+    "android.adservices.cobalt.IAdServicesCobaltUploadService",
+    "android.adservices.common.IAdServicesCommonCallback",
+    "android.adservices.common.IAdServicesCommonService",
+    "android.adservices.common.IAdServicesCommonStatesCallback",
+    "android.adservices.common.IEnableAdServicesCallback",
+    "android.adservices.common.IUpdateAdIdCallback",
+    "android.adservices.customaudience.CustomAudienceOverrideCallback",
+    "android.adservices.customaudience.FetchAndJoinCustomAudienceCallback",
+    "android.adservices.customaudience.ICustomAudienceCallback",
+    "android.adservices.customaudience.ICustomAudienceService",
+    "android.adservices.customaudience.ScheduleCustomAudienceUpdateCallback",
+    "android.adservices.extdata.IAdServicesExtDataStorageService",
+    "android.adservices.extdata.IGetAdServicesExtDataCallback",
+    "android.adservices.measurement.IMeasurementApiStatusCallback",
+    "android.adservices.measurement.IMeasurementCallback",
+    "android.adservices.measurement.IMeasurementService",
+    "android.adservices.ondevicepersonalization.aidl.IDataAccessService",
+    "android.adservices.ondevicepersonalization.aidl.IDataAccessServiceCallback",
+    "android.adservices.ondevicepersonalization.aidl.IExecuteCallback",
+    "android.adservices.ondevicepersonalization.aidl.IFederatedComputeCallback",
+    "android.adservices.ondevicepersonalization.aidl.IFederatedComputeService",
+    "android.adservices.ondevicepersonalization.aidl.IIsolatedModelService",
+    "android.adservices.ondevicepersonalization.aidl.IIsolatedModelServiceCallback",
+    "android.adservices.ondevicepersonalization.aidl.IIsolatedService",
+    "android.adservices.ondevicepersonalization.aidl.IIsolatedServiceCallback",
+    "android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationConfigService",
+    "android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationConfigServiceCallback",
+    "android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationDebugService",
+    "android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationManagingService",
+    "android.adservices.ondevicepersonalization.aidl.IRegisterMeasurementEventCallback",
+    "android.adservices.ondevicepersonalization.aidl.IRequestSurfacePackageCallback",
+    "android.adservices.shell.IShellCommand",
+    "android.adservices.shell.IShellCommandCallback",
+    "android.adservices.signals.IProtectedSignalsService",
+    "android.adservices.signals.UpdateSignalsCallback",
+    "android.adservices.topics.IGetTopicsCallback",
+    "android.adservices.topics.ITopicsService",
+    "android.app.admin.IDevicePolicyManager",
+    "android.app.adservices.IAdServicesManager",
+    "android.app.ambientcontext.IAmbientContextManager",
+    "android.app.ambientcontext.IAmbientContextObserver",
+    "android.app.appsearch.aidl.IAppFunctionService",
+    "android.app.appsearch.aidl.IAppSearchBatchResultCallback",
+    "android.app.appsearch.aidl.IAppSearchManager",
+    "android.app.appsearch.aidl.IAppSearchObserverProxy",
+    "android.app.appsearch.aidl.IAppSearchResultCallback",
+    "android.app.backup.IBackupCallback",
+    "android.app.backup.IBackupManager",
+    "android.app.backup.IRestoreSession",
+    "android.app.blob.IBlobCommitCallback",
+    "android.app.blob.IBlobStoreManager",
+    "android.app.blob.IBlobStoreSession",
+    "android.app.contentsuggestions.IContentSuggestionsManager",
+    "android.app.contextualsearch.IContextualSearchManager",
+    "android.app.ecm.IEnhancedConfirmationManager",
+    "android.apphibernation.IAppHibernationService",
+    "android.app.IActivityClientController",
+    "android.app.IActivityController",
+    "android.app.IActivityTaskManager",
+    "android.app.IAlarmCompleteListener",
+    "android.app.IAlarmListener",
+    "android.app.IAlarmManager",
+    "android.app.IApplicationThread",
+    "android.app.IAppTask",
+    "android.app.IAppTraceRetriever",
+    "android.app.IAssistDataReceiver",
+    "android.app.IForegroundServiceObserver",
+    "android.app.IGameManagerService",
+    "android.app.IGrammaticalInflectionManager",
+    "android.app.ILocaleManager",
+    "android.app.INotificationManager",
+    "android.app.IParcelFileDescriptorRetriever",
+    "android.app.IProcessObserver",
+    "android.app.ISearchManager",
+    "android.app.IStopUserCallback",
+    "android.app.ITaskStackListener",
+    "android.app.IUiModeManager",
+    "android.app.IUriGrantsManager",
+    "android.app.IUserSwitchObserver",
+    "android.app.IWallpaperManager",
+    "android.app.job.IJobCallback",
+    "android.app.job.IJobScheduler",
+    "android.app.job.IJobService",
+    "android.app.ondeviceintelligence.IDownloadCallback",
+    "android.app.ondeviceintelligence.IFeatureCallback",
+    "android.app.ondeviceintelligence.IFeatureDetailsCallback",
+    "android.app.ondeviceintelligence.IListFeaturesCallback",
+    "android.app.ondeviceintelligence.IOnDeviceIntelligenceManager",
+    "android.app.ondeviceintelligence.IProcessingSignal",
+    "android.app.ondeviceintelligence.IResponseCallback",
+    "android.app.ondeviceintelligence.IStreamingResponseCallback",
+    "android.app.ondeviceintelligence.ITokenInfoCallback",
+    "android.app.people.IPeopleManager",
+    "android.app.pinner.IPinnerService",
+    "android.app.prediction.IPredictionManager",
+    "android.app.role.IOnRoleHoldersChangedListener",
+    "android.app.role.IRoleController",
+    "android.app.role.IRoleManager",
+    "android.app.sdksandbox.ILoadSdkCallback",
+    "android.app.sdksandbox.IRequestSurfacePackageCallback",
+    "android.app.sdksandbox.ISdkSandboxManager",
+    "android.app.sdksandbox.ISdkSandboxProcessDeathCallback",
+    "android.app.sdksandbox.ISdkToServiceCallback",
+    "android.app.sdksandbox.ISharedPreferencesSyncCallback",
+    "android.app.sdksandbox.IUnloadSdkCallback",
+    "android.app.sdksandbox.testutils.testscenario.ISdkSandboxTestExecutor",
+    "android.app.search.ISearchUiManager",
+    "android.app.slice.ISliceManager",
+    "android.app.smartspace.ISmartspaceManager",
+    "android.app.timedetector.ITimeDetectorService",
+    "android.app.timezonedetector.ITimeZoneDetectorService",
+    "android.app.trust.ITrustManager",
+    "android.app.usage.IStorageStatsManager",
+    "android.app.usage.IUsageStatsManager",
+    "android.app.wallpapereffectsgeneration.IWallpaperEffectsGenerationManager",
+    "android.app.wearable.IWearableSensingCallback",
+    "android.app.wearable.IWearableSensingManager",
+    "android.bluetooth.IBluetooth",
+    "android.bluetooth.IBluetoothA2dp",
+    "android.bluetooth.IBluetoothA2dpSink",
+    "android.bluetooth.IBluetoothActivityEnergyInfoListener",
+    "android.bluetooth.IBluetoothAvrcpController",
+    "android.bluetooth.IBluetoothCallback",
+    "android.bluetooth.IBluetoothConnectionCallback",
+    "android.bluetooth.IBluetoothCsipSetCoordinator",
+    "android.bluetooth.IBluetoothCsipSetCoordinatorLockCallback",
+    "android.bluetooth.IBluetoothGatt",
+    "android.bluetooth.IBluetoothGattCallback",
+    "android.bluetooth.IBluetoothGattServerCallback",
+    "android.bluetooth.IBluetoothHapClient",
+    "android.bluetooth.IBluetoothHapClientCallback",
+    "android.bluetooth.IBluetoothHeadset",
+    "android.bluetooth.IBluetoothHeadsetClient",
+    "android.bluetooth.IBluetoothHearingAid",
+    "android.bluetooth.IBluetoothHidDevice",
+    "android.bluetooth.IBluetoothHidDeviceCallback",
+    "android.bluetooth.IBluetoothHidHost",
+    "android.bluetooth.IBluetoothLeAudio",
+    "android.bluetooth.IBluetoothLeAudioCallback",
+    "android.bluetooth.IBluetoothLeBroadcastAssistant",
+    "android.bluetooth.IBluetoothLeBroadcastAssistantCallback",
+    "android.bluetooth.IBluetoothLeBroadcastCallback",
+    "android.bluetooth.IBluetoothLeCallControl",
+    "android.bluetooth.IBluetoothLeCallControlCallback",
+    "android.bluetooth.IBluetoothManager",
+    "android.bluetooth.IBluetoothManagerCallback",
+    "android.bluetooth.IBluetoothMap",
+    "android.bluetooth.IBluetoothMapClient",
+    "android.bluetooth.IBluetoothMcpServiceManager",
+    "android.bluetooth.IBluetoothMetadataListener",
+    "android.bluetooth.IBluetoothOobDataCallback",
+    "android.bluetooth.IBluetoothPan",
+    "android.bluetooth.IBluetoothPanCallback",
+    "android.bluetooth.IBluetoothPbap",
+    "android.bluetooth.IBluetoothPbapClient",
+    "android.bluetooth.IBluetoothPreferredAudioProfilesCallback",
+    "android.bluetooth.IBluetoothQualityReportReadyCallback",
+    "android.bluetooth.IBluetoothSap",
+    "android.bluetooth.IBluetoothScan",
+    "android.bluetooth.IBluetoothSocketManager",
+    "android.bluetooth.IBluetoothVolumeControl",
+    "android.bluetooth.IBluetoothVolumeControlCallback",
+    "android.bluetooth.le.IAdvertisingSetCallback",
+    "android.bluetooth.le.IDistanceMeasurementCallback",
+    "android.bluetooth.le.IPeriodicAdvertisingCallback",
+    "android.bluetooth.le.IScannerCallback",
+    "android.companion.ICompanionDeviceManager",
+    "android.companion.IOnMessageReceivedListener",
+    "android.companion.IOnTransportsChangedListener",
+    "android.companion.virtualcamera.IVirtualCameraCallback",
+    "android.companion.virtual.IVirtualDevice",
+    "android.companion.virtual.IVirtualDeviceManager",
+    "android.companion.virtualnative.IVirtualDeviceManagerNative",
+    "android.content.IClipboard",
+    "android.content.IContentService",
+    "android.content.IIntentReceiver",
+    "android.content.IIntentSender",
+    "android.content.integrity.IAppIntegrityManager",
+    "android.content.IRestrictionsManager",
+    "android.content.ISyncAdapterUnsyncableAccountCallback",
+    "android.content.ISyncContext",
+    "android.content.om.IOverlayManager",
+    "android.content.pm.dex.IArtManager",
+    "android.content.pm.dex.ISnapshotRuntimeProfileCallback",
+    "android.content.pm.IBackgroundInstallControlService",
+    "android.content.pm.ICrossProfileApps",
+    "android.content.pm.IDataLoaderManager",
+    "android.content.pm.IDataLoaderStatusListener",
+    "android.content.pm.ILauncherApps",
+    "android.content.pm.IOnChecksumsReadyListener",
+    "android.content.pm.IOtaDexopt",
+    "android.content.pm.IPackageDataObserver",
+    "android.content.pm.IPackageDeleteObserver",
+    "android.content.pm.IPackageInstaller",
+    "android.content.pm.IPackageInstallerSession",
+    "android.content.pm.IPackageInstallerSessionFileSystemConnector",
+    "android.content.pm.IPackageInstallObserver2",
+    "android.content.pm.IPackageLoadingProgressCallback",
+    "android.content.pm.IPackageManager",
+    "android.content.pm.IPackageManagerNative",
+    "android.content.pm.IPackageMoveObserver",
+    "android.content.pm.IPinItemRequest",
+    "android.content.pm.IShortcutService",
+    "android.content.pm.IStagedApexObserver",
+    "android.content.pm.verify.domain.IDomainVerificationManager",
+    "android.content.res.IResourcesManager",
+    "android.content.rollback.IRollbackManager",
+    "android.credentials.ICredentialManager",
+    "android.debug.IAdbTransport",
+    "android.devicelock.IDeviceLockService",
+    "android.devicelock.IGetDeviceIdCallback",
+    "android.devicelock.IGetKioskAppsCallback",
+    "android.devicelock.IIsDeviceLockedCallback",
+    "android.devicelock.IVoidResultCallback",
+    "android.federatedcompute.aidl.IExampleStoreCallback",
+    "android.federatedcompute.aidl.IExampleStoreIterator",
+    "android.federatedcompute.aidl.IExampleStoreIteratorCallback",
+    "android.federatedcompute.aidl.IExampleStoreService",
+    "android.federatedcompute.aidl.IFederatedComputeCallback",
+    "android.federatedcompute.aidl.IFederatedComputeService",
+    "android.federatedcompute.aidl.IResultHandlingService",
+    "android.flags.IFeatureFlags",
+    "android.frameworks.location.altitude.IAltitudeService",
+    "android.frameworks.vibrator.IVibratorController",
+    "android.frameworks.vibrator.IVibratorControlService",
+    "android.gsi.IGsiServiceCallback",
+    "android.hardware.biometrics.AuthenticationStateListener",
+    "android.hardware.biometrics.common.ICancellationSignal",
+    "android.hardware.biometrics.face.IFace",
+    "android.hardware.biometrics.face.ISession",
+    "android.hardware.biometrics.face.ISessionCallback",
+    "android.hardware.biometrics.fingerprint.IFingerprint",
+    "android.hardware.biometrics.fingerprint.ISession",
+    "android.hardware.biometrics.fingerprint.ISessionCallback",
+    "android.hardware.biometrics.IAuthService",
+    "android.hardware.biometrics.IBiometricAuthenticator",
+    "android.hardware.biometrics.IBiometricContextListener",
+    "android.hardware.biometrics.IBiometricSensorReceiver",
+    "android.hardware.biometrics.IBiometricService",
+    "android.hardware.biometrics.IBiometricStateListener",
+    "android.hardware.biometrics.IBiometricSysuiReceiver",
+    "android.hardware.biometrics.IInvalidationCallback",
+    "android.hardware.biometrics.ITestSession",
+    "android.hardware.broadcastradio.IAnnouncementListener",
+    "android.hardware.broadcastradio.ITunerCallback",
+    "android.hardware.contexthub.IContextHubCallback",
+    "android.hardware.devicestate.IDeviceStateManager",
+    "android.hardware.display.IColorDisplayManager",
+    "android.hardware.display.IDisplayManager",
+    "android.hardware.face.IFaceAuthenticatorsRegisteredCallback",
+    "android.hardware.face.IFaceService",
+    "android.hardware.face.IFaceServiceReceiver",
+    "android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback",
+    "android.hardware.fingerprint.IFingerprintClientActiveCallback",
+    "android.hardware.fingerprint.IFingerprintService",
+    "android.hardware.fingerprint.IFingerprintServiceReceiver",
+    "android.hardware.fingerprint.IUdfpsOverlayControllerCallback",
+    "android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback",
+    "android.hardware.hdmi.IHdmiControlCallback",
+    "android.hardware.hdmi.IHdmiControlService",
+    "android.hardware.hdmi.IHdmiDeviceEventListener",
+    "android.hardware.hdmi.IHdmiHotplugEventListener",
+    "android.hardware.hdmi.IHdmiSystemAudioModeChangeListener",
+    "android.hardware.health.IHealthInfoCallback",
+    "android.hardware.ICameraServiceProxy",
+    "android.hardware.IConsumerIrService",
+    "android.hardware.input.IInputManager",
+    "android.hardware.iris.IIrisService",
+    "android.hardware.ISensorPrivacyManager",
+    "android.hardware.ISerialManager",
+    "android.hardware.lights.ILightsManager",
+    "android.hardware.location.IContextHubClient",
+    "android.hardware.location.IContextHubClientCallback",
+    "android.hardware.location.IContextHubService",
+    "android.hardware.location.IContextHubTransactionCallback",
+    "android.hardware.location.ISignificantPlaceProviderManager",
+    "android.hardware.radio.IAnnouncementListener",
+    "android.hardware.radio.ICloseHandle",
+    "android.hardware.radio.ims.media.IImsMedia",
+    "android.hardware.radio.ims.media.IImsMediaListener",
+    "android.hardware.radio.ims.media.IImsMediaSession",
+    "android.hardware.radio.ims.media.IImsMediaSessionListener",
+    "android.hardware.radio.IRadioService",
+    "android.hardware.radio.ITuner",
+    "android.hardware.radio.sap.ISapCallback",
+    "android.hardware.soundtrigger3.ISoundTriggerHw",
+    "android.hardware.soundtrigger3.ISoundTriggerHwCallback",
+    "android.hardware.soundtrigger3.ISoundTriggerHwGlobalCallback",
+    "android.hardware.soundtrigger.IRecognitionStatusCallback",
+    "android.hardware.tetheroffload.ITetheringOffloadCallback",
+    "android.hardware.thermal.IThermalChangedCallback",
+    "android.hardware.tv.hdmi.cec.IHdmiCecCallback",
+    "android.hardware.tv.hdmi.connection.IHdmiConnectionCallback",
+    "android.hardware.tv.hdmi.earc.IEArcCallback",
+    "android.hardware.usb.gadget.IUsbGadgetCallback",
+    "android.hardware.usb.IUsbCallback",
+    "android.hardware.usb.IUsbManager",
+    "android.hardware.usb.IUsbSerialReader",
+    "android.hardware.wifi.hostapd.IHostapdCallback",
+    "android.hardware.wifi.IWifiChipEventCallback",
+    "android.hardware.wifi.IWifiEventCallback",
+    "android.hardware.wifi.IWifiNanIfaceEventCallback",
+    "android.hardware.wifi.IWifiRttControllerEventCallback",
+    "android.hardware.wifi.IWifiStaIfaceEventCallback",
+    "android.hardware.wifi.supplicant.INonStandardCertCallback",
+    "android.hardware.wifi.supplicant.ISupplicantP2pIfaceCallback",
+    "android.hardware.wifi.supplicant.ISupplicantStaIfaceCallback",
+    "android.hardware.wifi.supplicant.ISupplicantStaNetworkCallback",
+    "android.health.connect.aidl.IAccessLogsResponseCallback",
+    "android.health.connect.aidl.IActivityDatesResponseCallback",
+    "android.health.connect.aidl.IAggregateRecordsResponseCallback",
+    "android.health.connect.aidl.IApplicationInfoResponseCallback",
+    "android.health.connect.aidl.IChangeLogsResponseCallback",
+    "android.health.connect.aidl.IDataStagingFinishedCallback",
+    "android.health.connect.aidl.IEmptyResponseCallback",
+    "android.health.connect.aidl.IGetChangeLogTokenCallback",
+    "android.health.connect.aidl.IGetHealthConnectDataStateCallback",
+    "android.health.connect.aidl.IGetHealthConnectMigrationUiStateCallback",
+    "android.health.connect.aidl.IGetPriorityResponseCallback",
+    "android.health.connect.aidl.IHealthConnectService",
+    "android.health.connect.aidl.IInsertRecordsResponseCallback",
+    "android.health.connect.aidl.IMedicalDataSourceResponseCallback",
+    "android.health.connect.aidl.IMedicalResourcesResponseCallback",
+    "android.health.connect.aidl.IMigrationCallback",
+    "android.health.connect.aidl.IReadMedicalResourcesResponseCallback",
+    "android.health.connect.aidl.IReadRecordsResponseCallback",
+    "android.health.connect.aidl.IRecordTypeInfoResponseCallback",
+    "android.health.connect.exportimport.IImportStatusCallback",
+    "android.health.connect.exportimport.IQueryDocumentProvidersCallback",
+    "android.health.connect.exportimport.IScheduledExportStatusCallback",
+    "android.location.ICountryDetector",
+    "android.location.IGpsGeofenceHardware",
+    "android.location.ILocationManager",
+    "android.location.provider.ILocationProviderManager",
+    "android.media.IAudioRoutesObserver",
+    "android.media.IMediaCommunicationService",
+    "android.media.IMediaCommunicationServiceCallback",
+    "android.media.IMediaController2",
+    "android.media.IMediaRoute2ProviderServiceCallback",
+    "android.media.IMediaRouterService",
+    "android.media.IMediaSession2",
+    "android.media.IMediaSession2Service",
+    "android.media.INativeSpatializerCallback",
+    "android.media.IPlaybackConfigDispatcher",
+    "android.media.IRecordingConfigDispatcher",
+    "android.media.IRemoteDisplayCallback",
+    "android.media.ISoundDoseCallback",
+    "android.media.ISpatializerHeadTrackingCallback",
+    "android.media.ITranscodingClientCallback",
+    "android.media.metrics.IMediaMetricsManager",
+    "android.media.midi.IMidiManager",
+    "android.media.musicrecognition.IMusicRecognitionAttributionTagCallback",
+    "android.media.musicrecognition.IMusicRecognitionManager",
+    "android.media.musicrecognition.IMusicRecognitionServiceCallback",
+    "android.media.projection.IMediaProjection",
+    "android.media.projection.IMediaProjectionCallback",
+    "android.media.projection.IMediaProjectionManager",
+    "android.media.projection.IMediaProjectionWatcherCallback",
+    "android.media.session.ISession",
+    "android.media.session.ISessionController",
+    "android.media.session.ISessionManager",
+    "android.media.soundtrigger.ISoundTriggerDetectionServiceClient",
+    "android.media.soundtrigger_middleware.IInjectGlobalEvent",
+    "android.media.soundtrigger_middleware.IInjectModelEvent",
+    "android.media.soundtrigger_middleware.IInjectRecognitionEvent",
+    "android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService",
+    "android.media.soundtrigger_middleware.ISoundTriggerModule",
+    "android.media.tv.ad.ITvAdManager",
+    "android.media.tv.ad.ITvAdSessionCallback",
+    "android.media.tv.interactive.ITvInteractiveAppManager",
+    "android.media.tv.interactive.ITvInteractiveAppServiceCallback",
+    "android.media.tv.interactive.ITvInteractiveAppSessionCallback",
+    "android.media.tv.ITvInputHardware",
+    "android.media.tv.ITvInputManager",
+    "android.media.tv.ITvInputServiceCallback",
+    "android.media.tv.ITvInputSessionCallback",
+    "android.media.tv.ITvRemoteServiceInput",
+    "android.nearby.aidl.IOffloadCallback",
+    "android.nearby.IBroadcastListener",
+    "android.nearby.INearbyManager",
+    "android.nearby.IScanListener",
+    "android.net.connectivity.aidl.ConnectivityNative",
+    "android.net.dhcp.IDhcpEventCallbacks",
+    "android.net.dhcp.IDhcpServer",
+    "android.net.dhcp.IDhcpServerCallbacks",
+    "android.net.ICaptivePortal",
+    "android.net.IConnectivityDiagnosticsCallback",
+    "android.net.IConnectivityManager",
+    "android.net.IEthernetManager",
+    "android.net.IEthernetServiceListener",
+    "android.net.IIntResultListener",
+    "android.net.IIpConnectivityMetrics",
+    "android.net.IIpMemoryStore",
+    "android.net.IIpMemoryStoreCallbacks",
+    "android.net.IIpSecService",
+    "android.net.INetdEventCallback",
+    "android.net.INetdUnsolicitedEventListener",
+    "android.net.INetworkActivityListener",
+    "android.net.INetworkAgent",
+    "android.net.INetworkAgentRegistry",
+    "android.net.INetworkInterfaceOutcomeReceiver",
+    "android.net.INetworkManagementEventObserver",
+    "android.net.INetworkMonitor",
+    "android.net.INetworkMonitorCallbacks",
+    "android.net.INetworkOfferCallback",
+    "android.net.INetworkPolicyListener",
+    "android.net.INetworkPolicyManager",
+    "android.net.INetworkScoreService",
+    "android.net.INetworkStackConnector",
+    "android.net.INetworkStackStatusCallback",
+    "android.net.INetworkStatsService",
+    "android.net.INetworkStatsSession",
+    "android.net.IOnCompleteListener",
+    "android.net.IPacProxyManager",
+    "android.net.ip.IIpClient",
+    "android.net.ip.IIpClientCallbacks",
+    "android.net.ipmemorystore.IOnBlobRetrievedListener",
+    "android.net.ipmemorystore.IOnL2KeyResponseListener",
+    "android.net.ipmemorystore.IOnNetworkAttributesRetrievedListener",
+    "android.net.ipmemorystore.IOnSameL3NetworkResponseListener",
+    "android.net.ipmemorystore.IOnStatusAndCountListener",
+    "android.net.ipmemorystore.IOnStatusListener",
+    "android.net.IQosCallback",
+    "android.net.ISocketKeepaliveCallback",
+    "android.net.ITestNetworkManager",
+    "android.net.ITetheredInterfaceCallback",
+    "android.net.ITetheringConnector",
+    "android.net.ITetheringEventCallback",
+    "android.net.IVpnManager",
+    "android.net.mdns.aidl.IMDnsEventListener",
+    "android.net.metrics.INetdEventListener",
+    "android.net.netstats.IUsageCallback",
+    "android.net.netstats.provider.INetworkStatsProvider",
+    "android.net.netstats.provider.INetworkStatsProviderCallback",
+    "android.net.nsd.INsdManager",
+    "android.net.nsd.INsdManagerCallback",
+    "android.net.nsd.INsdServiceConnector",
+    "android.net.nsd.IOffloadEngine",
+    "android.net.resolv.aidl.IDnsResolverUnsolicitedEventListener",
+    "android.net.thread.IActiveOperationalDatasetReceiver",
+    "android.net.thread.IConfigurationReceiver",
+    "android.net.thread.IOperationalDatasetCallback",
+    "android.net.thread.IOperationReceiver",
+    "android.net.thread.IStateCallback",
+    "android.net.thread.IThreadNetworkController",
+    "android.net.thread.IThreadNetworkManager",
+    "android.net.vcn.IVcnManagementService",
+    "android.net.wear.ICompanionDeviceManagerProxy",
+    "android.net.wifi.aware.IWifiAwareDiscoverySessionCallback",
+    "android.net.wifi.aware.IWifiAwareEventCallback",
+    "android.net.wifi.aware.IWifiAwareMacAddressProvider",
+    "android.net.wifi.aware.IWifiAwareManager",
+    "android.net.wifi.hotspot2.IProvisioningCallback",
+    "android.net.wifi.IActionListener",
+    "android.net.wifi.IBooleanListener",
+    "android.net.wifi.IByteArrayListener",
+    "android.net.wifi.ICoexCallback",
+    "android.net.wifi.IDppCallback",
+    "android.net.wifi.IIntegerListener",
+    "android.net.wifi.IInterfaceCreationInfoCallback",
+    "android.net.wifi.ILastCallerListener",
+    "android.net.wifi.IListListener",
+    "android.net.wifi.ILocalOnlyConnectionStatusListener",
+    "android.net.wifi.ILocalOnlyHotspotCallback",
+    "android.net.wifi.IMacAddressListListener",
+    "android.net.wifi.IMapListener",
+    "android.net.wifi.INetworkRequestMatchCallback",
+    "android.net.wifi.INetworkRequestUserSelectionCallback",
+    "android.net.wifi.IOnWifiActivityEnergyInfoListener",
+    "android.net.wifi.IOnWifiDriverCountryCodeChangedListener",
+    "android.net.wifi.IOnWifiUsabilityStatsListener",
+    "android.net.wifi.IPnoScanResultsCallback",
+    "android.net.wifi.IScanDataListener",
+    "android.net.wifi.IScanResultsCallback",
+    "android.net.wifi.IScoreUpdateObserver",
+    "android.net.wifi.ISoftApCallback",
+    "android.net.wifi.IStringListener",
+    "android.net.wifi.ISubsystemRestartCallback",
+    "android.net.wifi.ISuggestionConnectionStatusListener",
+    "android.net.wifi.ISuggestionUserApprovalStatusListener",
+    "android.net.wifi.ITrafficStateCallback",
+    "android.net.wifi.ITwtCallback",
+    "android.net.wifi.ITwtCapabilitiesListener",
+    "android.net.wifi.ITwtStatsListener",
+    "android.net.wifi.IWifiBandsListener",
+    "android.net.wifi.IWifiConnectedNetworkScorer",
+    "android.net.wifi.IWifiLowLatencyLockListener",
+    "android.net.wifi.IWifiManager",
+    "android.net.wifi.IWifiNetworkSelectionConfigListener",
+    "android.net.wifi.IWifiNetworkStateChangedListener",
+    "android.net.wifi.IWifiScanner",
+    "android.net.wifi.IWifiScannerListener",
+    "android.net.wifi.IWifiVerboseLoggingStatusChangedListener",
+    "android.net.wifi.p2p.IWifiP2pListener",
+    "android.net.wifi.p2p.IWifiP2pManager",
+    "android.net.wifi.rtt.IRttCallback",
+    "android.net.wifi.rtt.IWifiRttManager",
+    "android.ondevicepersonalization.IOnDevicePersonalizationSystemService",
+    "android.ondevicepersonalization.IOnDevicePersonalizationSystemServiceCallback",
+    "android.os.IBatteryPropertiesRegistrar",
+    "android.os.ICancellationSignal",
+    "android.os.IDeviceIdentifiersPolicyService",
+    "android.os.IDeviceIdleController",
+    "android.os.IDumpstate",
+    "android.os.IDumpstateListener",
+    "android.os.IExternalVibratorService",
+    "android.os.IHardwarePropertiesManager",
+    "android.os.IHintManager",
+    "android.os.IHintSession",
+    "android.os.IIncidentCompanion",
+    "android.os.image.IDynamicSystemService",
+    "android.os.incremental.IStorageHealthListener",
+    "android.os.INetworkManagementService",
+    "android.os.IPendingIntentRef",
+    "android.os.IPowerStatsService",
+    "android.os.IProfilingResultCallback",
+    "android.os.IProfilingService",
+    "android.os.IProgressListener",
+    "android.os.IPullAtomCallback",
+    "android.os.IRecoverySystem",
+    "android.os.IRemoteCallback",
+    "android.os.ISecurityStateManager",
+    "android.os.IServiceCallback",
+    "android.os.IStatsCompanionService",
+    "android.os.IStatsManagerService",
+    "android.os.IStatsQueryCallback",
+    "android.os.ISystemConfig",
+    "android.os.ISystemUpdateManager",
+    "android.os.IThermalEventListener",
+    "android.os.IUpdateLock",
+    "android.os.IUserManager",
+    "android.os.IUserRestrictionsListener",
+    "android.os.IVibratorManagerService",
+    "android.os.IVoldListener",
+    "android.os.IVoldMountCallback",
+    "android.os.IVoldTaskListener",
+    "android.os.logcat.ILogcatManagerService",
+    "android.permission.ILegacyPermissionManager",
+    "android.permission.IPermissionChecker",
+    "android.permission.IPermissionManager",
+    "android.print.IPrintManager",
+    "android.print.IPrintSpoolerCallbacks",
+    "android.print.IPrintSpoolerClient",
+    "android.printservice.IPrintServiceClient",
+    "android.printservice.recommendation.IRecommendationServiceCallbacks",
+    "android.provider.aidl.IDeviceConfigManager",
+    "android.remoteauth.IDeviceDiscoveryListener",
+    "android.safetycenter.IOnSafetyCenterDataChangedListener",
+    "android.safetycenter.ISafetyCenterManager",
+    "android.scheduling.IRebootReadinessManager",
+    "android.scheduling.IRequestRebootReadinessStatusListener",
+    "android.security.attestationverification.IAttestationVerificationManagerService",
+    "android.security.IFileIntegrityService",
+    "android.security.keystore.IKeyAttestationApplicationIdProvider",
+    "android.security.rkp.IRegistration",
+    "android.security.rkp.IRemoteProvisioning",
+    "android.service.appprediction.IPredictionService",
+    "android.service.assist.classification.IFieldClassificationCallback",
+    "android.service.attention.IAttentionCallback",
+    "android.service.attention.IProximityUpdateCallback",
+    "android.service.autofill.augmented.IFillCallback",
+    "android.service.autofill.IConvertCredentialCallback",
+    "android.service.autofill.IFillCallback",
+    "android.service.autofill.IInlineSuggestionUiCallback",
+    "android.service.autofill.ISaveCallback",
+    "android.service.autofill.ISurfacePackageResultCallback",
+    "android.service.contentcapture.IContentCaptureServiceCallback",
+    "android.service.contentcapture.IContentProtectionAllowlistCallback",
+    "android.service.contentcapture.IDataShareCallback",
+    "android.service.credentials.IBeginCreateCredentialCallback",
+    "android.service.credentials.IBeginGetCredentialCallback",
+    "android.service.credentials.IClearCredentialStateCallback",
+    "android.service.dreams.IDreamManager",
+    "android.service.games.IGameServiceController",
+    "android.service.games.IGameSessionController",
+    "android.service.notification.IStatusBarNotificationHolder",
+    "android.service.oemlock.IOemLockService",
+    "android.service.ondeviceintelligence.IProcessingUpdateStatusCallback",
+    "android.service.ondeviceintelligence.IRemoteProcessingService",
+    "android.service.ondeviceintelligence.IRemoteStorageService",
+    "android.service.persistentdata.IPersistentDataBlockService",
+    "android.service.resolver.IResolverRankerResult",
+    "android.service.rotationresolver.IRotationResolverCallback",
+    "android.service.textclassifier.ITextClassifierCallback",
+    "android.service.textclassifier.ITextClassifierService",
+    "android.service.timezone.ITimeZoneProviderManager",
+    "android.service.trust.ITrustAgentServiceCallback",
+    "android.service.voice.IDetectorSessionStorageService",
+    "android.service.voice.IDetectorSessionVisualQueryDetectionCallback",
+    "android.service.voice.IDspHotwordDetectionCallback",
+    "android.service.wallpaper.IWallpaperConnection",
+    "android.speech.IRecognitionListener",
+    "android.speech.IRecognitionService",
+    "android.speech.IRecognitionServiceManager",
+    "android.speech.tts.ITextToSpeechManager",
+    "android.speech.tts.ITextToSpeechSession",
+    "android.system.composd.ICompilationTaskCallback",
+    "android.system.virtualizationmaintenance.IVirtualizationReconciliationCallback",
+    "android.system.virtualizationservice.IVirtualMachineCallback",
+    "android.system.vmtethering.IVmTethering",
+    "android.telephony.imsmedia.IImsAudioSession",
+    "android.telephony.imsmedia.IImsAudioSessionCallback",
+    "android.telephony.imsmedia.IImsMedia",
+    "android.telephony.imsmedia.IImsMediaCallback",
+    "android.telephony.imsmedia.IImsTextSession",
+    "android.telephony.imsmedia.IImsTextSessionCallback",
+    "android.telephony.imsmedia.IImsVideoSession",
+    "android.telephony.imsmedia.IImsVideoSessionCallback",
+    "android.tracing.ITracingServiceProxy",
+    "android.uwb.IOnUwbActivityEnergyInfoListener",
+    "android.uwb.IUwbAdapter",
+    "android.uwb.IUwbAdapterStateCallbacks",
+    "android.uwb.IUwbAdfProvisionStateCallbacks",
+    "android.uwb.IUwbOemExtensionCallback",
+    "android.uwb.IUwbRangingCallbacks",
+    "android.uwb.IUwbVendorUciCallback",
+    "android.view.accessibility.IAccessibilityInteractionConnectionCallback",
+    "android.view.accessibility.IAccessibilityManager",
+    "android.view.accessibility.IMagnificationConnectionCallback",
+    "android.view.accessibility.IRemoteMagnificationAnimationCallback",
+    "android.view.autofill.IAutoFillManager",
+    "android.view.autofill.IAutofillWindowPresenter",
+    "android.view.contentcapture.IContentCaptureManager",
+    "android.view.IDisplayChangeWindowCallback",
+    "android.view.IDisplayWindowListener",
+    "android.view.IInputFilter",
+    "android.view.IInputFilterHost",
+    "android.view.IInputMonitorHost",
+    "android.view.IRecentsAnimationController",
+    "android.view.IRemoteAnimationFinishedCallback",
+    "android.view.ISensitiveContentProtectionManager",
+    "android.view.IWindowId",
+    "android.view.IWindowManager",
+    "android.view.IWindowSession",
+    "android.view.translation.ITranslationManager",
+    "android.view.translation.ITranslationServiceCallback",
+    "android.webkit.IWebViewUpdateService",
+    "android.window.IBackAnimationFinishedCallback",
+    "android.window.IDisplayAreaOrganizerController",
+    "android.window.ITaskFragmentOrganizerController",
+    "android.window.ITaskOrganizerController",
+    "android.window.ITransitionMetricsReporter",
+    "android.window.IUnhandledDragCallback",
+    "android.window.IWindowContainerToken",
+    "android.window.IWindowlessStartingSurfaceCallback",
+    "android.window.IWindowOrganizerController",
+    "androidx.core.uwb.backend.IUwb",
+    "androidx.core.uwb.backend.IUwbClient",
+    "com.android.clockwork.modes.IModeManager",
+    "com.android.clockwork.modes.IStateChangeListener",
+    "com.android.clockwork.power.IWearPowerService",
+    "com.android.devicelockcontroller.IDeviceLockControllerService",
+    "com.android.devicelockcontroller.storage.IGlobalParametersService",
+    "com.android.devicelockcontroller.storage.ISetupParametersService",
+    "com.android.federatedcompute.services.training.aidl.IIsolatedTrainingService",
+    "com.android.federatedcompute.services.training.aidl.ITrainingResultCallback",
+    "com.android.internal.app.IAppOpsActiveCallback",
+    "com.android.internal.app.ILogAccessDialogCallback",
+    "com.android.internal.app.ISoundTriggerService",
+    "com.android.internal.app.ISoundTriggerSession",
+    "com.android.internal.app.IVoiceInteractionAccessibilitySettingsListener",
+    "com.android.internal.app.IVoiceInteractionManagerService",
+    "com.android.internal.app.IVoiceInteractionSessionListener",
+    "com.android.internal.app.IVoiceInteractionSessionShowCallback",
+    "com.android.internal.app.IVoiceInteractionSoundTriggerSession",
+    "com.android.internal.app.procstats.IProcessStats",
+    "com.android.internal.appwidget.IAppWidgetService",
+    "com.android.internal.backup.ITransportStatusCallback",
+    "com.android.internal.compat.IOverrideValidator",
+    "com.android.internal.compat.IPlatformCompat",
+    "com.android.internal.compat.IPlatformCompatNative",
+    "com.android.internal.graphics.fonts.IFontManager",
+    "com.android.internal.inputmethod.IAccessibilityInputMethodSessionCallback",
+    "com.android.internal.inputmethod.IConnectionlessHandwritingCallback",
+    "com.android.internal.inputmethod.IImeTracker",
+    "com.android.internal.inputmethod.IInlineSuggestionsRequestCallback",
+    "com.android.internal.inputmethod.IInputContentUriToken",
+    "com.android.internal.inputmethod.IInputMethodPrivilegedOperations",
+    "com.android.internal.inputmethod.IInputMethodSessionCallback",
+    "com.android.internal.net.INetworkWatchlistManager",
+    "com.android.internal.os.IBinaryTransparencyService",
+    "com.android.internal.os.IDropBoxManagerService",
+    "com.android.internal.policy.IKeyguardDismissCallback",
+    "com.android.internal.policy.IKeyguardDrawnCallback",
+    "com.android.internal.policy.IKeyguardExitCallback",
+    "com.android.internal.policy.IKeyguardStateCallback",
+    "com.android.internal.statusbar.IAddTileResultCallback",
+    "com.android.internal.statusbar.ISessionListener",
+    "com.android.internal.statusbar.IStatusBarService",
+    "com.android.internal.telecom.IDeviceIdleControllerAdapter",
+    "com.android.internal.telecom.IInternalServiceRetriever",
+    "com.android.internal.telephony.IMms",
+    "com.android.internal.telephony.ITelephonyRegistry",
+    "com.android.internal.textservice.ISpellCheckerServiceCallback",
+    "com.android.internal.textservice.ITextServicesManager",
+    "com.android.internal.view.IDragAndDropPermissions",
+    "com.android.internal.view.IInputMethodManager",
+    "com.android.internal.view.inline.IInlineContentProvider",
+    "com.android.internal.widget.ILockSettings",
+    "com.android.net.IProxyPortListener",
+    "com.android.net.module.util.IRoutingCoordinator",
+    "com.android.ondevicepersonalization.libraries.plugin.internal.IPluginCallback",
+    "com.android.ondevicepersonalization.libraries.plugin.internal.IPluginExecutorService",
+    "com.android.ondevicepersonalization.libraries.plugin.internal.IPluginStateCallback",
+    "com.android.rkpdapp.IGetKeyCallback",
+    "com.android.rkpdapp.IGetRegistrationCallback",
+    "com.android.rkpdapp.IRegistration",
+    "com.android.rkpdapp.IRemoteProvisioning",
+    "com.android.rkpdapp.IStoreUpgradedKeyCallback",
+    "com.android.sdksandbox.IComputeSdkStorageCallback",
+    "com.android.sdksandbox.ILoadSdkInSandboxCallback",
+    "com.android.sdksandbox.IRequestSurfacePackageFromSdkCallback",
+    "com.android.sdksandbox.ISdkSandboxManagerToSdkSandboxCallback",
+    "com.android.sdksandbox.ISdkSandboxService",
+    "com.android.sdksandbox.IUnloadSdkInSandboxCallback",
+    "com.android.server.profcollect.IProviderStatusCallback",
+    "com.android.server.thread.openthread.IChannelMasksReceiver",
+    "com.android.server.thread.openthread.INsdPublisher",
+    "com.android.server.thread.openthread.IOtDaemonCallback",
+    "com.android.server.thread.openthread.IOtStatusReceiver",
+    "com.google.android.clockwork.ambient.offload.IDisplayOffloadService",
+    "com.google.android.clockwork.ambient.offload.IDisplayOffloadTransitionFinishedCallbacks",
+    "com.google.android.clockwork.healthservices.IHealthService",
+    "vendor.google_clockwork.healthservices.IHealthServicesCallback",
+)
diff --git a/tools/lint/utils/README.md b/tools/lint/utils/README.md
new file mode 100644
index 0000000..b5583c5
--- /dev/null
+++ b/tools/lint/utils/README.md
@@ -0,0 +1,11 @@
+# Utility Android Lint Checks for AOSP
+
+This directory contains scripts that execute utility Android Lint Checks for AOSP, specifically:
+* `enforce_permission_counter.py`: Provides statistics regarding the percentage of annotated/not
+  annotated `AIDL` methods with `@EnforcePermission` annotations.
+* `generate-exempt-aidl-interfaces.sh`: Provides a list of all `AIDL` interfaces in the entire
+  source tree.
+
+When adding a new utility Android Lint check to this directory, consider adding any utility or
+data processing tool you might require. Make sure that your contribution is documented in this
+README file.
diff --git a/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt b/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt
index fa61c42..9842881 100644
--- a/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt
+++ b/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt
@@ -19,6 +19,7 @@
 import com.android.tools.lint.client.api.IssueRegistry
 import com.android.tools.lint.client.api.Vendor
 import com.android.tools.lint.detector.api.CURRENT_API
+import com.google.android.lint.aidl.ExemptAidlInterfacesGenerator
 import com.google.android.lint.aidl.AnnotatedAidlCounter
 import com.google.auto.service.AutoService
 
@@ -27,6 +28,7 @@
 class AndroidUtilsIssueRegistry : IssueRegistry() {
     override val issues = listOf(
         AnnotatedAidlCounter.ISSUE_ANNOTATED_AIDL_COUNTER,
+        ExemptAidlInterfacesGenerator.ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES,
     )
 
     override val api: Int
@@ -38,6 +40,6 @@
     override val vendor: Vendor = Vendor(
         vendorName = "Android",
         feedbackUrl = "http://b/issues/new?component=315013",
-        contact = "tweek@google.com"
+        contact = "android-platform-abuse-prevention-withfriends@google.com"
     )
 }
diff --git a/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfacesGenerator.kt b/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfacesGenerator.kt
new file mode 100644
index 0000000..6ad223c
--- /dev/null
+++ b/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfacesGenerator.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.lint.aidl
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Context
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UMethod
+
+/**
+ * Generates a set of fully qualified AIDL Interface names present in the entire source tree with
+ * the following requirement: their implementations have to be inside directories whose path
+ * prefixes match `systemServicePathPrefixes`.
+ */
+class ExemptAidlInterfacesGenerator : AidlImplementationDetector() {
+    private val targetExemptAidlInterfaceNames = mutableSetOf<String>()
+    private val systemServicePathPrefixes = setOf(
+        "frameworks/base/services",
+        "frameworks/base/apex",
+        "frameworks/opt/wear",
+        "packages/modules"
+    )
+
+    // We could've improved performance by visiting classes rather than methods, however, this lint
+    // check won't be run regularly, hence we've decided not to add extra overrides to
+    // AidlImplementationDetector.
+    override fun visitAidlMethod(
+        context: JavaContext,
+        node: UMethod,
+        interfaceName: String,
+        body: UBlockExpression
+    ) {
+        val filePath = context.file.path
+
+        // We perform `filePath.contains` instead of `filePath.startsWith` since getting the
+        // relative path of a source file is non-trivial. That is because `context.file.path`
+        // returns the path to where soong builds the file (i.e. /out/soong/...). Moreover, the
+        // logic to extract the relative path would need to consider several /out/soong/...
+        // locations patterns.
+        if (systemServicePathPrefixes.none { filePath.contains(it) }) return
+
+        val fullyQualifiedInterfaceName =
+            getContainingAidlInterfaceQualified(context, node) ?: return
+
+        targetExemptAidlInterfaceNames.add("\"$fullyQualifiedInterfaceName\",")
+    }
+
+    override fun afterCheckEachProject(context: Context) {
+        if (targetExemptAidlInterfaceNames.isEmpty()) return
+
+        val message = targetExemptAidlInterfaceNames.joinToString("\n")
+
+        context.report(
+            ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES,
+            context.getLocation(context.project.dir),
+            "\n" + message + "\n",
+        )
+    }
+
+    companion object {
+        @JvmField
+        val ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES = Issue.create(
+            id = "PermissionAnnotationExemptAidlInterfaces",
+            briefDescription = "Returns a set of all AIDL interfaces",
+            explanation = """
+                Produces the exemptAidlInterfaces set used by PermissionAnnotationDetector
+            """.trimIndent(),
+            category = Category.SECURITY,
+            priority = 5,
+            severity = Severity.INFORMATIONAL,
+            implementation = Implementation(
+                ExemptAidlInterfacesGenerator::class.java,
+                Scope.JAVA_FILE_SCOPE
+            )
+        )
+    }
+}
diff --git a/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/ExemptAidlInterfacesGeneratorTest.kt b/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/ExemptAidlInterfacesGeneratorTest.kt
new file mode 100644
index 0000000..9a17bb4
--- /dev/null
+++ b/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/ExemptAidlInterfacesGeneratorTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.lint.aidl
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+class ExemptAidlInterfacesGeneratorTest : LintDetectorTest() {
+    override fun getDetector(): Detector = ExemptAidlInterfacesGenerator()
+
+    override fun getIssues(): List<Issue> = listOf(
+        ExemptAidlInterfacesGenerator.ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES,
+    )
+
+    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+    fun testMultipleAidlInterfacesImplemented() {
+        lint()
+            .files(
+                java(
+                    createVisitedPath("TestClass1.java"),
+                    """
+                        package com.android.server;
+                        public class TestClass1 extends IFoo.Stub {
+                            public void testMethod() {}
+                        }
+                    """
+                )
+                    .indented(),
+                java(
+                    createVisitedPath("TestClass2.java"),
+                    """
+                        package com.android.server;
+                        public class TestClass2 extends IBar.Stub {
+                            public void testMethod() {}
+                        }
+                    """
+                )
+                    .indented(),
+                *stubs,
+            )
+            .run()
+            .expect(
+                """
+                    app: Information: "IFoo",
+                    "IBar", [PermissionAnnotationExemptAidlInterfaces]
+                    0 errors, 0 warnings
+                """
+            )
+    }
+
+    fun testSingleAidlInterfaceRepeated() {
+        lint()
+            .files(
+                java(
+                    createVisitedPath("TestClass1.java"),
+                    """
+                        package com.android.server;
+                        public class TestClass1 extends IFoo.Stub {
+                            public void testMethod() {}
+                        }
+                    """
+                )
+                    .indented(),
+                java(
+                    createVisitedPath("TestClass2.java"),
+                    """
+                        package com.android.server;
+                        public class TestClass2 extends IFoo.Stub {
+                            public void testMethod() {}
+                        }
+                    """
+                )
+                    .indented(),
+                *stubs,
+            )
+            .run()
+            .expect(
+                """
+                    app: Information: "IFoo", [PermissionAnnotationExemptAidlInterfaces]
+                    0 errors, 0 warnings
+                """
+            )
+    }
+
+    fun testAnonymousClassExtendsAidlStub() {
+        lint()
+            .files(
+                java(
+                    createVisitedPath("TestClass.java"),
+                    """
+                        package com.android.server;
+                        public class TestClass {
+                            private IBinder aidlImpl = new IFoo.Stub() {
+                                public void testMethod() {}
+                            };
+                        }
+                        """
+                )
+                    .indented(),
+                *stubs,
+            )
+            .run()
+            .expect(
+                """
+                    app: Information: "IFoo", [PermissionAnnotationExemptAidlInterfaces]
+                    0 errors, 0 warnings
+                """
+            )
+    }
+
+    fun testNoAidlInterfacesImplemented() {
+        lint()
+            .files(
+                java(
+                    createVisitedPath("TestClass.java"),
+                    """
+                        package com.android.server;
+                        public class TestClass {
+                            public void testMethod() {}
+                        }
+                    """
+                )
+                    .indented(),
+                *stubs
+            )
+            .run()
+            .expectClean()
+    }
+
+    fun testAidlInterfaceImplementedInIgnoredDirectory() {
+        lint()
+            .files(
+                java(
+                    ignoredPath,
+                    """
+                        package com.android.server;
+                        public class TestClass1 extends IFoo.Stub {
+                            public void testMethod() {}
+                        }
+                    """
+                )
+                    .indented(),
+                *stubs,
+            )
+            .run()
+            .expectClean()
+    }
+
+    private val interfaceIFoo: TestFile = java(
+        """
+            public interface IFoo extends android.os.IInterface {
+                public static abstract class Stub extends android.os.Binder implements IFoo {}
+                public void testMethod();
+            }
+        """
+    ).indented()
+
+    private val interfaceIBar: TestFile = java(
+        """
+            public interface IBar extends android.os.IInterface {
+                public static abstract class Stub extends android.os.Binder implements IBar {}
+                public void testMethod();
+            }
+        """
+    ).indented()
+
+    private val stubs = arrayOf(interfaceIFoo, interfaceIBar)
+
+    private fun createVisitedPath(filename: String) =
+        "src/frameworks/base/services/java/com/android/server/$filename"
+
+    private val ignoredPath = "src/test/pkg/TestClass.java"
+}
diff --git a/tools/lint/utils/generate-exempt-aidl-interfaces.sh b/tools/lint/utils/generate-exempt-aidl-interfaces.sh
new file mode 100755
index 0000000..44dcdd7
--- /dev/null
+++ b/tools/lint/utils/generate-exempt-aidl-interfaces.sh
@@ -0,0 +1,59 @@
+#
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Create a directory for the results and a nested temporary directory.
+mkdir -p $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp
+
+# Create a copy of `AndroidGlobalLintChecker.jar` to restore it afterwards.
+cp $ANDROID_BUILD_TOP/prebuilts/cmdline-tools/AndroidGlobalLintChecker.jar \
+    $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/AndroidGlobalLintChecker.jar
+
+# Configure the environment variable required for running the lint check on the entire source tree.
+export ANDROID_LINT_CHECK=PermissionAnnotationExemptAidlInterfaces
+
+# Build the target corresponding to the lint checks present in the `utils` directory.
+m AndroidUtilsLintChecker
+
+# Replace `AndroidGlobalLintChecker.jar` with the newly built `jar` file.
+cp $ANDROID_BUILD_TOP/out/host/linux-x86/framework/AndroidUtilsLintChecker.jar \
+    $ANDROID_BUILD_TOP/prebuilts/cmdline-tools/AndroidGlobalLintChecker.jar;
+
+# Run the lint check on the entire source tree.
+m lint-check
+
+# Copy the archive containing the results of `lint-check` into the temporary directory.
+cp $ANDROID_BUILD_TOP/out/soong/lint-report-text.zip \
+    $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp
+
+cd $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp
+
+# Unzip the archive containing the results of `lint-check`.
+unzip lint-report-text.zip
+
+# Concatenate the results of `lint-check` into a single string.
+concatenated_reports=$(find . -type f | xargs cat)
+
+# Extract the fully qualified names of the AIDL Interfaces from the concatenated results. Output
+# this list into `out/soong/exempt_aidl_interfaces_generator_output/exempt_aidl_interfaces`.
+echo $concatenated_reports | grep -Eo '\"([a-zA-Z0-9_]*\.)+[a-zA-Z0-9_]*\",' | sort | uniq > ../exempt_aidl_interfaces
+
+# Remove the temporary directory.
+rm -rf $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp
+
+# Restore the original copy of `AndroidGlobalLintChecker.jar` and delete the copy.
+cp $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/AndroidGlobalLintChecker.jar \
+    $ANDROID_BUILD_TOP/prebuilts/cmdline-tools/AndroidGlobalLintChecker.jar
+rm $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/AndroidGlobalLintChecker.jar