Merge "Add multi-client support in camera2" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index 59dc314..2056056 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -336,7 +336,7 @@
     field public static final String WRITE_SECURE_SETTINGS = "android.permission.WRITE_SECURE_SETTINGS";
     field public static final String WRITE_SETTINGS = "android.permission.WRITE_SETTINGS";
     field public static final String WRITE_SYNC_SETTINGS = "android.permission.WRITE_SYNC_SETTINGS";
-    field @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final String WRITE_SYSTEM_PREFERENCES = "android.permission.WRITE_SYSTEM_PREFERENCES";
+    field @FlaggedApi("com.android.settingslib.flags.write_system_preference_permission_enabled") public static final String WRITE_SYSTEM_PREFERENCES = "android.permission.WRITE_SYSTEM_PREFERENCES";
     field public static final String WRITE_VOICEMAIL = "com.android.voicemail.permission.WRITE_VOICEMAIL";
   }
 
@@ -17497,6 +17497,25 @@
     method public void setIntUniform(@NonNull String, @NonNull int[]);
   }
 
+  @FlaggedApi("com.android.graphics.hwui.flags.runtime_color_filters_blenders") public class RuntimeXfermode extends android.graphics.Xfermode {
+    ctor public RuntimeXfermode(@NonNull String);
+    method public void setColorUniform(@NonNull String, @ColorInt int);
+    method public void setColorUniform(@NonNull String, @ColorLong long);
+    method public void setColorUniform(@NonNull String, @NonNull android.graphics.Color);
+    method public void setFloatUniform(@NonNull String, float);
+    method public void setFloatUniform(@NonNull String, float, float);
+    method public void setFloatUniform(@NonNull String, float, float, float);
+    method public void setFloatUniform(@NonNull String, float, float, float, float);
+    method public void setFloatUniform(@NonNull String, @NonNull float[]);
+    method public void setInputColorFilter(@NonNull String, @NonNull android.graphics.ColorFilter);
+    method public void setInputShader(@NonNull String, @NonNull android.graphics.Shader);
+    method public void setIntUniform(@NonNull String, int);
+    method public void setIntUniform(@NonNull String, int, int);
+    method public void setIntUniform(@NonNull String, int, int, int);
+    method public void setIntUniform(@NonNull String, int, int, int, int);
+    method public void setIntUniform(@NonNull String, @NonNull int[]);
+  }
+
   public class Shader {
     ctor @Deprecated public Shader();
     method public boolean getLocalMatrix(@NonNull android.graphics.Matrix);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 51ba63e..a46f872 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -65,6 +65,7 @@
     field @FlaggedApi("android.crashrecovery.flags.enable_crashrecovery") public static final String BIND_EXPLICIT_HEALTH_CHECK_SERVICE = "android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE";
     field public static final String BIND_EXTERNAL_STORAGE_SERVICE = "android.permission.BIND_EXTERNAL_STORAGE_SERVICE";
     field public static final String BIND_FIELD_CLASSIFICATION_SERVICE = "android.permission.BIND_FIELD_CLASSIFICATION_SERVICE";
+    field @FlaggedApi("android.security.afl_api") public static final String BIND_FORENSIC_EVENT_TRANSPORT_SERVICE = "android.permission.BIND_FORENSIC_EVENT_TRANSPORT_SERVICE";
     field public static final String BIND_GBA_SERVICE = "android.permission.BIND_GBA_SERVICE";
     field public static final String BIND_HOTWORD_DETECTION_SERVICE = "android.permission.BIND_HOTWORD_DETECTION_SERVICE";
     field public static final String BIND_IMS_SERVICE = "android.permission.BIND_IMS_SERVICE";
@@ -209,6 +210,7 @@
     field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String MANAGE_ENHANCED_CONFIRMATION_STATES = "android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES";
     field public static final String MANAGE_ETHERNET_NETWORKS = "android.permission.MANAGE_ETHERNET_NETWORKS";
     field public static final String MANAGE_FACTORY_RESET_PROTECTION = "android.permission.MANAGE_FACTORY_RESET_PROTECTION";
+    field @FlaggedApi("android.security.afl_api") public static final String MANAGE_FORENSIC_STATE = "android.permission.MANAGE_FORENSIC_STATE";
     field public static final String MANAGE_GAME_ACTIVITY = "android.permission.MANAGE_GAME_ACTIVITY";
     field public static final String MANAGE_GAME_MODE = "android.permission.MANAGE_GAME_MODE";
     field @FlaggedApi("android.media.tv.flags.media_quality_fw") public static final String MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE = "android.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE";
@@ -304,6 +306,7 @@
     field public static final String READ_CONTENT_RATING_SYSTEMS = "android.permission.READ_CONTENT_RATING_SYSTEMS";
     field public static final String READ_DEVICE_CONFIG = "android.permission.READ_DEVICE_CONFIG";
     field public static final String READ_DREAM_STATE = "android.permission.READ_DREAM_STATE";
+    field @FlaggedApi("android.security.afl_api") public static final String READ_FORENSIC_STATE = "android.permission.READ_FORENSIC_STATE";
     field public static final String READ_GLOBAL_APP_SEARCH_DATA = "android.permission.READ_GLOBAL_APP_SEARCH_DATA";
     field @FlaggedApi("android.content.pm.get_resolved_apk_path") public static final String READ_INSTALLED_SESSION_PATHS = "android.permission.READ_INSTALLED_SESSION_PATHS";
     field public static final String READ_INSTALL_SESSIONS = "android.permission.READ_INSTALL_SESSIONS";
@@ -5211,6 +5214,28 @@
 
 }
 
+package android.hardware.contexthub {
+
+  @FlaggedApi("android.chre.flags.offload_api") public class HubDiscoveryInfo {
+    method @NonNull public android.hardware.contexthub.HubEndpointInfo getHubEndpointInfo();
+  }
+
+  @FlaggedApi("android.chre.flags.offload_api") public final class HubEndpointInfo implements android.os.Parcelable {
+    method public int describeContents();
+    method @NonNull public android.hardware.contexthub.HubEndpointInfo.HubEndpointIdentifier getIdentifier();
+    method @NonNull public String getName();
+    method @Nullable public String getTag();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.contexthub.HubEndpointInfo> CREATOR;
+  }
+
+  public static class HubEndpointInfo.HubEndpointIdentifier {
+    method public long getEndpoint();
+    method public long getHub();
+  }
+
+}
+
 package android.hardware.devicestate {
 
   @FlaggedApi("android.hardware.devicestate.feature.flags.device_state_property_api") public final class DeviceState {
@@ -6202,6 +6227,7 @@
     method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public android.hardware.location.ContextHubClient createClient(@NonNull android.hardware.location.ContextHubInfo, @NonNull android.app.PendingIntent, long);
     method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public android.hardware.location.ContextHubTransaction<java.lang.Void> disableNanoApp(@NonNull android.hardware.location.ContextHubInfo, long);
     method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public android.hardware.location.ContextHubTransaction<java.lang.Void> enableNanoApp(@NonNull android.hardware.location.ContextHubInfo, long);
+    method @FlaggedApi("android.chre.flags.offload_api") @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public java.util.List<android.hardware.contexthub.HubDiscoveryInfo> findEndpoints(long);
     method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public int[] findNanoAppOnHub(int, @NonNull android.hardware.location.NanoAppFilter);
     method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public int[] getContextHubHandles();
     method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public android.hardware.location.ContextHubInfo getContextHubInfo(int);
@@ -12611,6 +12637,29 @@
 
 }
 
+package android.security.forensic {
+
+  @FlaggedApi("android.security.afl_api") public class ForensicManager {
+    method @RequiresPermission(android.Manifest.permission.READ_FORENSIC_STATE) public void addStateCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_FORENSIC_STATE) public void disable(@NonNull java.util.concurrent.Executor, @NonNull android.security.forensic.ForensicManager.CommandCallback);
+    method @RequiresPermission(android.Manifest.permission.MANAGE_FORENSIC_STATE) public void enable(@NonNull java.util.concurrent.Executor, @NonNull android.security.forensic.ForensicManager.CommandCallback);
+    method @RequiresPermission(android.Manifest.permission.READ_FORENSIC_STATE) public void removeStateCallback(@NonNull java.util.function.Consumer<java.lang.Integer>);
+    field public static final int ERROR_DATA_SOURCE_UNAVAILABLE = 4; // 0x4
+    field public static final int ERROR_PERMISSION_DENIED = 1; // 0x1
+    field public static final int ERROR_TRANSPORT_UNAVAILABLE = 3; // 0x3
+    field public static final int ERROR_UNKNOWN = 0; // 0x0
+    field public static final int STATE_DISABLED = 1; // 0x1
+    field public static final int STATE_ENABLED = 2; // 0x2
+    field public static final int STATE_UNKNOWN = 0; // 0x0
+  }
+
+  public static interface ForensicManager.CommandCallback {
+    method public void onFailure(int);
+    method public void onSuccess();
+  }
+
+}
+
 package android.security.keystore {
 
   public class AndroidKeyStoreProvider extends java.security.Provider {
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 8b37dbd..6c03b32 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1624,9 +1624,13 @@
     /** @hide Access to read oxygen saturation. */
     public static final int OP_READ_OXYGEN_SATURATION = AppOpEnums.APP_OP_READ_OXYGEN_SATURATION;
 
+    /** @hide Access to write system preferences. */
+    public static final int OP_WRITE_SYSTEM_PREFERENCES =
+            AppOpEnums.APP_OP_WRITE_SYSTEM_PREFERENCES;
+
     /** @hide */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public static final int _NUM_OP = 153;
+    public static final int _NUM_OP = 154;
 
     /**
      * All app ops represented as strings.
@@ -1783,6 +1787,7 @@
             OPSTR_READ_SKIN_TEMPERATURE,
             OPSTR_RANGING,
             OPSTR_READ_OXYGEN_SATURATION,
+            OPSTR_WRITE_SYSTEM_PREFERENCES,
     })
     public @interface AppOpString {}
 
@@ -2540,6 +2545,9 @@
     @FlaggedApi(Flags.FLAG_RANGING_PERMISSION_ENABLED)
     public static final String OPSTR_RANGING = "android:ranging";
 
+    /** @hide Access to system preferences write services */
+    public static final String OPSTR_WRITE_SYSTEM_PREFERENCES = "android:write_system_preferences";
+
     /** {@link #sAppOpsToNote} not initialized yet for this op */
     private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0;
     /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */
@@ -2656,6 +2664,7 @@
             OP_RECEIVE_SANDBOX_TRIGGER_AUDIO,
             OP_MEDIA_ROUTING_CONTROL,
             OP_READ_SYSTEM_GRAMMATICAL_GENDER,
+            OP_WRITE_SYSTEM_PREFERENCES,
     };
 
     @SuppressWarnings("FlaggedApi")
@@ -3144,6 +3153,10 @@
                 Flags.replaceBodySensorPermissionEnabled()
                     ? HealthPermissions.READ_OXYGEN_SATURATION : null)
             .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_WRITE_SYSTEM_PREFERENCES, OPSTR_WRITE_SYSTEM_PREFERENCES,
+            "WRITE_SYSTEM_PREFERENCES").setPermission(
+                     com.android.settingslib.flags.Flags.writeSystemPreferencePermissionEnabled()
+                     ? Manifest.permission.WRITE_SYSTEM_PREFERENCES : null).build(),
     };
 
     // The number of longs needed to form a full bitmask of app ops
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index ee0c38c..8b8fdef 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -239,6 +239,8 @@
 import android.security.advancedprotection.IAdvancedProtectionService;
 import android.security.attestationverification.AttestationVerificationManager;
 import android.security.attestationverification.IAttestationVerificationManagerService;
+import android.security.forensic.ForensicManager;
+import android.security.forensic.IForensicService;
 import android.security.keystore.KeyStoreManager;
 import android.service.oemlock.IOemLockService;
 import android.service.oemlock.OemLockManager;
@@ -1793,6 +1795,18 @@
                     }
                 });
 
+        registerService(Context.FORENSIC_SERVICE, ForensicManager.class,
+                new CachedServiceFetcher<ForensicManager>() {
+                    @Override
+                    public ForensicManager createService(ContextImpl ctx)
+                            throws ServiceNotFoundException {
+                        IBinder b = ServiceManager.getServiceOrThrow(
+                                Context.FORENSIC_SERVICE);
+                        IForensicService service = IForensicService.Stub.asInterface(b);
+                        return new ForensicManager(service);
+                    }
+                });
+
         sInitializing = true;
         try {
             // Note: the following functions need to be @SystemApis, once they become mainline
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java b/core/java/android/app/supervision/SupervisionManagerInternal.java
similarity index 84%
rename from services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
rename to core/java/android/app/supervision/SupervisionManagerInternal.java
index 5df9dd5..d571e14 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
+++ b/core/java/android/app/supervision/SupervisionManagerInternal.java
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.server.supervision;
+package android.app.supervision;
 
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
-import android.os.Bundle;
+import android.os.PersistableBundle;
 
 /**
  * Local system service interface for {@link SupervisionService}.
@@ -35,6 +35,11 @@
     public abstract boolean isSupervisionEnabledForUser(@UserIdInt int userId);
 
     /**
+     * Returns whether the supervision lock screen needs to be shown.
+     */
+    public abstract boolean isSupervisionLockscreenEnabledForUser(@UserIdInt int userId);
+
+    /**
      * Set whether supervision is enabled for the specified user.
      *
      * @param userId The user to set the supervision state for
@@ -50,5 +55,5 @@
      * @param options Optional configuration parameters for the supervision lock screen
      */
     public abstract void setSupervisionLockscreenEnabledForUser(
-            @UserIdInt int userId, boolean enabled, @Nullable Bundle options);
+            @UserIdInt int userId, boolean enabled, @Nullable PersistableBundle options);
 }
diff --git a/core/java/android/hardware/contexthub/HubDiscoveryInfo.java b/core/java/android/hardware/contexthub/HubDiscoveryInfo.java
new file mode 100644
index 0000000..875c4b4
--- /dev/null
+++ b/core/java/android/hardware/contexthub/HubDiscoveryInfo.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.contexthub;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.chre.flags.Flags;
+import android.hardware.location.ContextHubManager;
+
+/**
+ * Class that represents the result of from an hub endpoint discovery.
+ *
+ * <p>The type is returned from an endpoint discovery query via {@link
+ * ContextHubManager#findEndpoints}. Application may use the values {@link #getHubEndpointInfo} to
+ * retrieve the {@link HubEndpointInfo} that describes the endpoint that matches the query. The
+ * class provides flexibility in returning more information (e.g. service provided by the endpoint)
+ * in addition to the information about the endpoint.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_OFFLOAD_API)
+public class HubDiscoveryInfo {
+    // TODO(b/375487784): Add ServiceInfo to the result.
+    android.hardware.contexthub.HubEndpointInfo mEndpointInfo;
+
+    /**
+     * Constructor for internal use.
+     *
+     * @hide
+     */
+    public HubDiscoveryInfo(android.hardware.contexthub.HubEndpointInfo endpointInfo) {
+        mEndpointInfo = endpointInfo;
+    }
+
+    /** Get the {@link android.hardware.contexthub.HubEndpointInfo} for the endpoint found. */
+    @NonNull
+    public HubEndpointInfo getHubEndpointInfo() {
+        return mEndpointInfo;
+    }
+}
diff --git a/core/java/android/hardware/contexthub/HubEndpointInfo.aidl b/core/java/android/hardware/contexthub/HubEndpointInfo.aidl
new file mode 100644
index 0000000..025b2b1
--- /dev/null
+++ b/core/java/android/hardware/contexthub/HubEndpointInfo.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.contexthub;
+
+/** @hide */
+parcelable HubEndpointInfo;
diff --git a/core/java/android/hardware/contexthub/HubEndpointInfo.java b/core/java/android/hardware/contexthub/HubEndpointInfo.java
new file mode 100644
index 0000000..c17fc00
--- /dev/null
+++ b/core/java/android/hardware/contexthub/HubEndpointInfo.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.contexthub;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.chre.flags.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Parcelable representing an endpoint from ContextHub or VendorHub.
+ *
+ * <p>HubEndpointInfo contains information about an endpoint, including its name, tag and other
+ * information. A HubEndpointInfo object can be used to accurately identify a specific endpoint.
+ * Application can use this object to identify and describe an endpoint.
+ *
+ * <p>See: {@link android.hardware.location.ContextHubManager#findEndpoints} for how to retrieve
+ * {@link HubEndpointInfo} for endpoints on a hub.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_OFFLOAD_API)
+public final class HubEndpointInfo implements Parcelable {
+    /**
+     * A unique identifier for one endpoint. A unique identifier for one endpoint consists of two
+     * parts: (1) a unique long number for a hub and (2) a long number for the endpoint, unique
+     * within a hub. This class overrides equality methods and can be used to compare if two
+     * endpoints are the same.
+     */
+    public static class HubEndpointIdentifier {
+        private final long mEndpointId;
+        private final long mHubId;
+
+        /** @hide */
+        public HubEndpointIdentifier(long hubId, long endpointId) {
+            mEndpointId = endpointId;
+            mHubId = hubId;
+        }
+
+        /** @hide */
+        public HubEndpointIdentifier(android.hardware.contexthub.EndpointId halEndpointId) {
+            mEndpointId = halEndpointId.id;
+            mHubId = halEndpointId.hubId;
+        }
+
+        /** Get the endpoint portion of the identifier. */
+        public long getEndpoint() {
+            return mEndpointId;
+        }
+
+        /** Get the hub portion of the identifier. */
+        public long getHub() {
+            return mHubId;
+        }
+
+        /**
+         * Create an invalid endpoint id, to represent endpoint that are not yet registered with the
+         * HAL.
+         *
+         * @hide
+         */
+        public static HubEndpointIdentifier invalid() {
+            return new HubEndpointIdentifier(
+                    android.hardware.contexthub.HubInfo.HUB_ID_INVALID,
+                    android.hardware.contexthub.EndpointId.ENDPOINT_ID_INVALID);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mEndpointId, mHubId);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof HubEndpointIdentifier other)) {
+                return false;
+            }
+            if (other.mHubId != mHubId) {
+                return false;
+            }
+            return other.mEndpointId == mEndpointId;
+        }
+    }
+
+    private final HubEndpointIdentifier mId;
+    private final String mName;
+    @Nullable private final String mTag;
+
+    // TODO(b/375487784): Add Service/version and other information to this object
+
+    /** @hide */
+    public HubEndpointInfo(android.hardware.contexthub.EndpointInfo endpointInfo) {
+        mId = new HubEndpointIdentifier(endpointInfo.id.hubId, endpointInfo.id.id);
+        mName = endpointInfo.name;
+        mTag = endpointInfo.tag;
+    }
+
+    private HubEndpointInfo(Parcel in) {
+        long hubId = in.readLong();
+        long endpointId = in.readLong();
+        mName = in.readString();
+        mTag = in.readString();
+
+        mId = new HubEndpointIdentifier(hubId, endpointId);
+    }
+
+    /** Parcel implementation details */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Parcel implementation details */
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeLong(mId.getHub());
+        dest.writeLong(mId.getEndpoint());
+        dest.writeString(mName);
+        dest.writeString(mTag);
+    }
+
+    /** Get a unique identifier for this endpoint. */
+    @NonNull
+    public HubEndpointIdentifier getIdentifier() {
+        return mId;
+    }
+
+    /** Get the human-readable name of this endpoint (for debugging purposes). */
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    /**
+     * Get the tag that further identifies the submodule that created this endpoint. For example, a
+     * single application could provide multiple endpoints. These endpoints will share the same
+     * name, but will have different tags. This tag can be used to identify the submodule within the
+     * application that provided the endpoint.
+     */
+    @Nullable
+    public String getTag() {
+        return mTag;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder out = new StringBuilder();
+        out.append("Endpoint [0x");
+        out.append(Long.toHexString(mId.getEndpoint()));
+        out.append("@ Hub 0x");
+        out.append(Long.toHexString(mId.getHub()));
+        out.append("] Name=");
+        out.append(mName);
+        out.append(", Tag=");
+        out.append(mTag);
+        return out.toString();
+    }
+
+    public static final @android.annotation.NonNull Creator<HubEndpointInfo> CREATOR =
+            new Creator<>() {
+                public HubEndpointInfo createFromParcel(Parcel in) {
+                    return new HubEndpointInfo(in);
+                }
+
+                public HubEndpointInfo[] newArray(int size) {
+                    return new HubEndpointInfo[size];
+                }
+            };
+}
diff --git a/core/java/android/hardware/contexthub/OWNERS b/core/java/android/hardware/contexthub/OWNERS
new file mode 100644
index 0000000..a65a2bf
--- /dev/null
+++ b/core/java/android/hardware/contexthub/OWNERS
@@ -0,0 +1,2 @@
+# ContextHub team
+file:platform/system/chre:/OWNERS
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 494bfc9..e009c2f 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -34,6 +34,8 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.hardware.contexthub.ErrorCode;
+import android.hardware.contexthub.HubDiscoveryInfo;
+import android.hardware.contexthub.HubEndpointInfo;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.Looper;
@@ -42,6 +44,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
@@ -679,6 +682,29 @@
     }
 
     /**
+     * Find a list of endpoints that matches a specific ID.
+     *
+     * @param endpointId Statically generated ID for an endpoint.
+     * @return A list of {@link HubDiscoveryInfo} objects that represents the result of discovery.
+     */
+    @FlaggedApi(Flags.FLAG_OFFLOAD_API)
+    @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+    @NonNull
+    public List<HubDiscoveryInfo> findEndpoints(long endpointId) {
+        try {
+            List<HubEndpointInfo> endpointInfos = mService.findEndpoints(endpointId);
+            List<HubDiscoveryInfo> results = new ArrayList<>(endpointInfos.size());
+            // Wrap with result type
+            for (HubEndpointInfo endpointInfo : endpointInfos) {
+                results.add(new HubDiscoveryInfo(endpointInfo));
+            }
+            return results;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Set a callback to receive messages from the context hub
      *
      * @param callback Callback object
diff --git a/core/java/android/hardware/location/IContextHubService.aidl b/core/java/android/hardware/location/IContextHubService.aidl
index b0cc763..fc6a70a 100644
--- a/core/java/android/hardware/location/IContextHubService.aidl
+++ b/core/java/android/hardware/location/IContextHubService.aidl
@@ -18,17 +18,18 @@
 
 // Declare any non-default types here with import statements
 import android.app.PendingIntent;
-import android.hardware.location.HubInfo;
+import android.hardware.contexthub.HubEndpointInfo;
 import android.hardware.location.ContextHubInfo;
 import android.hardware.location.ContextHubMessage;
-import android.hardware.location.NanoApp;
-import android.hardware.location.NanoAppBinary;
-import android.hardware.location.NanoAppFilter;
-import android.hardware.location.NanoAppInstanceInfo;
+import android.hardware.location.HubInfo;
 import android.hardware.location.IContextHubCallback;
 import android.hardware.location.IContextHubClient;
 import android.hardware.location.IContextHubClientCallback;
 import android.hardware.location.IContextHubTransactionCallback;
+import android.hardware.location.NanoApp;
+import android.hardware.location.NanoAppBinary;
+import android.hardware.location.NanoAppFilter;
+import android.hardware.location.NanoAppInstanceInfo;
 
 /**
  * @hide
@@ -122,4 +123,8 @@
     // Enables or disables test mode
     @EnforcePermission("ACCESS_CONTEXT_HUB")
     boolean setTestMode(in boolean enable);
+
+    // Finds all endpoints that havea specific ID
+    @EnforcePermission("ACCESS_CONTEXT_HUB")
+    List<HubEndpointInfo> findEndpoints(long endpointId);
 }
diff --git a/core/java/android/security/forensic/ForensicManager.java b/core/java/android/security/forensic/ForensicManager.java
new file mode 100644
index 0000000..9126182
--- /dev/null
+++ b/core/java/android/security/forensic/ForensicManager.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.forensic;
+
+import static android.Manifest.permission.MANAGE_FORENSIC_STATE;
+import static android.Manifest.permission.READ_FORENSIC_STATE;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.RemoteException;
+import android.security.Flags;
+import android.util.Log;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+/**
+ * ForensicManager manages the forensic logging on Android devices.
+ * Upon user consent, forensic logging collects various device events for
+ * off-device investigation of potential device compromise.
+ * <p>
+ * Forensic logging can either be enabled ({@link #STATE_ENABLED}
+ * or disabled ({@link #STATE_DISABLED}).
+ * <p>
+ * The Forensic logs will be transferred to
+ * {@link android.security.forensic.ForensicEventTransport}.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_AFL_API)
+@SystemService(Context.FORENSIC_SERVICE)
+public class ForensicManager {
+    private static final String TAG = "ForensicManager";
+
+    /** @hide */
+    @Target(ElementType.TYPE_USE)
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "STATE_" }, value = {
+            STATE_UNKNOWN,
+            STATE_DISABLED,
+            STATE_ENABLED
+    })
+    public @interface ForensicState {}
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "ERROR_" }, value = {
+            ERROR_UNKNOWN,
+            ERROR_PERMISSION_DENIED,
+            ERROR_TRANSPORT_UNAVAILABLE,
+            ERROR_DATA_SOURCE_UNAVAILABLE
+    })
+    public @interface ForensicError {}
+
+    /**
+     * Indicates an unknown state
+     */
+    public static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN;
+
+    /**
+     * Indicates an state that the forensic is turned off.
+     */
+    public static final int STATE_DISABLED = IForensicServiceStateCallback.State.DISABLED;
+
+    /**
+     * Indicates an state that the forensic is turned on.
+     */
+    public static final int STATE_ENABLED = IForensicServiceStateCallback.State.ENABLED;
+
+    /**
+     * Indicates an unknown error
+     */
+    public static final int ERROR_UNKNOWN = IForensicServiceCommandCallback.ErrorCode.UNKNOWN;
+
+    /**
+     * Indicates an error due to insufficient access rights.
+     */
+    public static final int ERROR_PERMISSION_DENIED =
+            IForensicServiceCommandCallback.ErrorCode.PERMISSION_DENIED;
+
+    /**
+     * Indicates an error due to unavailability of the forensic event transport.
+     */
+    public static final int ERROR_TRANSPORT_UNAVAILABLE =
+            IForensicServiceCommandCallback.ErrorCode.TRANSPORT_UNAVAILABLE;
+
+    /**
+     * Indicates an error due to unavailability of the data source.
+     */
+    public static final int ERROR_DATA_SOURCE_UNAVAILABLE =
+            IForensicServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE;
+
+
+    private final IForensicService mService;
+
+    private final ConcurrentHashMap<Consumer<Integer>, IForensicServiceStateCallback>
+            mStateCallbacks = new ConcurrentHashMap<>();
+
+    /**
+     * Constructor
+     *
+     * @param service A valid instance of IForensicService.
+     * @hide
+     */
+    public ForensicManager(IForensicService service) {
+        mService = service;
+    }
+
+    /**
+     * Add a callback to monitor the state of the ForensicService.
+     *
+     * @param executor The executor through which the callback should be invoked.
+     * @param callback The callback for state change.
+     *                 Once the callback is registered, the callback will be called
+     *                 to reflect the init state.
+     *                 The callback can be registered only once.
+     */
+    @RequiresPermission(READ_FORENSIC_STATE)
+    public void addStateCallback(@NonNull @CallbackExecutor Executor executor,
+            @NonNull @ForensicState Consumer<Integer> callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
+        if (mStateCallbacks.get(callback) != null) {
+            Log.d(TAG, "addStateCallback callback already present");
+            return;
+        }
+
+        final IForensicServiceStateCallback wrappedCallback =
+                new IForensicServiceStateCallback.Stub() {
+                    @Override
+                    public void onStateChange(int state) {
+                        executor.execute(() -> callback.accept(state));
+                    }
+                };
+        try {
+            mService.addStateCallback(wrappedCallback);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        mStateCallbacks.put(callback, wrappedCallback);
+    }
+
+    /**
+     * Remove a callback to monitor the state of the ForensicService.
+     *
+     * @param callback The callback to remove.
+     */
+    @RequiresPermission(READ_FORENSIC_STATE)
+    public void removeStateCallback(@NonNull Consumer<@ForensicState Integer> callback) {
+        Objects.requireNonNull(callback);
+        if (!mStateCallbacks.containsKey(callback)) {
+            Log.d(TAG, "removeStateCallback callback not present");
+            return;
+        }
+
+        IForensicServiceStateCallback wrappedCallback = mStateCallbacks.get(callback);
+
+        try {
+            mService.removeStateCallback(wrappedCallback);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        mStateCallbacks.remove(callback);
+    }
+
+    /**
+     * Enable forensic logging.
+     * If successful, ForensicService will transition to {@link #STATE_ENABLED} state.
+     * <p>
+     * When forensic logging is enabled, various device events will be collected and
+     * sent over to the registered {@link android.security.forensic.ForensicEventTransport}.
+     *
+     * @param executor The executor through which the callback should be invoked.
+     * @param callback The callback for the command result.
+     */
+    @RequiresPermission(MANAGE_FORENSIC_STATE)
+    public void enable(@NonNull @CallbackExecutor Executor executor,
+            @NonNull CommandCallback callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+        try {
+            mService.enable(new IForensicServiceCommandCallback.Stub() {
+                @Override
+                public void onSuccess() {
+                    executor.execute(callback::onSuccess);
+                }
+
+                @Override
+                public void onFailure(int error) {
+                    executor.execute(() -> callback.onFailure(error));
+                }
+            });
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Disable forensic logging.
+     * If successful, ForensicService will transition to {@link #STATE_DISABLED}.
+     * <p>
+     * When forensic logging is disabled, device events will no longer be collected.
+     * Any events that have been collected but not yet sent to ForensicEventTransport
+     * will be transferred as a final batch.
+     *
+     * @param executor The executor through which the callback should be invoked.
+     * @param callback The callback for the command result.
+     */
+    @RequiresPermission(MANAGE_FORENSIC_STATE)
+    public void disable(@NonNull @CallbackExecutor Executor executor,
+            @NonNull CommandCallback callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+        try {
+            mService.disable(new IForensicServiceCommandCallback.Stub() {
+                @Override
+                public void onSuccess() {
+                    executor.execute(callback::onSuccess);
+                }
+
+                @Override
+                public void onFailure(int error) {
+                    executor.execute(() -> callback.onFailure(error));
+                }
+            });
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Callback used in {@link #enable} and {@link #disable} to indicate the result of the command.
+     */
+    public interface CommandCallback {
+        /**
+         * Called when command succeeds.
+         */
+        void onSuccess();
+
+        /**
+         * Called when command fails.
+         * @param error The error number.
+         */
+        void onFailure(@ForensicError int error);
+    }
+}
diff --git a/core/java/android/security/forensic/IBackupTransport.aidl b/core/java/android/security/forensic/IForensicEventTransport.aidl
similarity index 96%
rename from core/java/android/security/forensic/IBackupTransport.aidl
rename to core/java/android/security/forensic/IForensicEventTransport.aidl
index c2cbc83..80e78eb 100644
--- a/core/java/android/security/forensic/IBackupTransport.aidl
+++ b/core/java/android/security/forensic/IForensicEventTransport.aidl
@@ -20,7 +20,7 @@
 import com.android.internal.infra.AndroidFuture;
 
 /** {@hide} */
-oneway interface IBackupTransport {
+oneway interface IForensicEventTransport {
     /**
      * Initialize the server side.
      */
diff --git a/core/java/android/security/forensic/IForensicService.aidl b/core/java/android/security/forensic/IForensicService.aidl
index a944b18..8039b26 100644
--- a/core/java/android/security/forensic/IForensicService.aidl
+++ b/core/java/android/security/forensic/IForensicService.aidl
@@ -24,9 +24,12 @@
  * @hide
  */
 interface IForensicService {
-    void monitorState(IForensicServiceStateCallback callback);
-    void makeVisible(IForensicServiceCommandCallback callback);
-    void makeInvisible(IForensicServiceCommandCallback callback);
+    @EnforcePermission("READ_FORENSIC_STATE")
+    void addStateCallback(IForensicServiceStateCallback callback);
+    @EnforcePermission("READ_FORENSIC_STATE")
+    void removeStateCallback(IForensicServiceStateCallback callback);
+    @EnforcePermission("MANAGE_FORENSIC_STATE")
     void enable(IForensicServiceCommandCallback callback);
+    @EnforcePermission("MANAGE_FORENSIC_STATE")
     void disable(IForensicServiceCommandCallback callback);
 }
diff --git a/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl b/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl
index 7fa0c7f..6d1456e 100644
--- a/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl
+++ b/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl
@@ -25,8 +25,8 @@
          UNKNOWN = 0,
          PERMISSION_DENIED = 1,
          INVALID_STATE_TRANSITION = 2,
-         BACKUP_TRANSPORT_UNAVAILABLE = 3,
-         DATA_SOURCE_UNAVAILABLE = 3,
+         TRANSPORT_UNAVAILABLE = 3,
+         DATA_SOURCE_UNAVAILABLE = 4,
      }
     void onSuccess();
     void onFailure(ErrorCode error);
diff --git a/core/java/android/security/forensic/IForensicServiceStateCallback.aidl b/core/java/android/security/forensic/IForensicServiceStateCallback.aidl
index 0cda350..1b68c7b 100644
--- a/core/java/android/security/forensic/IForensicServiceStateCallback.aidl
+++ b/core/java/android/security/forensic/IForensicServiceStateCallback.aidl
@@ -23,9 +23,8 @@
     @Backing(type="int")
     enum State{
         UNKNOWN = 0,
-        INVISIBLE = 1,
-        VISIBLE = 2,
-        ENABLED = 3,
+        DISABLED = 1,
+        ENABLED = 2,
     }
     void onStateChange(State state);
  }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index e50662a..19d3dc4 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2516,6 +2516,11 @@
     public void notifyInsetsAnimationRunningStateChanged(boolean running) {
         if (sToolkitSetFrameRateReadOnlyFlagValue) {
             mInsetsAnimationRunning = running;
+            if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+                Trace.instant(Trace.TRACE_TAG_VIEW,
+                        TextUtils.formatSimple("notifyInsetsAnimationRunningStateChanged(%s)",
+                        Boolean.toString(running)));
+            }
         }
     }
 
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 4f924a82..ff69610 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -418,6 +418,17 @@
 }
 
 flag {
+  name: "record_task_snapshots_before_shutdown"
+  namespace: "windowing_frontend"
+  description: "Record task snapshots before shutdown"
+  bug: "376821232"
+  is_fixed_read_only: true
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
     name: "predictive_back_three_button_nav"
     namespace: "systemui"
     description: "Enable Predictive Back Animation for 3-button-nav"
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 450b88b..1925b3a 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -1342,8 +1342,9 @@
     }
 }
 
-static jobject convertDeviceProductInfoToJavaObject(
-        JNIEnv* env, const std::optional<DeviceProductInfo>& info) {
+static jobject convertDeviceProductInfoToJavaObject(JNIEnv* env,
+                                                    const std::optional<DeviceProductInfo>& info,
+                                                    bool isInternal) {
     using ModelYear = android::DeviceProductInfo::ModelYear;
     using ManufactureYear = android::DeviceProductInfo::ManufactureYear;
     using ManufactureWeekAndYear = android::DeviceProductInfo::ManufactureWeekAndYear;
@@ -1378,7 +1379,8 @@
     // Section 8.7 - Physical Address of HDMI Specification Version 1.3a
     using android::hardware::display::IDeviceProductInfoConstants;
     if (info->relativeAddress.size() != 4) {
-        connectionToSinkType = IDeviceProductInfoConstants::CONNECTION_TO_SINK_UNKNOWN;
+        connectionToSinkType = isInternal ? IDeviceProductInfoConstants::CONNECTION_TO_SINK_BUILT_IN
+                                          : IDeviceProductInfoConstants::CONNECTION_TO_SINK_UNKNOWN;
     } else if (info->relativeAddress[0] == 0) {
         connectionToSinkType = IDeviceProductInfoConstants::CONNECTION_TO_SINK_BUILT_IN;
     } else if (info->relativeAddress[1] == 0) {
@@ -1400,12 +1402,14 @@
 
     jobject object =
             env->NewObject(gStaticDisplayInfoClassInfo.clazz, gStaticDisplayInfoClassInfo.ctor);
-    env->SetBooleanField(object, gStaticDisplayInfoClassInfo.isInternal,
-                         info.connectionType == ui::DisplayConnectionType::Internal);
+
+    const bool isInternal = info.connectionType == ui::DisplayConnectionType::Internal;
+    env->SetBooleanField(object, gStaticDisplayInfoClassInfo.isInternal, isInternal);
     env->SetFloatField(object, gStaticDisplayInfoClassInfo.density, info.density);
     env->SetBooleanField(object, gStaticDisplayInfoClassInfo.secure, info.secure);
     env->SetObjectField(object, gStaticDisplayInfoClassInfo.deviceProductInfo,
-                        convertDeviceProductInfoToJavaObject(env, info.deviceProductInfo));
+                        convertDeviceProductInfoToJavaObject(env, info.deviceProductInfo,
+                                                             isInternal));
     env->SetIntField(object, gStaticDisplayInfoClassInfo.installOrientation,
                      static_cast<uint32_t>(info.installOrientation));
     return object;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 3e0c120..9514aaf 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -4144,6 +4144,37 @@
     <uses-permission android:name="android.permission.QUERY_ADVANCED_PROTECTION_MODE"
         android:featureFlag="android.security.aapm_api"/>
 
+    <!-- Allows an application to read the state of the ForensicService
+         @FlaggedApi(android.security.Flags.FLAG_AFL_API)
+         @SystemApi
+         @hide -->
+    <permission android:name="android.permission.READ_FORENSIC_STATE"
+        android:featureFlag="android.security.afl_api"
+        android:protectionLevel="signature|privileged" />
+    <uses-permission android:name="android.permission.READ_FORENSIC_STATE"
+        android:featureFlag="android.security.afl_api"/>
+
+    <!-- Allows an application to change the state of the ForensicService
+         @FlaggedApi(android.security.Flags.FLAG_AFL_API)
+         @SystemApi
+         @hide -->
+    <permission android:name="android.permission.MANAGE_FORENSIC_STATE"
+        android:featureFlag="android.security.afl_api"
+        android:protectionLevel="signature|privileged" />
+    <uses-permission android:name="android.permission.MANAGE_FORENSIC_STATE"
+        android:featureFlag="android.security.afl_api"/>
+
+    <!-- Must be required by any ForensicEventTransportService to ensure that
+         only the system can bind to it.
+         @FlaggedApi(android.security.Flags.FLAG_AFL_API)
+         @SystemApi
+         @hide -->
+    <permission android:name="android.permission.BIND_FORENSIC_EVENT_TRANSPORT_SERVICE"
+        android:featureFlag="android.security.afl_api"
+        android:protectionLevel="signature" />
+    <uses-permission android:name="android.permission.BIND_FORENSIC_EVENT_TRANSPORT_SERVICE"
+        android:featureFlag="android.security.afl_api"/>
+
     <!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.-->
     <permission android:name="android.permission.PROVISION_DEMO_DEVICE"
                 android:protectionLevel="signature|setup|knownSigner"
@@ -4991,16 +5022,16 @@
         android:protectionLevel="signature|privileged|role"
         android:featureFlag="com.android.settingslib.flags.settings_catalyst" />
 
-    <!-- @FlaggedApi(com.android.settingslib.flags.Flags.FLAG_SETTINGS_CATALYST)
+    <!-- @FlaggedApi(com.android.settingslib.flags.Flags.FLAG_WRITE_SYSTEM_PREFERENCE_PERMISSION_ENABLED)
          Allows an application to access the Settings Preference services to write settings
          values exposed by the system Settings app and system apps that contribute settings surfaced
          in the Settings app.
          <p>This allows the calling application to write settings values
          through the host application, agnostic of underlying storage.
-         <p>Protection Level: signature|privileged|appop - appop to be added in followup -->
+         <p>Protection Level: signature|privileged|appop -->
     <permission android:name="android.permission.WRITE_SYSTEM_PREFERENCES"
-        android:protectionLevel="signature|privileged"
-        android:featureFlag="com.android.settingslib.flags.settings_catalyst" />
+        android:protectionLevel="signature|privileged|appop"
+        android:featureFlag="com.android.settingslib.flags.write_system_preference_permission_enabled" />
 
     <!-- ========================================= -->
     <!-- Permissions for special development tools -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 969ee2e..3287725 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -7212,8 +7212,8 @@
     <!-- Package for opening identity check settings page [CHAR LIMIT=NONE] [DO NOT TRANSLATE] -->
     <string name="identity_check_settings_package_name">com\u002eandroid\u002esettings</string>
 
-    <!-- The name of the service for forensic backup transport. -->
-    <string name="config_forensicBackupTransport" translatable="false"></string>
+    <!-- The name of the service for forensic event transport. -->
+    <string name="config_forensicEventTransport" translatable="false"></string>
 
     <!-- Whether to enable fp unlock when screen turns off on udfps devices -->
     <bool name="config_screen_off_udfps_enabled">false</bool>
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index 31e9913..4ec27a3 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -318,6 +318,12 @@
     <bool name="config_oem_enabled_satellite_access_allow">true</bool>
     <java-symbol type="bool" name="config_oem_enabled_satellite_access_allow" />
 
+    <!-- Whether the satellite modem support concurrent TN scanning while device is in
+         NTN mode.
+         -->
+    <bool name="config_satellite_modem_support_concurrent_tn_scanning">true</bool>
+    <java-symbol type="bool" name="config_satellite_modem_support_concurrent_tn_scanning" />
+
     <!-- The time duration in seconds which is used to decide whether the Location returned from
          LocationManager#getLastKnownLocation is fresh.
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 9dd3027..7fe0912 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5679,8 +5679,8 @@
   <java-symbol type="string" name="identity_check_settings_action" />
   <java-symbol type="string" name="identity_check_settings_package_name" />
 
-  <!-- Forensic backup transport -->
-  <java-symbol type="string" name="config_forensicBackupTransport" />
+  <!-- Forensic event transport -->
+  <java-symbol type="string" name="config_forensicEventTransport" />
 
   <!-- Fingerprint screen off unlock config -->
   <java-symbol type="bool" name="config_screen_off_udfps_enabled" />
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 541ca60..7423567 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -597,6 +597,9 @@
         <!-- Permissions required for CTS test - SettingsPreferenceServiceClientTest -->
         <permission name="android.permission.READ_SYSTEM_PREFERENCES" />
         <permission name="android.permission.WRITE_SYSTEM_PREFERENCES" />
+        <!-- Permission required for CTS test - ForensicManagerTest -->
+        <permission name="android.permission.READ_FORENSIC_STATE" />
+        <permission name="android.permission.MANAGE_FORENSIC_STATE" />
     </privapp-permissions>
 
     <privapp-permissions package="com.android.statementservice">
diff --git a/graphics/java/android/graphics/BlendMode.java b/graphics/java/android/graphics/BlendMode.java
index 5c294aa..c6ae680 100644
--- a/graphics/java/android/graphics/BlendMode.java
+++ b/graphics/java/android/graphics/BlendMode.java
@@ -571,10 +571,10 @@
     }
 
     @NonNull
-    private final Xfermode mXfermode;
+    private final PorterDuffXfermode mXfermode;
 
     BlendMode(int mode) {
-        mXfermode = new Xfermode();
+        mXfermode = new PorterDuffXfermode();
         mXfermode.porterDuffMode = mode;
     }
 
@@ -582,7 +582,7 @@
      * @hide
      */
     @NonNull
-    public Xfermode getXfermode() {
+    public PorterDuffXfermode getXfermode() {
         return mXfermode;
     }
 }
diff --git a/graphics/java/android/graphics/ComposeShader.java b/graphics/java/android/graphics/ComposeShader.java
index 977aeaa..e714568 100644
--- a/graphics/java/android/graphics/ComposeShader.java
+++ b/graphics/java/android/graphics/ComposeShader.java
@@ -40,9 +40,12 @@
      * @param mode     The mode that combines the colors from the two shaders. If mode
      *                 is null, then SRC_OVER is assumed.
      */
+    //TODO(358126864): allow a ComposeShader to accept a RuntimeXfermode
     @Deprecated
     public ComposeShader(@NonNull Shader shaderA, @NonNull Shader shaderB, @NonNull Xfermode mode) {
-        this(shaderA, shaderB, mode.porterDuffMode);
+        this(shaderA, shaderB,
+                mode instanceof PorterDuffXfermode ? ((PorterDuffXfermode) mode).porterDuffMode
+                : BlendMode.SRC_OVER.getXfermode().porterDuffMode);
     }
 
     /**
diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java
index 56bb0f0..2c166c3 100644
--- a/graphics/java/android/graphics/Paint.java
+++ b/graphics/java/android/graphics/Paint.java
@@ -71,6 +71,7 @@
     private long mNativePaint;
     private long mNativeShader;
     private long mNativeColorFilter;
+    private long mNativeXfermode;
 
     // Use a Holder to allow static initialization of Paint in the boot image.
     private static class NoImagePreloadHolder {
@@ -735,6 +736,7 @@
         mPathEffect = null;
         mShader = null;
         mNativeShader = 0;
+        mNativeXfermode = 0;
         mTypeface = null;
         mXfermode = null;
 
@@ -780,6 +782,7 @@
         mNativeShader = paint.mNativeShader;
         mTypeface = paint.mTypeface;
         mXfermode = paint.mXfermode;
+        mNativeXfermode = paint.mNativeXfermode;
 
         mHasCompatScaling = paint.mHasCompatScaling;
         mCompatScaling = paint.mCompatScaling;
@@ -815,7 +818,7 @@
      *
      * Note: Although this method is |synchronized|, this is simply so it
      * is not thread-hostile to multiple threads calling this method. It
-     * is still unsafe to attempt to change the Shader/ColorFilter while
+     * is still unsafe to attempt to change the Shader/ColorFilter/Xfermode while
      * another thread attempts to access the native object.
      *
      * @hide
@@ -833,6 +836,13 @@
             mNativeColorFilter = newNativeColorFilter;
             nSetColorFilter(mNativePaint, mNativeColorFilter);
         }
+        if (mXfermode instanceof RuntimeXfermode) {
+            long newNativeXfermode = ((RuntimeXfermode) mXfermode).createNativeInstance();
+            if (newNativeXfermode != mNativeXfermode) {
+                mNativeXfermode = newNativeXfermode;
+                nSetXfermode(mNativePaint, mNativeXfermode);
+            }
+        }
         return mNativePaint;
     }
 
@@ -1427,16 +1437,17 @@
     }
 
     /**
-     * Get the paint's blend mode object.
+     * Get the paint's blend mode object. Will return null if there is a Xfermode applied that
+     * cannot be represented by a blend mode (i.e. a custom {@code RuntimeXfermode}
      *
      * @return the paint's blend mode (or null)
      */
     @Nullable
     public BlendMode getBlendMode() {
-        if (mXfermode == null) {
+        if (mXfermode == null || !(mXfermode instanceof PorterDuffXfermode)) {
             return null;
         } else {
-            return BlendMode.fromValue(mXfermode.porterDuffMode);
+            return BlendMode.fromValue(((PorterDuffXfermode) mXfermode).porterDuffMode);
         }
     }
 
@@ -1459,8 +1470,15 @@
 
     @Nullable
     private Xfermode installXfermode(Xfermode xfermode) {
-        int newMode = xfermode != null ? xfermode.porterDuffMode : Xfermode.DEFAULT;
-        int curMode = mXfermode != null ? mXfermode.porterDuffMode : Xfermode.DEFAULT;
+        if (xfermode instanceof RuntimeXfermode) {
+            mXfermode = xfermode;
+            nSetXfermode(mNativePaint, ((RuntimeXfermode) xfermode).createNativeInstance());
+            return xfermode;
+        }
+        int newMode = (xfermode instanceof PorterDuffXfermode)
+                ? ((PorterDuffXfermode) xfermode).porterDuffMode : PorterDuffXfermode.DEFAULT;
+        int curMode = (mXfermode instanceof PorterDuffXfermode)
+                ? ((PorterDuffXfermode) mXfermode).porterDuffMode : PorterDuffXfermode.DEFAULT;
         if (newMode != curMode) {
             nSetXfermode(mNativePaint, newMode);
         }
@@ -3823,6 +3841,8 @@
     @CriticalNative
     private static native void nSetXfermode(long paintPtr, int xfermode);
     @CriticalNative
+    private static native void nSetXfermode(long paintPtr, long xfermodePtr);
+    @CriticalNative
     private static native long nSetPathEffect(long paintPtr, long effect);
     @CriticalNative
     private static native long nSetMaskFilter(long paintPtr, long maskfilter);
diff --git a/graphics/java/android/graphics/PorterDuffXfermode.java b/graphics/java/android/graphics/PorterDuffXfermode.java
index ff9ff8b..83d0507 100644
--- a/graphics/java/android/graphics/PorterDuffXfermode.java
+++ b/graphics/java/android/graphics/PorterDuffXfermode.java
@@ -29,6 +29,9 @@
      *
      * @param mode           The porter-duff mode that is applied
      */
+    static final int DEFAULT = PorterDuff.Mode.SRC_OVER.nativeInt;
+    int porterDuffMode = DEFAULT;
+    PorterDuffXfermode() {}
     public PorterDuffXfermode(PorterDuff.Mode mode) {
         porterDuffMode = mode.nativeInt;
     }
diff --git a/graphics/java/android/graphics/RuntimeXfermode.java b/graphics/java/android/graphics/RuntimeXfermode.java
new file mode 100644
index 0000000..f5a6568
--- /dev/null
+++ b/graphics/java/android/graphics/RuntimeXfermode.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics;
+
+import android.annotation.ColorInt;
+import android.annotation.ColorLong;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+
+import com.android.graphics.hwui.flags.Flags;
+
+import libcore.util.NativeAllocationRegistry;
+
+
+/**
+ * <p>A {@link RuntimeXfermode} calculates a per-pixel color based on the output of a user
+ *  * defined Android Graphics Shading Language (AGSL) function.</p>
+ *
+ * <p>This AGSL function takes in two input colors to be operated on. These colors are in sRGB
+ *  * and the output is also interpreted as sRGB. The AGSL function signature expects a single input
+ *  * of color (packed as a half4 or float4 or vec4).</p>
+ *
+ * <pre class="prettyprint">
+ * vec4 main(half4 src, half4 dst);
+ * </pre>
+ */
+@FlaggedApi(Flags.FLAG_RUNTIME_COLOR_FILTERS_BLENDERS)
+public class RuntimeXfermode extends Xfermode {
+
+    private static class NoImagePreloadHolder {
+        public static final NativeAllocationRegistry sRegistry =
+                NativeAllocationRegistry.createMalloced(
+                        RuntimeXfermode.class.getClassLoader(), nativeGetFinalizer());
+    }
+
+    private long mBuilderNativeInstance;
+
+    /**
+     * Creates a new RuntimeBlender.
+     *
+     * @param agsl The text of AGSL color filter program to run.
+     */
+    public RuntimeXfermode(@NonNull String agsl) {
+        if (agsl == null) {
+            throw new NullPointerException("RuntimeShader requires a non-null AGSL string");
+        }
+        mBuilderNativeInstance = nativeCreateBlenderBuilder(agsl);
+        RuntimeXfermode.NoImagePreloadHolder.sRegistry.registerNativeAllocation(
+                this, mBuilderNativeInstance);
+    }
+    /**
+     * Sets the uniform color value corresponding to this color filter.  If the effect does not have
+     * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4
+     * and corresponding layout(color) annotation then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the color uniform declared in the AGSL program
+     * @param color the provided sRGB color
+     */
+    public void setColorUniform(@NonNull String uniformName, @ColorInt int color) {
+        setUniform(uniformName, Color.valueOf(color).getComponents(), true);
+    }
+
+    /**
+     * Sets the uniform color value corresponding to this color filter.  If the effect does not have
+     * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4
+     * and corresponding layout(color) annotation then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the color uniform declared in the AGSL program
+     * @param color the provided sRGB color
+     */
+    public void setColorUniform(@NonNull String uniformName, @ColorLong long color) {
+        Color exSRGB = Color.valueOf(color).convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB));
+        setUniform(uniformName, exSRGB.getComponents(), true);
+    }
+
+    /**
+     * Sets the uniform color value corresponding to this color filter.  If the effect does not have
+     * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4
+     * and corresponding layout(color) annotation then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the color uniform declared in the AGSL program
+     * @param color the provided sRGB color
+     */
+    public void setColorUniform(@NonNull String uniformName, @NonNull Color color) {
+        if (color == null) {
+            throw new NullPointerException("The color parameter must not be null");
+        }
+        Color exSRGB = color.convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB));
+        setUniform(uniformName, exSRGB.getComponents(), true);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a float or
+     * float[1] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, float value) {
+        setFloatUniform(uniformName, value, 0.0f, 0.0f, 0.0f, 1);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a vec2 or
+     * float[2] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, float value1, float value2) {
+        setFloatUniform(uniformName, value1, value2, 0.0f, 0.0f, 2);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a vec3 or
+     * float[3] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, float value1, float value2,
+            float value3) {
+        setFloatUniform(uniformName, value1, value2, value3, 0.0f, 3);
+
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a vec4 or
+     * float[4] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, float value1, float value2,
+            float value3, float value4) {
+        setFloatUniform(uniformName, value1, value2, value3, value4, 4);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than a float
+     * (for N=1), vecN, or float[N] where N is the length of the values param then an
+     * IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setFloatUniform(@NonNull String uniformName, @NonNull float[] values) {
+        setUniform(uniformName, values, false);
+    }
+
+    private void setFloatUniform(@NonNull String uniformName, float value1, float value2,
+            float value3, float value4, int count) {
+        if (uniformName == null) {
+            throw new NullPointerException("The uniformName parameter must not be null");
+        }
+        nativeUpdateUniforms(mBuilderNativeInstance, uniformName, value1, value2, value3, value4,
+                count);
+    }
+
+    private void setUniform(@NonNull String uniformName, @NonNull float[] values, boolean isColor) {
+        if (uniformName == null) {
+            throw new NullPointerException("The uniformName parameter must not be null");
+        }
+        if (values == null) {
+            throw new NullPointerException("The uniform values parameter must not be null");
+        }
+        nativeUpdateUniforms(mBuilderNativeInstance, uniformName, values, isColor);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an int or int[1]
+     * then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, int value) {
+        setIntUniform(uniformName, value, 0, 0, 0, 1);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an ivec2 or
+     * int[2] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, int value1, int value2) {
+        setIntUniform(uniformName, value1, value2, 0, 0, 2);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an ivec3 or
+     * int[3] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, int value1, int value2, int value3) {
+        setIntUniform(uniformName, value1, value2, value3, 0, 3);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an ivec4 or
+     * int[4] then an IllegalArgumentException is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, int value1, int value2,
+            int value3, int value4) {
+        setIntUniform(uniformName, value1, value2, value3, value4, 4);
+    }
+
+    /**
+     * Sets the uniform value corresponding to this color filter.  If the effect does not have a
+     * uniform with that name or if the uniform is declared with a type other than an int (for N=1),
+     * ivecN, or int[N] where N is the length of the values param then an IllegalArgumentException
+     * is thrown.
+     *
+     * @param uniformName name matching the uniform declared in the AGSL program
+     */
+    public void setIntUniform(@NonNull String uniformName, @NonNull int[] values) {
+        if (uniformName == null) {
+            throw new NullPointerException("The uniformName parameter must not be null");
+        }
+        if (values == null) {
+            throw new NullPointerException("The uniform values parameter must not be null");
+        }
+        nativeUpdateUniforms(mBuilderNativeInstance, uniformName, values);
+    }
+
+    private void setIntUniform(@NonNull String uniformName, int value1, int value2, int value3,
+            int value4, int count) {
+        if (uniformName == null) {
+            throw new NullPointerException("The uniformName parameter must not be null");
+        }
+        nativeUpdateUniforms(mBuilderNativeInstance, uniformName, value1, value2, value3, value4,
+                count);
+    }
+
+    /**
+     * Assigns the uniform shader to the provided shader parameter.  If the shader program does not
+     * have a uniform shader with that name then an IllegalArgumentException is thrown.
+     *
+     * @param shaderName name matching the uniform declared in the AGSL program
+     * @param shader shader passed into the AGSL program for sampling
+     */
+    public void setInputShader(@NonNull String shaderName, @NonNull Shader shader) {
+        if (shaderName == null) {
+            throw new NullPointerException("The shaderName parameter must not be null");
+        }
+        if (shader == null) {
+            throw new NullPointerException("The shader parameter must not be null");
+        }
+        nativeUpdateChild(mBuilderNativeInstance, shaderName, shader.getNativeInstance());
+    }
+
+    /**
+     * Assigns the uniform color filter to the provided color filter parameter.  If the shader
+     * program does not have a uniform color filter with that name then an IllegalArgumentException
+     * is thrown.
+     *
+     * @param filterName name matching the uniform declared in the AGSL program
+     * @param colorFilter filter passed into the AGSL program for sampling
+     */
+    public void setInputColorFilter(@NonNull String filterName, @NonNull ColorFilter colorFilter) {
+        if (filterName == null) {
+            throw new NullPointerException("The filterName parameter must not be null");
+        }
+        if (colorFilter == null) {
+            throw new NullPointerException("The colorFilter parameter must not be null");
+        }
+        nativeUpdateChild(mBuilderNativeInstance, filterName, colorFilter.getNativeInstance());
+    }
+
+    /** @hide */
+    public long createNativeInstance() {
+        return nativeCreateNativeInstance(mBuilderNativeInstance);
+    }
+
+    /** @hide */
+    private static native long nativeGetFinalizer();
+    private static native long nativeCreateBlenderBuilder(String agsl);
+    private static native long nativeCreateNativeInstance(long builder);
+    private static native void nativeUpdateUniforms(
+            long builder, String uniformName, float[] uniforms, boolean isColor);
+    private static native void nativeUpdateUniforms(
+            long builder, String uniformName, float value1, float value2, float value3,
+            float value4, int count);
+    private static native void nativeUpdateUniforms(
+            long builder, String uniformName, int[] uniforms);
+    private static native void nativeUpdateUniforms(
+            long builder, String uniformName, int value1, int value2, int value3,
+            int value4, int count);
+    private static native void nativeUpdateChild(long builder, String childName, long child);
+
+}
diff --git a/graphics/java/android/graphics/Xfermode.java b/graphics/java/android/graphics/Xfermode.java
index 6bb22a1..fb689e4 100644
--- a/graphics/java/android/graphics/Xfermode.java
+++ b/graphics/java/android/graphics/Xfermode.java
@@ -21,9 +21,6 @@
 
 package android.graphics;
 
-import android.compat.annotation.UnsupportedAppUsage;
-import android.os.Build;
-
 /**
  * Xfermode is the base class for objects that are called to implement custom
  * "transfer-modes" in the drawing pipeline. The static function Create(Modes)
@@ -31,8 +28,4 @@
  * specified in the Modes enum. When an Xfermode is assigned to a Paint, then
  * objects drawn with that paint have the xfermode applied.
  */
-public class Xfermode {
-    static final int DEFAULT = PorterDuff.Mode.SRC_OVER.nativeInt;
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    int porterDuffMode = DEFAULT;
-}
+public class Xfermode {}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
index a31acc8..4300e84 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/GroupedTaskInfo.java
@@ -168,6 +168,16 @@
     }
 
     /**
+     * @return The task info for the task in this group with the given {@code taskId}.
+     */
+    @Nullable
+    public TaskInfo getTaskById(int taskId) {
+        return mTasks.stream()
+                .filter(task -> task.taskId == taskId)
+                .findFirst().orElse(null);
+    }
+
+    /**
      * Get all {@link RecentTaskInfo}s grouped together.
      */
     @NonNull
@@ -176,6 +186,14 @@
     }
 
     /**
+     * @return Whether this grouped task contains a task with the given {@code taskId}.
+     */
+    public boolean containsTask(int taskId) {
+        return mTasks.stream()
+                .anyMatch((task -> task.taskId == taskId));
+    }
+
+    /**
      * Return {@link SplitBounds} if this is a split screen entry or {@code null}
      */
     @Nullable
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 823c533..39dc267 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -274,11 +274,8 @@
     private final DragAndDropController mDragAndDropController;
     /** Used to send bubble events to launcher. */
     private Bubbles.BubbleStateListener mBubbleStateListener;
-    /**
-     * Used to track previous navigation mode to detect switch to buttons navigation. Set to
-     * true to switch the bubble bar to the opposite side for 3 nav buttons mode on device boot.
-     */
-    private boolean mIsPrevNavModeGestures = true;
+    /** Used to track previous navigation mode to detect switch to buttons navigation. */
+    private boolean mIsPrevNavModeGestures;
     /** Used to send updates to the views from {@link #mBubbleDataListener}. */
     private BubbleViewCallback mBubbleViewCallback;
 
@@ -360,6 +357,7 @@
             }
         };
         mExpandedViewManager = BubbleExpandedViewManager.fromBubbleController(this);
+        mIsPrevNavModeGestures = ContextUtils.isGestureNavigationMode(mContext);
     }
 
     private void registerOneHandedState(OneHandedController oneHanded) {
@@ -595,9 +593,9 @@
         if (mBubbleStateListener != null) {
             boolean isCurrentNavModeGestures = ContextUtils.isGestureNavigationMode(mContext);
             if (mIsPrevNavModeGestures && !isCurrentNavModeGestures) {
-                BubbleBarLocation bubbleBarLocation = ContextUtils.isRtl(mContext)
+                BubbleBarLocation navButtonsLocation = ContextUtils.isRtl(mContext)
                         ? BubbleBarLocation.RIGHT : BubbleBarLocation.LEFT;
-                mBubblePositioner.setBubbleBarLocation(bubbleBarLocation);
+                mBubblePositioner.setBubbleBarLocation(navButtonsLocation);
             }
             mIsPrevNavModeGestures = isCurrentNavModeGestures;
             BubbleBarUpdate update = mBubbleData.getInitialStateForBubbleBar();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 77e041e..e455985 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -1024,10 +1024,13 @@
     @WMSingleton
     @Provides
     static TaskStackTransitionObserver provideTaskStackTransitionObserver(
-            Lazy<Transitions> transitions,
-            ShellInit shellInit
+            ShellInit shellInit,
+            Lazy<ShellTaskOrganizer> shellTaskOrganizer,
+            ShellCommandHandler shellCommandHandler,
+            Lazy<Transitions> transitions
     ) {
-        return new TaskStackTransitionObserver(transitions, shellInit);
+        return new TaskStackTransitionObserver(shellInit, shellTaskOrganizer, shellCommandHandler,
+                transitions);
     }
 
     //
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
index 2001f97..82c2ebc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
@@ -23,6 +23,7 @@
 import android.os.IBinder
 import android.view.SurfaceControl
 import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_OPEN
 import android.window.DesktopModeFlags
 import android.window.TransitionInfo
 import android.window.TransitionInfo.Change
@@ -95,7 +96,7 @@
     fun startLaunchTransition(
         @WindowManager.TransitionType transitionType: Int,
         wct: WindowContainerTransaction,
-        taskId: Int,
+        taskId: Int?,
         minimizingTaskId: Int? = null,
         exitingImmersiveTask: Int? = null,
     ): IBinder {
@@ -216,12 +217,12 @@
     ): Boolean {
         // Check if there's also an immersive change during this launch.
         val immersiveExitChange = pending.exitingImmersiveTask?.let { exitingTask ->
-            findDesktopTaskChange(info, exitingTask)
+            findTaskChange(info, exitingTask)
         }
         val minimizeChange = pending.minimizingTask?.let { minimizingTask ->
-            findDesktopTaskChange(info, minimizingTask)
+            findTaskChange(info, minimizingTask)
         }
-        val launchChange = findDesktopTaskChange(info, pending.launchingTask)
+        val launchChange = findDesktopTaskLaunchChange(info, pending.launchingTask)
         if (launchChange == null) {
             check(minimizeChange == null)
             check(immersiveExitChange == null)
@@ -291,7 +292,7 @@
     ): Boolean {
         if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue) return false
 
-        val minimizeChange = findDesktopTaskChange(info, pending.minimizingTask)
+        val minimizeChange = findTaskChange(info, pending.minimizingTask)
         if (minimizeChange == null) {
             logW("Should have minimizing desktop task")
             return false
@@ -417,8 +418,24 @@
         }
     }
 
-    private fun findDesktopTaskChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? {
-        return info.changes.firstOrNull { change -> change.taskInfo?.taskId == taskId }
+    private fun findTaskChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? =
+        info.changes.firstOrNull { change -> change.taskInfo?.taskId == taskId }
+
+    private fun findDesktopTaskLaunchChange(
+        info: TransitionInfo,
+        launchTaskId: Int?
+    ): TransitionInfo.Change? {
+        return if (launchTaskId != null) {
+            // Launching a known task (probably from background or moving to front), so
+            // specifically look for it.
+            findTaskChange(info, launchTaskId)
+        } else {
+            // Launching a new task, so the first opening freeform task.
+            info.changes.firstOrNull { change ->
+                change.mode == TRANSIT_OPEN
+                        && change.taskInfo != null && change.taskInfo!!.isFreeform
+            }
+        }
     }
 
     private fun WindowContainerTransaction?.merge(
@@ -441,7 +458,7 @@
         /** A task is opening or moving to front. */
         data class Launch(
             override val transition: IBinder,
-            val launchingTask: Int,
+            val launchingTask: Int?,
             val minimizingTask: Int?,
             val exitingImmersiveTask: Int?,
         ) : PendingMixedTransition()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index 08ca55f..7fcb767 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -411,6 +411,12 @@
             desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.toDumpString())
         // Remove task from unminimized task if it is minimized.
         unminimizeTask(displayId, taskId)
+        // Mark task as not in immersive if it was immersive.
+        setTaskInFullImmersiveState(
+            displayId = displayId,
+            taskId = taskId,
+            immersive = false
+        )
         removeActiveTask(taskId)
         removeVisibleTask(taskId)
         if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
@@ -487,6 +493,9 @@
             mainCoroutineScope.launch {
                 try {
                     persistentRepository.addOrUpdateDesktop(
+                        // Use display id as desktop id for now since only once desktop per display
+                        // is supported.
+                        desktopId = displayId,
                         visibleTasks = desktopTaskDataByDisplayIdCopy.visibleTasks,
                         minimizedTasks = desktopTaskDataByDisplayIdCopy.minimizedTasks,
                         freeformTasksInZOrder = desktopTaskDataByDisplayIdCopy.freeformTasksInZOrder
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 6928c25..4db0be5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -89,7 +89,6 @@
 import com.android.wm.shell.common.SingleInstanceRemoteListener
 import com.android.wm.shell.common.SyncTransactionQueue
 import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing
-import com.android.wm.shell.desktopmode.DesktopImmersiveController.ExitResult
 import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.DragStartState
 import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType
 import com.android.wm.shell.desktopmode.DesktopRepository.VisibleTasksListener
@@ -618,25 +617,18 @@
     private fun moveBackgroundTaskToFront(taskId: Int, remoteTransition: RemoteTransition?) {
         logV("moveBackgroundTaskToFront taskId=%s", taskId)
         val wct = WindowContainerTransaction()
-        // TODO: b/342378842 - Instead of using default display, support multiple displays
-        val exitResult = desktopImmersiveController.exitImmersiveIfApplicable(
-            wct = wct,
-            displayId = DEFAULT_DISPLAY,
-            excludeTaskId = taskId,
-        )
         wct.startTask(
             taskId,
             ActivityOptions.makeBasic().apply {
                 launchWindowingMode = WINDOWING_MODE_FREEFORM
             }.toBundle(),
         )
-        val transition = startLaunchTransition(
+        startLaunchTransition(
             TRANSIT_OPEN,
             wct,
             taskId,
             remoteTransition = remoteTransition
         )
-        exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
     }
 
     /**
@@ -655,47 +647,53 @@
         }
         val wct = WindowContainerTransaction()
         wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */)
-        val result = desktopImmersiveController.exitImmersiveIfApplicable(
-            wct = wct,
-            displayId = taskInfo.displayId,
-            excludeTaskId = taskInfo.taskId,
-        )
-        val exitResult = if (result is ExitResult.Exit) { result } else { null }
-        val transition = startLaunchTransition(
+        startLaunchTransition(
             transitionType = TRANSIT_TO_FRONT,
             wct = wct,
-            taskId = taskInfo.taskId,
-            exitingImmersiveTask = exitResult?.exitingTask,
+            launchingTaskId = taskInfo.taskId,
             remoteTransition = remoteTransition,
             displayId = taskInfo.displayId,
         )
-        exitResult?.runOnTransitionStart?.invoke(transition)
     }
 
     private fun startLaunchTransition(
         transitionType: Int,
         wct: WindowContainerTransaction,
-        taskId: Int,
-        exitingImmersiveTask: Int? = null,
+        launchingTaskId: Int?,
         remoteTransition: RemoteTransition? = null,
         displayId: Int = DEFAULT_DISPLAY,
     ): IBinder {
-        val taskIdToMinimize = addAndGetMinimizeChanges(displayId, wct, taskId)
+        val taskIdToMinimize = if (launchingTaskId != null) {
+            addAndGetMinimizeChanges(displayId, wct, newTaskId = launchingTaskId)
+        } else {
+            logW("Starting desktop task launch without checking the task-limit")
+            // TODO(b/378920066): This currently does not respect the desktop window limit.
+            //  It's possible that |launchingTaskId| is null when launching using an intent, and
+            //  the task-limit should be respected then too.
+            null
+        }
+        val exitImmersiveResult = desktopImmersiveController.exitImmersiveIfApplicable(
+            wct = wct,
+            displayId = displayId,
+            excludeTaskId = launchingTaskId,
+        )
         if (remoteTransition == null) {
             val t = desktopMixedTransitionHandler.startLaunchTransition(
                 transitionType = transitionType,
                 wct = wct,
-                taskId = taskId,
+                taskId = launchingTaskId,
                 minimizingTaskId = taskIdToMinimize,
-                exitingImmersiveTask = exitingImmersiveTask,
+                exitingImmersiveTask = exitImmersiveResult.asExit()?.exitingTask,
             )
             taskIdToMinimize?.let { addPendingMinimizeTransition(t, it) }
+            exitImmersiveResult.asExit()?.runOnTransitionStart?.invoke(t)
             return t
         }
         if (taskIdToMinimize == null) {
             val remoteTransitionHandler = OneShotRemoteHandler(mainExecutor, remoteTransition)
             val t = transitions.startTransition(transitionType, wct, remoteTransitionHandler)
             remoteTransitionHandler.setTransition(t)
+            exitImmersiveResult.asExit()?.runOnTransitionStart?.invoke(t)
             return t
         }
         val remoteTransitionHandler =
@@ -704,6 +702,7 @@
         val t = transitions.startTransition(transitionType, wct, remoteTransitionHandler)
         remoteTransitionHandler.setTransition(t)
         taskIdToMinimize.let { addPendingMinimizeTransition(t, it) }
+        exitImmersiveResult.asExit()?.runOnTransitionStart?.invoke(t)
         return t
     }
 
@@ -1472,10 +1471,14 @@
                 )
             }
             WINDOWING_MODE_FREEFORM -> {
-                // TODO(b/336289597): This currently does not respect the desktop window limit.
                 val wct = WindowContainerTransaction()
                 wct.sendPendingIntent(launchIntent, fillIn, options.toBundle())
-                transitions.startTransition(TRANSIT_OPEN, wct, null)
+                startLaunchTransition(
+                    transitionType = TRANSIT_OPEN,
+                    wct = wct,
+                    launchingTaskId = null,
+                    displayId = callingTaskInfo.displayId
+                )
             }
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
index 49cf8ae..35e6c8d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -46,6 +46,8 @@
             "ShellBackPreview"),
     WM_SHELL_RECENT_TASKS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
             Consts.TAG_WM_SHELL),
+    WM_SHELL_TASK_OBSERVER(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
+            Consts.TAG_WM_SHELL),
     // TODO(b/282232877): turn logToLogcat to false.
     WM_SHELL_PICTURE_IN_PICTURE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
             Consts.TAG_WM_SHELL),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
index b58f068..68dc0f2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
@@ -45,9 +45,15 @@
      */
     void onRunningTaskChanged(in RunningTaskInfo taskInfo);
 
-    /** A task has moved to front. */
-    void onTaskMovedToFront(in GroupedTaskInfo[] visibleTasks);
+    /** A task has moved to front. Only used if enableShellTopTaskTracking() is disabled. */
+    void onTaskMovedToFront(in GroupedTaskInfo taskToFront);
 
-    /** A task info has changed. */
+    /** A task info has changed. Only used if enableShellTopTaskTracking() is disabled. */
     void onTaskInfoChanged(in RunningTaskInfo taskInfo);
+
+    /**
+     * If enableShellTopTaskTracking() is enabled, this reports the set of all visible tasks.
+     * Otherwise, this reports only the new top most visible task.
+     */
+    void onVisibleTasksChanged(in GroupedTaskInfo[] visibleTasks);
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index cea4d33..6da4f51 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -17,14 +17,18 @@
 package com.android.wm.shell.recents;
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.content.pm.PackageManager.FEATURE_PC;
 
+import static com.android.wm.shell.Flags.enableShellTopTaskTracking;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_OBSERVER;
 import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
 
 import android.Manifest;
 import android.annotation.RequiresPermission;
 import android.app.ActivityManager;
 import android.app.ActivityManager.RecentTaskInfo;
+import android.app.ActivityManager.RunningTaskInfo;
 import android.app.ActivityTaskManager;
 import android.app.IApplicationThread;
 import android.app.KeyguardManager;
@@ -65,7 +69,6 @@
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
-import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -111,6 +114,11 @@
     private final Map<Integer, SplitBounds> mTaskSplitBoundsMap = new HashMap<>();
 
     /**
+     * Cached list of the visible tasks, sorted from top most to bottom most.
+     */
+    private final List<RunningTaskInfo> mVisibleTasks = new ArrayList<>();
+
+    /**
      * Creates {@link RecentTasksController}, returns {@code null} if the feature is not
      * supported.
      */
@@ -170,10 +178,8 @@
         mShellCommandHandler.addDumpCallback(this::dump, this);
         mTaskStackListener.addListener(this);
         mDesktopRepository.ifPresent(it -> it.addActiveTaskListener(this));
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            mTaskStackTransitionObserver.addTaskStackTransitionObserverListener(this,
-                    mMainExecutor);
-        }
+        mTaskStackTransitionObserver.addTaskStackTransitionObserverListener(this,
+                mMainExecutor);
         mContext.getSystemService(KeyguardManager.class).addKeyguardLockedStateListener(
                 mMainExecutor, isKeyguardLocked -> notifyRecentTasksChanged());
     }
@@ -259,7 +265,10 @@
 
     @Override
     public void onTaskStackChanged() {
-        notifyRecentTasksChanged();
+        if (!enableShellTopTaskTracking()) {
+            // Skip notifying recent tasks changed whenever task stack changes
+            notifyRecentTasksChanged();
+        }
     }
 
     @Override
@@ -273,15 +282,18 @@
         notifyRecentTasksChanged();
     }
 
-    public void onTaskAdded(ActivityManager.RunningTaskInfo taskInfo) {
+    public void onTaskAdded(RunningTaskInfo taskInfo) {
         notifyRunningTaskAppeared(taskInfo);
     }
 
-    public void onTaskRemoved(ActivityManager.RunningTaskInfo taskInfo) {
+    public void onTaskRemoved(RunningTaskInfo taskInfo) {
         // Remove any split pairs associated with this task
         removeSplitPair(taskInfo.taskId);
-        notifyRecentTasksChanged();
         notifyRunningTaskVanished(taskInfo);
+        if (!enableShellTopTaskTracking()) {
+            // Only notify recent tasks changed if we aren't already notifying the visible tasks
+            notifyRecentTasksChanged();
+        }
     }
 
     /**
@@ -289,7 +301,7 @@
      *
      * This currently includes windowing mode and visibility.
      */
-    public void onTaskRunningInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+    public void onTaskRunningInfoChanged(RunningTaskInfo taskInfo) {
         notifyRecentTasksChanged();
         notifyRunningTaskChanged(taskInfo);
     }
@@ -300,14 +312,21 @@
     }
 
     @Override
+    public void onTaskMovedToFrontThroughTransition(RunningTaskInfo runningTaskInfo) {
+        notifyTaskMovedToFront(runningTaskInfo);
+    }
+
+    @Override
     public void onTaskChangedThroughTransition(@NonNull ActivityManager.RunningTaskInfo taskInfo) {
         notifyTaskInfoChanged(taskInfo);
     }
 
     @Override
-    public void onTaskMovedToFrontThroughTransition(
-            ActivityManager.RunningTaskInfo runningTaskInfo) {
-        notifyTaskMovedToFront(runningTaskInfo);
+    public void onVisibleTasksChanged(@NonNull List<? extends RunningTaskInfo> visibleTasks) {
+        mVisibleTasks.clear();
+        mVisibleTasks.addAll(visibleTasks);
+        // Notify with all the info and not just the running task info
+        notifyVisibleTasksChanged(visibleTasks);
     }
 
     @VisibleForTesting
@@ -326,7 +345,7 @@
     /**
      * Notify the running task listener that a task appeared on desktop environment.
      */
-    private void notifyRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) {
+    private void notifyRunningTaskAppeared(RunningTaskInfo taskInfo) {
         if (mListener == null
                 || !shouldEnableRunningTasksForDesktopMode()
                 || taskInfo.realActivity == null) {
@@ -340,9 +359,25 @@
     }
 
     /**
+     * Notify the running task listener that a task was changed on desktop environment.
+     */
+    private void notifyRunningTaskChanged(RunningTaskInfo taskInfo) {
+        if (mListener == null
+                || !shouldEnableRunningTasksForDesktopMode()
+                || taskInfo.realActivity == null) {
+            return;
+        }
+        try {
+            mListener.onRunningTaskChanged(taskInfo);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Failed call onRunningTaskChanged", e);
+        }
+    }
+
+    /**
      * Notify the running task listener that a task was removed on desktop environment.
      */
-    private void notifyRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+    private void notifyRunningTaskVanished(RunningTaskInfo taskInfo) {
         if (mListener == null
                 || !shouldEnableRunningTasksForDesktopMode()
                 || taskInfo.realActivity == null) {
@@ -356,25 +391,30 @@
     }
 
     /**
-     * Notify the running task listener that a task was changed on desktop environment.
+     * Notify the recents task listener that a task moved to front via a transition.
      */
-    private void notifyRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
+    private void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
         if (mListener == null
-                || !shouldEnableRunningTasksForDesktopMode()
-                || taskInfo.realActivity == null) {
+                || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue()
+                || taskInfo.realActivity == null
+                || enableShellTopTaskTracking()) {
             return;
         }
         try {
-            mListener.onRunningTaskChanged(taskInfo);
+            mListener.onTaskMovedToFront(GroupedTaskInfo.forFullscreenTasks(taskInfo));
         } catch (RemoteException e) {
-            Slog.w(TAG, "Failed call onRunningTaskChanged", e);
+            Slog.w(TAG, "Failed call onTaskMovedToFront", e);
         }
     }
 
+    /**
+     * Notify the recents task listener that a task changed via a transition.
+     */
     private void notifyTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
         if (mListener == null
                 || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue()
-                || taskInfo.realActivity == null) {
+                || taskInfo.realActivity == null
+                || enableShellTopTaskTracking()) {
             return;
         }
         try {
@@ -384,17 +424,21 @@
         }
     }
 
-    private void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
+    /**
+     * Notifies that the test of visible tasks have changed.
+     */
+    private void notifyVisibleTasksChanged(@NonNull List<? extends RunningTaskInfo> visibleTasks) {
         if (mListener == null
                 || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue()
-                || taskInfo.realActivity == null) {
+                || !enableShellTopTaskTracking()) {
             return;
         }
         try {
-            GroupedTaskInfo runningTask = GroupedTaskInfo.forFullscreenTasks(taskInfo);
-            mListener.onTaskMovedToFront(new GroupedTaskInfo[]{ runningTask });
+            // Compute the visible recent tasks in order, and move the task to the top
+            mListener.onVisibleTasksChanged(generateList(visibleTasks)
+                    .toArray(new GroupedTaskInfo[0]));
         } catch (RemoteException e) {
-            Slog.w(TAG, "Failed call onTaskMovedToFront", e);
+            Slog.w(TAG, "Failed call onVisibleTasksChanged", e);
         }
     }
 
@@ -407,6 +451,11 @@
     @VisibleForTesting
     void registerRecentTasksListener(IRecentTasksListener listener) {
         mListener = listener;
+        if (enableShellTopTaskTracking()) {
+            ProtoLog.v(WM_SHELL_TASK_OBSERVER, "registerRecentTasksListener");
+            // Post a notification for the current set of visible tasks
+            mMainExecutor.executeDelayed(() -> notifyVisibleTasksChanged(mVisibleTasks), 0);
+        }
     }
 
     @VisibleForTesting
@@ -421,14 +470,18 @@
 
     @VisibleForTesting
     ArrayList<GroupedTaskInfo> getRecentTasks(int maxNum, int flags, int userId) {
-        // Note: the returned task list is from the most-recent to least-recent order
-        final List<RecentTaskInfo> rawList = mActivityTaskManager.getRecentTasks(
-                maxNum, flags, userId);
+        // Note: the returned task list is ordered from the most-recent to least-recent order
+        return generateList(mActivityTaskManager.getRecentTasks(maxNum, flags, userId));
+    }
 
+    /**
+     * Generates a list of GroupedTaskInfos for the given list of tasks.
+     */
+    private <T extends TaskInfo> ArrayList<GroupedTaskInfo> generateList(@NonNull List<T> tasks) {
         // Make a mapping of task id -> task info
         final SparseArray<TaskInfo> rawMapping = new SparseArray<>();
-        for (int i = 0; i < rawList.size(); i++) {
-            final TaskInfo taskInfo = rawList.get(i);
+        for (int i = 0; i < tasks.size(); i++) {
+            final TaskInfo taskInfo = tasks.get(i);
             rawMapping.put(taskInfo.taskId, taskInfo);
         }
 
@@ -437,10 +490,10 @@
 
         int mostRecentFreeformTaskIndex = Integer.MAX_VALUE;
 
+        ArrayList<GroupedTaskInfo> groupedTasks = new ArrayList<>();
         // Pull out the pairs as we iterate back in the list
-        ArrayList<GroupedTaskInfo> recentTasks = new ArrayList<>();
-        for (int i = 0; i < rawList.size(); i++) {
-            final RecentTaskInfo taskInfo = rawList.get(i);
+        for (int i = 0; i < tasks.size(); i++) {
+            final TaskInfo taskInfo = tasks.get(i);
             if (!rawMapping.contains(taskInfo.taskId)) {
                 // If it's not in the mapping, then it was already paired with another task
                 continue;
@@ -451,7 +504,7 @@
                     && mDesktopRepository.get().isActiveTask(taskInfo.taskId)) {
                 // Freeform tasks will be added as a separate entry
                 if (mostRecentFreeformTaskIndex == Integer.MAX_VALUE) {
-                    mostRecentFreeformTaskIndex = recentTasks.size();
+                    mostRecentFreeformTaskIndex = groupedTasks.size();
                 }
                 // If task has their app bounds set to null which happens after reboot, set the
                 // app bounds to persisted lastFullscreenBounds. Also set the position in parent
@@ -471,36 +524,34 @@
             }
 
             final int pairedTaskId = mSplitTasks.get(taskInfo.taskId, INVALID_TASK_ID);
-            if (pairedTaskId != INVALID_TASK_ID && rawMapping.contains(
-                    pairedTaskId)) {
+            if (pairedTaskId != INVALID_TASK_ID && rawMapping.contains(pairedTaskId)) {
                 final TaskInfo pairedTaskInfo = rawMapping.get(pairedTaskId);
                 rawMapping.remove(pairedTaskId);
-                recentTasks.add(GroupedTaskInfo.forSplitTasks(taskInfo, pairedTaskInfo,
+                groupedTasks.add(GroupedTaskInfo.forSplitTasks(taskInfo, pairedTaskInfo,
                         mTaskSplitBoundsMap.get(pairedTaskId)));
             } else {
-                recentTasks.add(GroupedTaskInfo.forFullscreenTasks(taskInfo));
+                // TODO(346588978): Consolidate multiple visible fullscreen tasks into the same
+                //  grouped task
+                groupedTasks.add(GroupedTaskInfo.forFullscreenTasks(taskInfo));
             }
         }
 
         // Add a special entry for freeform tasks
         if (!freeformTasks.isEmpty()) {
-            recentTasks.add(mostRecentFreeformTaskIndex,
+            groupedTasks.add(mostRecentFreeformTaskIndex,
                     GroupedTaskInfo.forFreeformTasks(
                             freeformTasks,
                             minimizedFreeformTasks));
         }
 
-        return recentTasks;
-    }
+        if (enableShellTopTaskTracking()) {
+            // We don't current send pinned tasks as a part of recent or running tasks, so remove
+            // them from the list here
+            groupedTasks.removeIf(
+                    gti -> gti.getTaskInfo1().getWindowingMode() == WINDOWING_MODE_PINNED);
+        }
 
-    /**
-     * Returns the top running leaf task.
-     */
-    @Nullable
-    public ActivityManager.RunningTaskInfo getTopRunningTask() {
-        List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(1,
-                false /* filterOnlyVisibleRecents */);
-        return tasks.isEmpty() ? null : tasks.get(0);
+        return groupedTasks;
     }
 
     /**
@@ -508,12 +559,13 @@
      * NOTE: This path currently makes assumptions that ignoreTaskToken is for the top task.
      */
     @Nullable
-    public ActivityManager.RunningTaskInfo getTopRunningTask(
+    public RunningTaskInfo getTopRunningTask(
             @Nullable WindowContainerToken ignoreTaskToken) {
-        List<ActivityManager.RunningTaskInfo> tasks = mActivityTaskManager.getTasks(2,
-                false /* filterOnlyVisibleRecents */);
+        final List<RunningTaskInfo> tasks = enableShellTopTaskTracking()
+                ? mVisibleTasks
+                : mActivityTaskManager.getTasks(2, false /* filterOnlyVisibleRecents */);
         for (int i = 0; i < tasks.size(); i++) {
-            final ActivityManager.RunningTaskInfo task = tasks.get(i);
+            final RunningTaskInfo task = tasks.get(i);
             if (task.token.equals(ignoreTaskToken)) {
                 continue;
             }
@@ -551,7 +603,7 @@
     }
 
     /**
-     * Find the background task that match the given taskId.
+     * Find the background task (in the recent tasks list) that matches the given taskId.
      */
     @Nullable
     public RecentTaskInfo findTaskInBackground(int taskId) {
@@ -648,29 +700,34 @@
             }
 
             @Override
-            public void onRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) {
+            public void onRunningTaskAppeared(RunningTaskInfo taskInfo) {
                 mListener.call(l -> l.onRunningTaskAppeared(taskInfo));
             }
 
             @Override
-            public void onRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
+            public void onRunningTaskVanished(RunningTaskInfo taskInfo) {
                 mListener.call(l -> l.onRunningTaskVanished(taskInfo));
             }
 
             @Override
-            public void onRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
+            public void onRunningTaskChanged(RunningTaskInfo taskInfo) {
                 mListener.call(l -> l.onRunningTaskChanged(taskInfo));
             }
 
             @Override
-            public void onTaskMovedToFront(GroupedTaskInfo[] taskInfo) {
-                mListener.call(l -> l.onTaskMovedToFront(taskInfo));
+            public void onTaskMovedToFront(GroupedTaskInfo taskToFront) {
+                mListener.call(l -> l.onTaskMovedToFront(taskToFront));
             }
 
             @Override
             public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
                 mListener.call(l -> l.onTaskInfoChanged(taskInfo));
             }
+
+            @Override
+            public void onVisibleTasksChanged(GroupedTaskInfo[] visibleTasks) {
+                mListener.call(l -> l.onVisibleTasksChanged(visibleTasks));
+            }
         };
 
         public IRecentTasksImpl(RecentTasksController controller) {
@@ -724,12 +781,12 @@
         }
 
         @Override
-        public ActivityManager.RunningTaskInfo[] getRunningTasks(int maxNum) {
-            final ActivityManager.RunningTaskInfo[][] tasks =
-                    new ActivityManager.RunningTaskInfo[][]{null};
+        public RunningTaskInfo[] getRunningTasks(int maxNum) {
+            final RunningTaskInfo[][] tasks =
+                    new RunningTaskInfo[][]{null};
             executeRemoteCallWithTaskPermission(mController, "getRunningTasks",
                     (controller) -> tasks[0] = ActivityTaskManager.getInstance().getTasks(maxNum)
-                            .toArray(new ActivityManager.RunningTaskInfo[0]),
+                            .toArray(new RunningTaskInfo[0]),
                     true /* blocking */);
             return tasks[0];
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
index d28a462..93f2e4c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
@@ -17,37 +17,162 @@
 package com.android.wm.shell.recents
 
 import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
 import android.os.IBinder
 import android.util.ArrayMap
 import android.view.SurfaceControl
 import android.view.WindowManager.TRANSIT_CHANGE
 import android.window.DesktopModeFlags
 import android.window.TransitionInfo
+import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.Flags.enableShellTopTaskTracking
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_OBSERVER
 import com.android.wm.shell.shared.TransitionUtil
+import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
 import dagger.Lazy
+import java.io.PrintWriter
+import java.util.StringJoiner
 import java.util.concurrent.Executor
 
 /**
- * A [Transitions.TransitionObserver] that observes shell transitions and sends updates to listeners
- * about task stack changes.
+ * A [Transitions.TransitionObserver] that observes shell transitions, tracks the visible tasks
+ * and notifies listeners whenever the visible tasks change (at the start and end of a transition).
  *
- * TODO(346588978) Move split/pip signals here as well so that launcher don't need to handle it
+ * This can be replaced once we have a generalized task repository tracking visible tasks.
  */
 class TaskStackTransitionObserver(
+    shellInit: ShellInit,
+    private val shellTaskOrganizer: Lazy<ShellTaskOrganizer>,
+    private val shellCommandHandler: ShellCommandHandler,
     private val transitions: Lazy<Transitions>,
-    shellInit: ShellInit
-) : Transitions.TransitionObserver {
+) : Transitions.TransitionObserver, ShellTaskOrganizer.TaskVanishedListener {
+
+    // List of currently visible tasks sorted in z-order from top-most to bottom-most, only used
+    // when Flags.enableShellTopTaskTracking() is enabled.
+    private var visibleTasks: MutableList<RunningTaskInfo> = mutableListOf()
+    private val pendingCloseTasks: MutableList<RunningTaskInfo> = mutableListOf()
+    // Set of listeners to notify when the visible tasks change
     private val taskStackTransitionObserverListeners =
         ArrayMap<TaskStackTransitionObserverListener, Executor>()
+    // Used to filter out leaf-tasks
+    private val leafTaskFilter: TransitionUtil.LeafTaskFilter = TransitionUtil.LeafTaskFilter()
 
     init {
         shellInit.addInitCallback(::onInit, this)
     }
 
     fun onInit() {
+        shellTaskOrganizer.get().addTaskVanishedListener(this)
+        shellCommandHandler.addDumpCallback(::dump, this)
         transitions.get().registerObserver(this)
+
+        // TODO(346588978): We need to update the running tasks once the ShellTaskOrganizer is
+        // registered since there is no existing transition (yet) corresponding for the already
+        // visible tasks
+    }
+
+    /**
+     * This method handles transition ready when only
+     * DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL is set.
+     */
+    private fun onDesktopOnlyFlagTransitionReady(info: TransitionInfo) {
+        for (change in info.changes) {
+            if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) {
+                continue
+            }
+
+            val taskInfo = change.taskInfo
+            if (taskInfo == null || taskInfo.taskId == -1) {
+                continue
+            }
+
+            // Find the first task that is opening, this should be the one at the front after
+            // the transition
+            if (TransitionUtil.isOpeningType(change.mode)) {
+                notifyOnTaskMovedToFront(taskInfo)
+                break
+            } else if (change.mode == TRANSIT_CHANGE) {
+                notifyOnTaskChanged(taskInfo)
+            }
+        }
+    }
+
+    /**
+     * This method handles transition ready when Flags.enableShellTopTaskTracking() is set.
+     */
+    private fun onShellTopTaskTrackerFlagTransitionReady(info: TransitionInfo) {
+        ProtoLog.v(WM_SHELL_TASK_OBSERVER, "Transition ready: %d", info.debugId)
+
+        // Filter out non-leaf tasks (we will likely need them later, but visible task tracking
+        // is currently used only for visible leaf tasks)
+        val changesReversed = mutableListOf<TransitionInfo.Change>()
+        for (change in info.changes) {
+            if (!leafTaskFilter.test(change)) {
+                // Not a leaf task
+                continue
+            }
+            changesReversed.add(0, change)
+        }
+
+        // We iterate the change list in reverse order because changes are sorted top to bottom and
+        // we want to update the lists such that the top most tasks are inserted at the front last
+        var notifyChanges = false
+        for (change in changesReversed) {
+            val taskInfo = change.taskInfo
+            if (taskInfo == null || taskInfo.taskId == -1) {
+                // Not a valid task
+                continue
+            }
+
+            if (TransitionUtil.isClosingMode(change.mode)) {
+                ProtoLog.v(WM_SHELL_TASK_OBSERVER, "\tClosing task=%d", taskInfo.taskId)
+
+                // Closing task's visibilities are not committed until after the transition
+                // completes, so track such tasks so that we can notify on finish
+                if (!pendingCloseTasks.any { it.taskId == taskInfo.taskId }) {
+                    pendingCloseTasks.add(taskInfo)
+                }
+            } else if (TransitionUtil.isOpeningMode(change.mode)
+                    || TransitionUtil.isOrderOnly(change)) {
+                ProtoLog.v(WM_SHELL_TASK_OBSERVER, "\tOpening task=%d", taskInfo.taskId)
+
+                // Remove from pending close tasks list if it's being opened again
+                pendingCloseTasks.removeIf { it.taskId == taskInfo.taskId }
+                // Move the task to the front of the visible tasks list
+                visibleTasks.removeIf { it.taskId == taskInfo.taskId }
+                visibleTasks.add(0, taskInfo)
+                notifyChanges = true
+            }
+        }
+
+        // TODO(346588978): We should verify the task list has actually changed before notifying
+        //  (ie. starting an activity that's already top-most would result in no visible change)
+        if (notifyChanges) {
+            updateVisibleTasksList("transition-start")
+        }
+    }
+
+    private fun updateVisibleTasksList(reason: String) {
+        // This simply constructs a list of visible tasks, where the always-on-top tasks are moved
+        // to the front of the list in-order, to ensure that they match the visible z order
+        val orderedVisibleTasks = mutableListOf<RunningTaskInfo>()
+        var numAlwaysOnTop = 0
+        for (info in visibleTasks) {
+            if (info.windowingMode == WINDOWING_MODE_PINNED
+                    || info.configuration.windowConfiguration.isAlwaysOnTop) {
+                orderedVisibleTasks.add(numAlwaysOnTop, info)
+                numAlwaysOnTop++
+            } else {
+                orderedVisibleTasks.add(info)
+            }
+        }
+        visibleTasks = orderedVisibleTasks
+
+        dumpVisibleTasks(reason)
+        notifyVisibleTasksChanged(visibleTasks)
     }
 
     override fun onTransitionReady(
@@ -56,26 +181,10 @@
         startTransaction: SurfaceControl.Transaction,
         finishTransaction: SurfaceControl.Transaction
     ) {
-        if (DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue) {
-            for (change in info.changes) {
-                if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) {
-                    continue
-                }
-
-                val taskInfo = change.taskInfo
-                if (taskInfo == null || taskInfo.taskId == -1) {
-                    continue
-                }
-
-                // Find the first task that is opening, this should be the one at the front after
-                // the transition
-                if (TransitionUtil.isOpeningType(change.mode)) {
-                    notifyOnTaskMovedToFront(taskInfo)
-                    break
-                } else if (change.mode == TRANSIT_CHANGE) {
-                    notifyOnTaskChanged(taskInfo)
-                }
-            }
+        if (enableShellTopTaskTracking()) {
+            onShellTopTaskTrackerFlagTransitionReady(info)
+        } else if (DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue) {
+            onDesktopOnlyFlagTransitionReady(info)
         }
     }
 
@@ -83,8 +192,35 @@
 
     override fun onTransitionMerged(merged: IBinder, playing: IBinder) {}
 
-    override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {}
+    override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {
+        if (enableShellTopTaskTracking()) {
+            if (pendingCloseTasks.isNotEmpty()) {
+                // Update the visible task list based on the pending close tasks
+                for (change in pendingCloseTasks) {
+                    visibleTasks.removeIf {
+                        it.taskId == change.taskId
+                    }
+                }
+                updateVisibleTasksList("transition-finished")
+            }
+        }
+    }
 
+    override fun onTaskVanished(taskInfo: RunningTaskInfo?) {
+        if (!enableShellTopTaskTracking()) {
+            return
+        }
+        ProtoLog.v(WM_SHELL_TASK_OBSERVER, "Task vanished: task=%d", taskInfo?.taskId)
+        pendingCloseTasks.removeIf { it.taskId == taskInfo?.taskId }
+        if (visibleTasks.any { it.taskId == taskInfo?.taskId }) {
+            visibleTasks.removeIf { it.taskId == taskInfo?.taskId }
+            updateVisibleTasksList("task-vanished")
+        }
+    }
+
+    /**
+     * Adds a new task stack observer.
+     */
     fun addTaskStackTransitionObserverListener(
         taskStackTransitionObserverListener: TaskStackTransitionObserverListener,
         executor: Executor
@@ -92,6 +228,9 @@
         taskStackTransitionObserverListeners[taskStackTransitionObserverListener] = executor
     }
 
+    /**
+     * Removes an existing task stack observer.
+     */
     fun removeTaskStackTransitionObserverListener(
         taskStackTransitionObserverListener: TaskStackTransitionObserverListener
     ) {
@@ -99,22 +238,66 @@
     }
 
     private fun notifyOnTaskMovedToFront(taskInfo: RunningTaskInfo) {
+        if (enableShellTopTaskTracking()) {
+            return
+        }
         taskStackTransitionObserverListeners.forEach { (listener, executor) ->
             executor.execute { listener.onTaskMovedToFrontThroughTransition(taskInfo) }
         }
     }
 
     private fun notifyOnTaskChanged(taskInfo: RunningTaskInfo) {
+        if (enableShellTopTaskTracking()) {
+            return
+        }
         taskStackTransitionObserverListeners.forEach { (listener, executor) ->
             executor.execute { listener.onTaskChangedThroughTransition(taskInfo) }
         }
     }
 
+    private fun notifyVisibleTasksChanged(visibleTasks: List<RunningTaskInfo>) {
+        taskStackTransitionObserverListeners.forEach { (listener, executor) ->
+            executor.execute { listener.onVisibleTasksChanged(visibleTasks) }
+        }
+    }
+
+    fun dump(pw: PrintWriter, prefix: String) {
+        pw.println("${prefix}$TAG:")
+
+        if (visibleTasks.isEmpty()) {
+            pw.println("$prefix  visibleTasks=[]")
+        } else {
+            val stringJoiner = StringJoiner(",\n\t", "[\n\t", "\n]")
+            visibleTasks.forEach {
+                stringJoiner.add("id=${it.taskId} cmp=${it.baseIntent.component}")
+            }
+            pw.println("$prefix  visibleTasks=$stringJoiner")
+        }
+    }
+
+    /** Dumps the set of visible tasks to protolog */
+    private fun dumpVisibleTasks(reason: String) {
+        if (!WM_SHELL_TASK_OBSERVER.isEnabled) {
+            return
+        }
+        ProtoLog.v(WM_SHELL_TASK_OBSERVER, "\tVisible tasks (%s)", reason)
+        for (task in visibleTasks) {
+            ProtoLog.v(WM_SHELL_TASK_OBSERVER, "\t\ttaskId=%d package=%s", task.taskId,
+                task.baseIntent.component?.packageName)
+        }
+    }
+
     /** Listener to use to get updates regarding task stack from this observer */
     interface TaskStackTransitionObserverListener {
         /** Called when a task is moved to front. */
         fun onTaskMovedToFrontThroughTransition(taskInfo: RunningTaskInfo) {}
+        /** Called when the set of visible tasks have changed. */
+        fun onVisibleTasksChanged(visibleTasks: List<RunningTaskInfo>) {}
         /** Called when a task info has changed. */
         fun onTaskChangedThroughTransition(taskInfo: RunningTaskInfo) {}
     }
+
+    companion object {
+        const val TAG = "TaskStackTransitionObserver"
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZoneLandscape.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZoneLandscape.kt
new file mode 100644
index 0000000..e1120bd
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZoneLandscape.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.wm.shell.flicker
+
+import android.tools.Rotation.ROTATION_90
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP
+import com.android.wm.shell.scenarios.MaximizeAppWindowWithDragToTopDragZone
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Maximize app window by dragging it to the top drag zone.
+ *
+ * Assert that the app window keeps the same increases in size, filling the vertical and horizontal
+ * stable display bounds.
+ */
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class MaximizeAppWindowWithDragToTopDragZoneLandscape : MaximizeAppWindowWithDragToTopDragZone(
+    rotation = ROTATION_90
+) {
+    @ExpectedScenarios(["MAXIMIZE_APP"])
+    @Test
+    override fun maximizeAppWithDragToTopDragZone() = super.maximizeAppWithDragToTopDragZone()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP)
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZonePortrait.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZonePortrait.kt
new file mode 100644
index 0000000..fb910c7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MaximizeAppWindowWithDragToTopDragZonePortrait.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MAXIMIZE_APP
+import com.android.wm.shell.scenarios.MaximizeAppWindowWithDragToTopDragZone
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Maximize app window by dragging it to the top drag zone.
+ *
+ * Assert that the app window keeps the same increases in size, filling the vertical and horizontal
+ * stable display bounds.
+ */
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class MaximizeAppWindowWithDragToTopDragZonePortrait : MaximizeAppWindowWithDragToTopDragZone() {
+    @ExpectedScenarios(["MAXIMIZE_APP"])
+    @Test
+    override fun maximizeAppWithDragToTopDragZone() = super.maximizeAppWithDragToTopDragZone()
+
+    companion object {
+        @JvmStatic
+        @FlickerConfigProvider
+        fun flickerConfigProvider(): FlickerConfig =
+            FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(MAXIMIZE_APP)
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestSyncExecutor.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestSyncExecutor.kt
new file mode 100644
index 0000000..528ca7e
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestSyncExecutor.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.wm.shell
+
+import com.android.wm.shell.common.ShellExecutor
+
+/**
+ * Test ShellExecutor that runs everything synchronously.
+ */
+class TestSyncExecutor : ShellExecutor {
+    override fun execute(runnable: Runnable) {
+        runnable.run()
+    }
+
+    override fun executeDelayed(runnable: Runnable, delayMillis: Long) {
+        runnable.run()
+    }
+
+    override fun removeCallbacks(runnable: Runnable) {
+    }
+
+    override fun hasCallback(runnable: Runnable): Boolean {
+        return false
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
index f21f264..62717a3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -31,6 +31,8 @@
 import android.testing.TestableLooper.RunWithLooper
 import android.view.SurfaceControl
 import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_NONE
 import android.view.WindowManager.TRANSIT_OPEN
 import android.view.WindowManager.TRANSIT_TO_BACK
 import android.view.WindowManager.TransitionType
@@ -47,6 +49,7 @@
 import com.android.wm.shell.freeform.FreeformTaskTransitionHandler
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.util.StubTransaction
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNull
@@ -491,6 +494,72 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun startLaunchTransition_unknownLaunchingTask_animates() {
+        val wct = WindowContainerTransaction()
+        val task = createTask(WINDOWING_MODE_FREEFORM)
+        val transition = Binder()
+        whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull()))
+            .thenReturn(transition)
+        whenever(transitions.dispatchTransition(eq(transition), any(), any(), any(), any(), any()))
+            .thenReturn(mock())
+
+        mixedHandler.startLaunchTransition(
+            transitionType = TRANSIT_OPEN,
+            wct = wct,
+            taskId = null,
+        )
+
+        val started = mixedHandler.startAnimation(
+            transition,
+            createTransitionInfo(
+                TRANSIT_OPEN,
+                listOf(createChange(task, mode = TRANSIT_OPEN))
+            ),
+            StubTransaction(),
+            StubTransaction(),
+        ) { }
+
+        assertThat(started).isEqualTo(true)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun startLaunchTransition_unknownLaunchingTaskOverImmersive_animatesImmersiveChange() {
+        val wct = WindowContainerTransaction()
+        val immersiveTask = createTask(WINDOWING_MODE_FREEFORM)
+        val openingTask = createTask(WINDOWING_MODE_FREEFORM)
+        val transition = Binder()
+        whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull()))
+            .thenReturn(transition)
+        whenever(transitions.dispatchTransition(eq(transition), any(), any(), any(), any(), any()))
+            .thenReturn(mock())
+
+        mixedHandler.startLaunchTransition(
+            transitionType = TRANSIT_OPEN,
+            wct = wct,
+            taskId = null,
+            exitingImmersiveTask = immersiveTask.taskId,
+        )
+
+        val immersiveChange = createChange(immersiveTask, mode = TRANSIT_CHANGE)
+        val openingChange = createChange(openingTask, mode = TRANSIT_OPEN)
+        val started = mixedHandler.startAnimation(
+            transition,
+            createTransitionInfo(
+                TRANSIT_OPEN,
+                listOf(immersiveChange, openingChange)
+            ),
+            StubTransaction(),
+            StubTransaction(),
+        ) { }
+
+        assertThat(started).isEqualTo(true)
+        verify(desktopImmersiveController)
+            .animateResizeChange(eq(immersiveChange), any(), any(), any())
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS)
     fun addPendingAndAnimateLaunchTransition_noMinimizeChange_doesNotReparentMinimizeChange() {
         val wct = WindowContainerTransaction()
@@ -712,9 +781,13 @@
         changes.forEach { change -> addChange(change) }
     }
 
-    private fun createChange(task: RunningTaskInfo): TransitionInfo.Change =
+    private fun createChange(
+        task: RunningTaskInfo,
+        @TransitionInfo.TransitionMode mode: Int = TRANSIT_NONE
+    ): TransitionInfo.Change =
         TransitionInfo.Change(task.token, SurfaceControl()).apply {
             taskInfo = task
+            setMode(mode)
         }
 
     private fun createTask(@WindowingMode windowingMode: Int): RunningTaskInfo =
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index ad266ea..2319716 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -100,6 +100,7 @@
 import com.android.wm.shell.common.MultiInstanceHelper
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.desktopmode.DesktopImmersiveController.ExitResult
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
 import com.android.wm.shell.desktopmode.DesktopTasksController.TaskbarDesktopTaskListener
@@ -167,6 +168,7 @@
 import org.mockito.Mockito.times
 import org.mockito.kotlin.any
 import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.atLeastOnce
 import org.mockito.kotlin.capture
 import org.mockito.kotlin.eq
@@ -294,10 +296,10 @@
     whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(any(), any<RunningTaskInfo>()))
-      .thenReturn(DesktopImmersiveController.ExitResult.NoExit)
+      .thenReturn(ExitResult.NoExit)
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull()))
-      .thenReturn(DesktopImmersiveController.ExitResult.NoExit)
+      .thenReturn(ExitResult.NoExit)
 
     controller = createController()
     controller.setSplitScreenController(splitScreenController)
@@ -1833,7 +1835,8 @@
     whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
       .thenReturn(transition)
     whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task)))
-      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+      .thenReturn(
+        ExitResult.Exit(
         exitingTask = task.taskId,
         runOnTransitionStart = runOnTransit,
       ))
@@ -3214,13 +3217,43 @@
   fun newWindow_fromFreeformAddsNewWindow() {
     setUpLandscapeDisplay()
     val task = setUpFreeformTask()
-    val wctCaptor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+    val wctCaptor = argumentCaptor<WindowContainerTransaction>()
+    val transition = Binder()
+    whenever(mMockDesktopImmersiveController
+      .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull()))
+      .thenReturn(ExitResult.NoExit)
+    whenever(desktopMixedTransitionHandler
+      .startLaunchTransition(anyInt(), any(), anyOrNull(), anyOrNull(), anyOrNull()))
+      .thenReturn(transition)
+
     runOpenNewWindow(task)
-    verify(transitions).startTransition(anyInt(), wctCaptor.capture(), anyOrNull())
-    assertThat(ActivityOptions.fromBundle(wctCaptor.value.hierarchyOps[0].launchOptions)
+
+    verify(desktopMixedTransitionHandler)
+      .startLaunchTransition(anyInt(), wctCaptor.capture(), anyOrNull(), anyOrNull(), anyOrNull())
+    assertThat(ActivityOptions.fromBundle(wctCaptor.firstValue.hierarchyOps[0].launchOptions)
       .launchWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
   }
 
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES)
+  fun newWindow_fromFreeform_exitsImmersiveIfNeeded() {
+    setUpLandscapeDisplay()
+    val immersiveTask = setUpFreeformTask()
+    val task = setUpFreeformTask()
+    val runOnStart = RunOnStartTransitionCallback()
+    val transition = Binder()
+    whenever(mMockDesktopImmersiveController
+      .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull()))
+      .thenReturn(ExitResult.Exit(immersiveTask.taskId, runOnStart))
+    whenever(desktopMixedTransitionHandler
+      .startLaunchTransition(anyInt(), any(), anyOrNull(), anyOrNull(), anyOrNull()))
+      .thenReturn(transition)
+
+    runOpenNewWindow(task)
+
+    runOnStart.assertOnlyInvocation(transition)
+  }
+
   private fun runOpenNewWindow(task: RunningTaskInfo) {
     markTaskVisible(task)
     task.baseActivity = mock(ComponentName::class.java)
@@ -3314,7 +3347,8 @@
       .thenReturn(transition)
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId), eq(freeformTask.taskId)))
-      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+      .thenReturn(
+        ExitResult.Exit(
         exitingTask = immersiveTask.taskId,
         runOnTransitionStart = runOnStartTransit,
       ))
@@ -3719,7 +3753,8 @@
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(wct, task.displayId, task.taskId))
-      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+      .thenReturn(
+        ExitResult.Exit(
         exitingTask = 5,
         runOnTransitionStart = runOnStartTransit,
       ))
@@ -3740,7 +3775,8 @@
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(wct, task.displayId, task.taskId))
-      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+      .thenReturn(
+        ExitResult.Exit(
         exitingTask = 5,
         runOnTransitionStart = runOnStartTransit,
       ))
@@ -3760,7 +3796,8 @@
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId)))
-      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+      .thenReturn(
+        ExitResult.Exit(
         exitingTask = 5,
         runOnTransitionStart = runOnStartTransit,
       ))
@@ -3782,7 +3819,8 @@
     val transition = Binder()
     whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId)))
-      .thenReturn(DesktopImmersiveController.ExitResult.Exit(
+      .thenReturn(
+        ExitResult.Exit(
         exitingTask = 5,
         runOnTransitionStart = runOnStartTransit,
       ))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
index 2b30bc3..fd3adab 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
@@ -176,6 +176,30 @@
         assertThat(recentTaskInfoParcel.minimizedTaskIds).isEqualTo(arrayOf(2).toIntArray())
     }
 
+    @Test
+    fun testGetTaskById_singleTasks() {
+        val task1 = createTaskInfo(id = 1234)
+
+        val taskInfo = GroupedTaskInfo.forFullscreenTasks(task1)
+
+        assertThat(taskInfo.getTaskById(1234)).isEqualTo(task1)
+        assertThat(taskInfo.containsTask(1234)).isTrue()
+    }
+
+    @Test
+    fun testGetTaskById_multipleTasks() {
+        val task1 = createTaskInfo(id = 1)
+        val task2 = createTaskInfo(id = 2)
+        val splitBounds = SplitBounds(Rect(), Rect(), 1, 2, SNAP_TO_2_50_50)
+
+        val taskInfo = GroupedTaskInfo.forSplitTasks(task1, task2, splitBounds)
+
+        assertThat(taskInfo.getTaskById(1)).isEqualTo(task1)
+        assertThat(taskInfo.getTaskById(2)).isEqualTo(task2)
+        assertThat(taskInfo.containsTask(1)).isTrue()
+        assertThat(taskInfo.containsTask(2)).isTrue()
+    }
+
     private fun createTaskInfo(id: Int) = ActivityManager.RecentTaskInfo().apply {
         taskId = id
         token = WindowContainerToken(mock(IWindowContainerToken::class.java))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index dede583..12c3978 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -610,7 +610,7 @@
         mRecentTasksControllerReal.onTaskMovedToFrontThroughTransition(taskInfo);
 
         GroupedTaskInfo runningTask = GroupedTaskInfo.forFullscreenTasks(taskInfo);
-        verify(mRecentTasksListener).onTaskMovedToFront(eq(new GroupedTaskInfo[] { runningTask }));
+        verify(mRecentTasksListener).onTaskMovedToFront(eq(runningTask));
     }
 
     @Test
@@ -656,6 +656,35 @@
         assertEquals(splitBounds4, pair2Bounds);
     }
 
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    public void shellTopTaskTracker_onTaskStackChanged_expectNoRecentsChanged() throws Exception {
+        mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+        mRecentTasksControllerReal.onTaskStackChanged();
+        verify(mRecentTasksListener, never()).onRecentTasksChanged();
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    public void shellTopTaskTracker_onTaskRemoved_expectNoRecentsChanged() throws Exception {
+        mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+        ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
+        mRecentTasksControllerReal.onTaskRemoved(taskInfo);
+        verify(mRecentTasksListener, never()).onRecentTasksChanged();
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    public void shellTopTaskTracker_onVisibleTasksChanged() throws Exception {
+        mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+        ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
+        mRecentTasksControllerReal.onVisibleTasksChanged(List.of(taskInfo));
+        verify(mRecentTasksListener, never()).onVisibleTasksChanged(any());
+    }
+
     /**
      * Helper to create a task with a given task id.
      */
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
index efe4fb1..9919462 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
@@ -16,21 +16,36 @@
 
 package com.android.wm.shell.recents
 
-import android.app.ActivityManager
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.TaskInfo
 import android.app.WindowConfiguration
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
+import android.content.ComponentName
+import android.content.Intent
 import android.os.IBinder
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.platform.test.flag.junit.SetFlagsRule
 import android.testing.AndroidTestingRunner
 import android.view.SurfaceControl
 import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_CLOSE
+import android.view.WindowManager.TRANSIT_FIRST_CUSTOM
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_FRONT
 import android.window.IWindowContainerToken
 import android.window.TransitionInfo
+import android.window.TransitionInfo.FLAG_MOVED_TO_TOP
 import android.window.WindowContainerToken
 import androidx.test.filters.SmallTest
 import com.android.window.flags.Flags
+import com.android.wm.shell.ShellTaskOrganizer
 import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.TestSyncExecutor
 import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.sysui.ShellCommandHandler
 import com.android.wm.shell.sysui.ShellInit
 import com.android.wm.shell.transition.TransitionInfoBuilder
 import com.android.wm.shell.transition.Transitions
@@ -61,7 +76,10 @@
     @JvmField @Rule val setFlagsRule = SetFlagsRule()
 
     @Mock private lateinit var shellInit: ShellInit
-    @Mock lateinit var testExecutor: ShellExecutor
+    @Mock private lateinit var shellTaskOrganizerLazy: Lazy<ShellTaskOrganizer>
+    @Mock private lateinit var shellTaskOrganizer: ShellTaskOrganizer
+    @Mock private lateinit var shellCommandHandler: ShellCommandHandler
+    @Mock private lateinit var testExecutor: ShellExecutor
     @Mock private lateinit var transitionsLazy: Lazy<Transitions>
     @Mock private lateinit var transitions: Transitions
     @Mock private lateinit var mockTransitionBinder: IBinder
@@ -73,24 +91,23 @@
         MockitoAnnotations.initMocks(this)
         shellInit = Mockito.spy(ShellInit(testExecutor))
         whenever(transitionsLazy.get()).thenReturn(transitions)
-        transitionObserver = TaskStackTransitionObserver(transitionsLazy, shellInit)
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
-            verify(shellInit)
-                .addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
-            initRunnableCaptor.value.run()
-        } else {
-            transitionObserver.onInit()
-        }
+        whenever(shellTaskOrganizerLazy.get()).thenReturn(shellTaskOrganizer)
+        transitionObserver = TaskStackTransitionObserver(shellInit, shellTaskOrganizerLazy,
+            shellCommandHandler, transitionsLazy)
+
+        val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
+        verify(shellInit)
+            .addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
+        initRunnableCaptor.value.run()
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun testRegistersObserverAtInit() {
         verify(transitions).registerObserver(same(transitionObserver))
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
     @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun taskCreated_freeformWindow_listenerNotified() {
         val listener = TestListener()
@@ -98,11 +115,11 @@
         transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
         val change =
             createChange(
-                WindowManager.TRANSIT_OPEN,
+                TRANSIT_OPEN,
                 createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
             )
         val transitionInfo =
-            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build()
+            TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
 
         callOnTransitionReady(transitionInfo)
         callOnTransitionFinished()
@@ -114,6 +131,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
     @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun taskCreated_fullscreenWindow_listenerNotified() {
         val listener = TestListener()
@@ -121,11 +139,11 @@
         transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
         val change =
             createChange(
-                WindowManager.TRANSIT_OPEN,
-                createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
+                TRANSIT_OPEN,
+                createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)
             )
         val transitionInfo =
-            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build()
+            TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
 
         callOnTransitionReady(transitionInfo)
         callOnTransitionFinished()
@@ -133,10 +151,11 @@
 
         assertThat(listener.taskInfoOnTaskMovedToFront.taskId).isEqualTo(1)
         assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode)
-            .isEqualTo(WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
+            .isEqualTo(WINDOWING_MODE_FULLSCREEN)
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
     @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun taskCreated_freeformWindowOnTopOfFreeform_listenerNotified() {
         val listener = TestListener()
@@ -144,7 +163,7 @@
         transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
         val freeformOpenChange =
             createChange(
-                WindowManager.TRANSIT_OPEN,
+                TRANSIT_OPEN,
                 createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
             )
         val freeformReorderChange =
@@ -153,7 +172,7 @@
                 createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM)
             )
         val transitionInfo =
-            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0)
+            TransitionInfoBuilder(TRANSIT_OPEN, 0)
                 .addChange(freeformOpenChange)
                 .addChange(freeformReorderChange)
                 .build()
@@ -169,6 +188,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
     @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun transitionMerged_withChange_onlyOpenChangeIsNotified() {
         val listener = TestListener()
@@ -178,11 +198,11 @@
         // Create open transition
         val change =
             createChange(
-                WindowManager.TRANSIT_OPEN,
+                TRANSIT_OPEN,
                 createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
             )
         val transitionInfo =
-            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build()
+            TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
 
         // create change transition to be merged to above transition
         val mergedChange =
@@ -212,6 +232,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
     @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun transitionMerged_withOpen_lastOpenChangeIsNotified() {
         val listener = TestListener()
@@ -221,20 +242,20 @@
         // Create open transition
         val change =
             createChange(
-                WindowManager.TRANSIT_OPEN,
+                TRANSIT_OPEN,
                 createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
             )
         val transitionInfo =
-            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build()
+            TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
 
         // create change transition to be merged to above transition
         val mergedChange =
             createChange(
-                WindowManager.TRANSIT_OPEN,
+                TRANSIT_OPEN,
                 createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM)
             )
         val mergedTransitionInfo =
-            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(mergedChange).build()
+            TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(mergedChange).build()
         val mergedTransition = Mockito.mock(IBinder::class.java)
 
         callOnTransitionReady(transitionInfo)
@@ -250,6 +271,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
     @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun taskChange_freeformWindowToFullscreenWindow_listenerNotified() {
         val listener = TestListener()
@@ -257,11 +279,11 @@
         transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
         val freeformState =
             createChange(
-                WindowManager.TRANSIT_OPEN,
+                TRANSIT_OPEN,
                 createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
             )
         val transitionInfoOpen =
-            TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(freeformState).build()
+            TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(freeformState).build()
         callOnTransitionReady(transitionInfoOpen)
         callOnTransitionFinished()
         executor.flushAll()
@@ -276,7 +298,7 @@
         val fullscreenState =
             createChange(
                 WindowManager.TRANSIT_CHANGE,
-                createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
+                createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)
             )
         val transitionInfoChange =
             TransitionInfoBuilder(WindowManager.TRANSIT_CHANGE, 0)
@@ -301,6 +323,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
     @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun singleTransition_withOpenAndChange_onlyOpenIsNotified() {
         val listener = TestListener()
@@ -310,13 +333,13 @@
         // Creating multiple changes to be fired in a single transition
         val freeformState =
             createChange(
-                mode = WindowManager.TRANSIT_OPEN,
+                mode = TRANSIT_OPEN,
                 taskInfo = createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
             )
         val fullscreenState =
             createChange(
                 mode = WindowManager.TRANSIT_CHANGE,
-                taskInfo = createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
+                taskInfo = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)
             )
 
         val transitionInfoWithChanges =
@@ -336,6 +359,7 @@
     }
 
     @Test
+    @DisableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
     @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
     fun singleTransition_withMultipleChanges_listenerNotified_forEachChange() {
         val listener = TestListener()
@@ -349,7 +373,7 @@
             listOf(
                     WindowConfiguration.WINDOWING_MODE_FREEFORM,
                     WindowConfiguration.WINDOW_CONFIG_DISPLAY_ROTATION,
-                    WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+                    WINDOWING_MODE_FULLSCREEN
                 )
                 .map { change ->
                     createChange(
@@ -376,19 +400,259 @@
         }
     }
 
-    class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener {
-        var taskInfoOnTaskMovedToFront = ActivityManager.RunningTaskInfo()
-        var taskInfoOnTaskChanged = mutableListOf<ActivityManager.RunningTaskInfo>()
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    fun openTransition_visibleTasksChanged() {
+        val listener = TestListener()
+        val executor = TestSyncExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
 
-        override fun onTaskMovedToFrontThroughTransition(
-            taskInfo: ActivityManager.RunningTaskInfo
-        ) {
+        // Model an opening task
+        val firstOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(firstOpeningTransition)
+        callOnTransitionFinished()
+        // Assert that the task is reported visible
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1)
+        assertVisibleTasks(listener, listOf(1))
+
+        // Model opening another task
+        val nextOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 2, WINDOWING_MODE_FULLSCREEN),
+                    createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(nextOpeningTransition)
+        // Assert that the visible list from top to bottom is valid (opening, closing)
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(2)
+        assertVisibleTasks(listener, listOf(2, 1))
+
+        callOnTransitionFinished()
+        // Assert that after the transition finishes, there is only the opening task remaining
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(3)
+        assertVisibleTasks(listener, listOf(2))
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    fun toFrontTransition_visibleTasksChanged() {
+        val listener = TestListener()
+        val executor = TestSyncExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+
+        // Model an opening task
+        val firstOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(firstOpeningTransition)
+        callOnTransitionFinished()
+        // Assert that the task is reported visible
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1)
+        assertVisibleTasks(listener, listOf(1))
+
+        // Model opening another task
+        val nextOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 2, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(nextOpeningTransition)
+        callOnTransitionFinished()
+        // Assert that the visible list from top to bottom is valid
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(2)
+        assertVisibleTasks(listener, listOf(2, 1))
+
+        // Model the first task moving to front
+        val toFrontTransition =
+            createTransitionInfo(TRANSIT_TO_FRONT,
+                listOf(
+                    createChange(TRANSIT_CHANGE, 1, WINDOWING_MODE_FULLSCREEN,
+                        FLAG_MOVED_TO_TOP),
+                )
+            )
+
+        callOnTransitionReady(toFrontTransition)
+        callOnTransitionFinished()
+        // Assert that the visible list from top to bottom is valid
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(3)
+        assertVisibleTasks(listener, listOf(1, 2))
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    fun closeTransition_visibleTasksChanged() {
+        val listener = TestListener()
+        val executor = TestSyncExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+
+        // Model an opening task
+        val firstOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(firstOpeningTransition)
+        callOnTransitionFinished()
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1)
+
+        // Model a closing task
+        val nextOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(nextOpeningTransition)
+        // Assert that the visible list hasn't changed (the close is pending)
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1)
+
+        callOnTransitionFinished()
+        // Assert that after the transition finishes, there is only the opening task remaining
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(2)
+        assertVisibleTasks(listener, listOf())
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    fun changeTransition_visibleTasksUnchanged() {
+        val listener = TestListener()
+        val executor = TestSyncExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+
+        // Model an opening task
+        val firstOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(firstOpeningTransition)
+        callOnTransitionFinished()
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1)
+
+        // Model a closing task
+        val nextOpeningTransition =
+            createTransitionInfo(
+                TRANSIT_FIRST_CUSTOM,
+                listOf(
+                    createChange(TRANSIT_CHANGE, 1, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(nextOpeningTransition)
+        // Assert that the visible list hasn't changed
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    fun taskVanished_visibleTasksChanged() {
+        val listener = TestListener()
+        val executor = TestSyncExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+
+        // Model an opening task
+        val firstOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(firstOpeningTransition)
+        callOnTransitionFinished()
+        // Assert that the task is reported visible
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1)
+        assertVisibleTasks(listener, listOf(1))
+
+        // Trigger task vanished
+        val removedTaskInfo = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)
+        transitionObserver.onTaskVanished(removedTaskInfo)
+
+        // Assert that the visible list is now empty
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(2)
+        assertVisibleTasks(listener, listOf())
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+    @EnableFlags(com.android.wm.shell.Flags.FLAG_ENABLE_SHELL_TOP_TASK_TRACKING)
+    fun alwaysOnTop_taskIsTopMostVisible() {
+        val listener = TestListener()
+        val executor = TestSyncExecutor()
+        transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+
+        // Model an opening PIP task
+        val pipOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_PINNED),
+                )
+            )
+
+        callOnTransitionReady(pipOpeningTransition)
+        callOnTransitionFinished()
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(1)
+        assertVisibleTasks(listener, listOf(1))
+
+        // Model an opening fullscreen task
+        val firstOpeningTransition =
+            createTransitionInfo(TRANSIT_OPEN,
+                listOf(
+                    createChange(TRANSIT_OPEN, 2, WINDOWING_MODE_FULLSCREEN),
+                )
+            )
+
+        callOnTransitionReady(firstOpeningTransition)
+        callOnTransitionFinished()
+        assertThat(listener.visibleTasksUpdatedCount).isEqualTo(2)
+        assertVisibleTasks(listener, listOf(1, 2))
+    }
+
+    class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener {
+        // Only used if FLAG_ENABLE_SHELL_TOP_TASK_TRACKING is disabled
+        var taskInfoOnTaskMovedToFront = RunningTaskInfo()
+        var taskInfoOnTaskChanged = mutableListOf<RunningTaskInfo>()
+        // Only used if FLAG_ENABLE_SHELL_TOP_TASK_TRACKING is enabled
+        var visibleTasks = mutableListOf<TaskInfo>()
+        var visibleTasksUpdatedCount = 0
+
+        override fun onTaskMovedToFrontThroughTransition(taskInfo: RunningTaskInfo) {
             taskInfoOnTaskMovedToFront = taskInfo
         }
 
-        override fun onTaskChangedThroughTransition(taskInfo: ActivityManager.RunningTaskInfo) {
+        override fun onTaskChangedThroughTransition(taskInfo: RunningTaskInfo) {
             taskInfoOnTaskChanged += taskInfo
         }
+
+        override fun onVisibleTasksChanged(visibleTasks: List<RunningTaskInfo>) {
+            this.visibleTasks.clear()
+            this.visibleTasks.addAll(visibleTasks)
+            visibleTasksUpdatedCount++
+        }
     }
 
     /** Simulate calling the onTransitionReady() method */
@@ -412,27 +676,64 @@
         transitionObserver.onTransitionMerged(merged, playing)
     }
 
+    /**
+     * Asserts that the listener has the given expected task ids (in order).
+     */
+    private fun assertVisibleTasks(
+        listener: TestListener,
+        expectedVisibleTaskIds: List<Int>
+    ) {
+        assertThat(listener.visibleTasks.size).isEqualTo(expectedVisibleTaskIds.size)
+        expectedVisibleTaskIds.forEachIndexed { index, taskId ->
+            assertThat(listener.visibleTasks[index].taskId).isEqualTo(taskId)
+        }
+    }
+
     companion object {
-        fun createTaskInfo(taskId: Int, windowingMode: Int): ActivityManager.RunningTaskInfo {
-            val taskInfo = ActivityManager.RunningTaskInfo()
+        fun createTaskInfo(taskId: Int, windowingMode: Int): RunningTaskInfo {
+            val taskInfo = RunningTaskInfo()
+            taskInfo.baseIntent = Intent().setComponent(
+                ComponentName(javaClass.packageName, "Test"))
             taskInfo.taskId = taskId
             taskInfo.configuration.windowConfiguration.windowingMode = windowingMode
-
+            if (windowingMode == WINDOWING_MODE_PINNED) {
+                taskInfo.configuration.windowConfiguration.isAlwaysOnTop = true
+            }
             return taskInfo
         }
 
         fun createChange(
             mode: Int,
-            taskInfo: ActivityManager.RunningTaskInfo
+            taskInfo: RunningTaskInfo,
+            flags: Int = 0,
         ): TransitionInfo.Change {
             val change =
                 TransitionInfo.Change(
                     WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)),
                     Mockito.mock(SurfaceControl::class.java)
                 )
+            change.flags = flags
             change.mode = mode
             change.taskInfo = taskInfo
             return change
         }
+
+        fun createChange(
+            mode: Int,
+            taskId: Int,
+            windowingMode: Int,
+            flags: Int = 0,
+        ): TransitionInfo.Change {
+            return createChange(mode, createTaskInfo(taskId, windowingMode), flags)
+        }
+
+        fun createTransitionInfo(
+            transitionType: Int,
+            changes: List<TransitionInfo.Change>
+        ): TransitionInfo {
+            return TransitionInfoBuilder(transitionType, 0)
+                .apply { changes.forEach { c -> this@apply.addChange(c) } }
+                .build()
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java
index c36b88e..71af97e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java
@@ -43,6 +43,7 @@
 import android.window.TransitionRequestInfo;
 import android.window.WindowContainerTransaction;
 
+import com.android.wm.shell.TestSyncExecutor;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.shared.TransactionPool;
 import com.android.wm.shell.sysui.ShellInit;
@@ -475,27 +476,6 @@
         }
     }
 
-    private static class TestSyncExecutor implements ShellExecutor {
-        @Override
-        public void execute(Runnable runnable) {
-            runnable.run();
-        }
-
-        @Override
-        public void executeDelayed(Runnable runnable, long delayMillis) {
-            runnable.run();
-        }
-
-        @Override
-        public void removeCallbacks(Runnable runnable) {
-        }
-
-        @Override
-        public boolean hasCallback(Runnable runnable) {
-            return false;
-        }
-    }
-
     private TransitionInfo createUnfoldTransitionInfo() {
         TransitionInfo transitionInfo = new TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0);
         TransitionInfo.Change change = new TransitionInfo.Change(null, mock(SurfaceControl.class));
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index fcb7efc..e2db2c9 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -355,6 +355,7 @@
         "jni/AnimatedImageDrawable.cpp",
         "jni/Bitmap.cpp",
         "jni/BitmapRegionDecoder.cpp",
+        "jni/RuntimeXfermode.cpp",
         "jni/BufferUtils.cpp",
         "jni/HardwareBufferHelpers.cpp",
         "jni/BitmapFactory.cpp",
diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp
index 15b2bac..56de568 100644
--- a/libs/hwui/apex/jni_runtime.cpp
+++ b/libs/hwui/apex/jni_runtime.cpp
@@ -28,6 +28,7 @@
 extern int register_android_graphics_Bitmap(JNIEnv*);
 extern int register_android_graphics_BitmapFactory(JNIEnv*);
 extern int register_android_graphics_BitmapRegionDecoder(JNIEnv*);
+extern int register_android_graphics_RuntimeXfermode(JNIEnv*);
 extern int register_android_graphics_ByteBufferStreamAdaptor(JNIEnv* env);
 extern int register_android_graphics_Camera(JNIEnv* env);
 extern int register_android_graphics_CreateJavaOutputStreamAdaptor(JNIEnv* env);
@@ -107,6 +108,7 @@
             REG_JNI(register_android_graphics_Bitmap),
             REG_JNI(register_android_graphics_BitmapFactory),
             REG_JNI(register_android_graphics_BitmapRegionDecoder),
+            REG_JNI(register_android_graphics_RuntimeXfermode),
             REG_JNI(register_android_graphics_ByteBufferStreamAdaptor),
             REG_JNI(register_android_graphics_Camera),
             REG_JNI(register_android_graphics_CreateJavaOutputStreamAdaptor),
diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp
index da23792..a7d855d 100644
--- a/libs/hwui/jni/Paint.cpp
+++ b/libs/hwui/jni/Paint.cpp
@@ -906,6 +906,13 @@
         paint->setBlendMode(mode);
     }
 
+    static void setRuntimeXfermode(CRITICAL_JNI_PARAMS_COMMA jlong paintHandle,
+                                   jlong xfermodeHandle) {
+        Paint* paint = reinterpret_cast<Paint*>(paintHandle);
+        SkBlender* blender = reinterpret_cast<SkBlender*>(xfermodeHandle);
+        paint->setBlender(sk_ref_sp(blender));
+    }
+
     static jlong setPathEffect(CRITICAL_JNI_PARAMS_COMMA jlong objHandle, jlong effectHandle) {
         Paint* obj = reinterpret_cast<Paint*>(objHandle);
         SkPathEffect* effect  = reinterpret_cast<SkPathEffect*>(effectHandle);
@@ -1233,6 +1240,7 @@
         {"nSetShader", "(JJ)J", (void*)PaintGlue::setShader},
         {"nSetColorFilter", "(JJ)J", (void*)PaintGlue::setColorFilter},
         {"nSetXfermode", "(JI)V", (void*)PaintGlue::setXfermode},
+        {"nSetXfermode", "(JJ)V", (void*)PaintGlue::setRuntimeXfermode},
         {"nSetPathEffect", "(JJ)J", (void*)PaintGlue::setPathEffect},
         {"nSetMaskFilter", "(JJ)J", (void*)PaintGlue::setMaskFilter},
         {"nSetTypeface", "(JJ)V", (void*)PaintGlue::setTypeface},
diff --git a/libs/hwui/jni/RuntimeXfermode.cpp b/libs/hwui/jni/RuntimeXfermode.cpp
new file mode 100644
index 0000000..c1c8964
--- /dev/null
+++ b/libs/hwui/jni/RuntimeXfermode.cpp
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+#include "GraphicsJNI.h"
+#include "RuntimeEffectUtils.h"
+#include "SkBlender.h"
+
+using namespace android::uirenderer;
+
+static void SkRuntimeEffectBuilder_delete(SkRuntimeEffectBuilder* builder) {
+    delete builder;
+}
+
+static jlong RuntimeXfermode_getNativeFinalizer(JNIEnv*, jobject) {
+    return static_cast<jlong>(reinterpret_cast<uintptr_t>(&SkRuntimeEffectBuilder_delete));
+}
+
+static jlong RuntimeXfermode_createBuilder(JNIEnv* env, jobject, jstring sksl) {
+    ScopedUtfChars strSksl(env, sksl);
+    auto result =
+            SkRuntimeEffect::MakeForBlender(SkString(strSksl.c_str()), SkRuntimeEffect::Options{});
+    if (result.effect.get() == nullptr) {
+        doThrowIAE(env, result.errorText.c_str());
+        return 0;
+    }
+    return reinterpret_cast<jlong>(new SkRuntimeEffectBuilder(std::move(result.effect)));
+}
+
+static jlong RuntimeXfermode_create(JNIEnv* env, jobject, jlong builderPtr) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    sk_sp<SkBlender> blender = builder->makeBlender();
+    if (!blender) {
+        doThrowIAE(env);
+    }
+    return reinterpret_cast<jlong>(blender.release());
+}
+
+static void RuntimeXfermode_updateFloatArrayUniforms(JNIEnv* env, jobject, jlong builderPtr,
+                                                     jstring uniformName, jfloatArray uniforms,
+                                                     jboolean isColor) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    ScopedUtfChars name(env, uniformName);
+    AutoJavaFloatArray autoValues(env, uniforms, 0, kRO_JNIAccess);
+    UpdateFloatUniforms(env, builder, name.c_str(), autoValues.ptr(), autoValues.length(), isColor);
+}
+
+static void RuntimeXfermode_updateFloatUniforms(JNIEnv* env, jobject, jlong builderPtr,
+                                                jstring uniformName, jfloat value1, jfloat value2,
+                                                jfloat value3, jfloat value4, jint count) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    ScopedUtfChars name(env, uniformName);
+    const float values[4] = {value1, value2, value3, value4};
+    UpdateFloatUniforms(env, builder, name.c_str(), values, count, false);
+}
+
+static void RuntimeXfermode_updateIntArrayUniforms(JNIEnv* env, jobject, jlong builderPtr,
+                                                   jstring uniformName, jintArray uniforms) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    ScopedUtfChars name(env, uniformName);
+    AutoJavaIntArray autoValues(env, uniforms, 0);
+    UpdateIntUniforms(env, builder, name.c_str(), autoValues.ptr(), autoValues.length());
+}
+
+static void RuntimeXfermode_updateIntUniforms(JNIEnv* env, jobject, jlong builderPtr,
+                                              jstring uniformName, jint value1, jint value2,
+                                              jint value3, jint value4, jint count) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    ScopedUtfChars name(env, uniformName);
+    const int values[4] = {value1, value2, value3, value4};
+    UpdateIntUniforms(env, builder, name.c_str(), values, count);
+}
+
+static void RuntimeXfermode_updateChild(JNIEnv* env, jobject, jlong builderPtr, jstring childName,
+                                        jlong childPtr) {
+    auto* builder = reinterpret_cast<SkRuntimeEffectBuilder*>(builderPtr);
+    ScopedUtfChars name(env, childName);
+    auto* child = reinterpret_cast<SkFlattenable*>(childPtr);
+    if (child) {
+        UpdateChild(env, builder, name.c_str(), child);
+    }
+}
+
+static const JNINativeMethod gRuntimeXfermodeMethods[] = {
+        {"nativeGetFinalizer", "()J", (void*)RuntimeXfermode_getNativeFinalizer},
+        {"nativeCreateBlenderBuilder", "(Ljava/lang/String;)J",
+         (void*)RuntimeXfermode_createBuilder},
+        {"nativeCreateNativeInstance", "(J)J", (void*)RuntimeXfermode_create},
+        {"nativeUpdateUniforms", "(JLjava/lang/String;[FZ)V",
+         (void*)RuntimeXfermode_updateFloatArrayUniforms},
+        {"nativeUpdateUniforms", "(JLjava/lang/String;FFFFI)V",
+         (void*)RuntimeXfermode_updateFloatUniforms},
+        {"nativeUpdateUniforms", "(JLjava/lang/String;[I)V",
+         (void*)RuntimeXfermode_updateIntArrayUniforms},
+        {"nativeUpdateUniforms", "(JLjava/lang/String;IIIII)V",
+         (void*)RuntimeXfermode_updateIntUniforms},
+        {"nativeUpdateChild", "(JLjava/lang/String;J)V", (void*)RuntimeXfermode_updateChild},
+};
+
+int register_android_graphics_RuntimeXfermode(JNIEnv* env) {
+    android::RegisterMethodsOrDie(env, "android/graphics/RuntimeXfermode", gRuntimeXfermodeMethods,
+                                  NELEM(gRuntimeXfermodeMethods));
+
+    return 0;
+}
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
index 5ceee6d..088bef2 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
@@ -22,11 +22,13 @@
 import com.android.settingslib.ipc.ApiHandler
 import com.android.settingslib.ipc.MessageCodec
 import com.android.settingslib.metadata.PreferenceScreenRegistry
+import com.android.settingslib.preference.PreferenceScreenProvider
 import java.util.Locale
 
 /** API to get preference graph. */
-abstract class GetPreferenceGraphApiHandler :
-    ApiHandler<GetPreferenceGraphRequest, PreferenceGraphProto> {
+abstract class GetPreferenceGraphApiHandler(
+    private val preferenceScreenProviders: Set<Class<out PreferenceScreenProvider>>
+) : ApiHandler<GetPreferenceGraphRequest, PreferenceGraphProto> {
 
     override val requestCodec: MessageCodec<GetPreferenceGraphRequest>
         get() = GetPreferenceGraphRequestCodec
@@ -40,14 +42,16 @@
         callingUid: Int,
         request: GetPreferenceGraphRequest,
     ): PreferenceGraphProto {
-        val builderRequest =
-            if (request.screenKeys.isEmpty()) {
-                val keys = PreferenceScreenRegistry.preferenceScreens.keys
-                GetPreferenceGraphRequest(keys, request.visitedScreens, request.locale)
-            } else {
-                request
+        val builder = PreferenceGraphBuilder.of(application, request)
+        if (request.screenKeys.isEmpty()) {
+            for (key in PreferenceScreenRegistry.preferenceScreens.keys) {
+                builder.addPreferenceScreenFromRegistry(key)
             }
-        return PreferenceGraphBuilder.of(application, builderRequest).build()
+            for (provider in preferenceScreenProviders) {
+                builder.addPreferenceScreenProvider(provider)
+            }
+        }
+        return builder.build()
     }
 }
 
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
index 2256bb3..6760e72 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
@@ -133,7 +133,7 @@
             null
         }
 
-    private suspend fun addPreferenceScreenFromRegistry(key: String): Boolean {
+    suspend fun addPreferenceScreenFromRegistry(key: String): Boolean {
         val metadata = PreferenceScreenRegistry[key] ?: return false
         return addPreferenceScreenMetadata(metadata)
     }
@@ -146,7 +146,7 @@
             }
         }
 
-    private suspend fun addPreferenceScreenProvider(activityClass: Class<*>) {
+    suspend fun addPreferenceScreenProvider(activityClass: Class<*>) {
         Log.d(TAG, "add $activityClass")
         createPreferenceScreen { activityClass.newInstance() }
             ?.let {
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
index 6e4db1d..7cfce0d 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt
@@ -22,6 +22,7 @@
 import com.android.settingslib.graph.proto.PreferenceValueProto
 import com.android.settingslib.ipc.ApiDescriptor
 import com.android.settingslib.ipc.ApiHandler
+import com.android.settingslib.ipc.ApiPermissionChecker
 import com.android.settingslib.ipc.IntMessageCodec
 import com.android.settingslib.ipc.MessageCodec
 import com.android.settingslib.metadata.BooleanValue
@@ -45,7 +46,11 @@
     PreferenceSetterResult.OK,
     PreferenceSetterResult.UNSUPPORTED,
     PreferenceSetterResult.DISABLED,
+    PreferenceSetterResult.RESTRICTED,
     PreferenceSetterResult.UNAVAILABLE,
+    PreferenceSetterResult.REQUIRE_APP_PERMISSION,
+    PreferenceSetterResult.REQUIRE_USER_AGREEMENT,
+    PreferenceSetterResult.DISALLOW,
     PreferenceSetterResult.INVALID_REQUEST,
     PreferenceSetterResult.INTERNAL_ERROR,
 )
@@ -87,14 +92,17 @@
 }
 
 /** Preference setter API implementation. */
-class PreferenceSetterApiHandler(override val id: Int) : ApiHandler<PreferenceSetterRequest, Int> {
+class PreferenceSetterApiHandler(
+    override val id: Int,
+    private val permissionChecker: ApiPermissionChecker<PreferenceSetterRequest>,
+) : ApiHandler<PreferenceSetterRequest, Int> {
 
     override fun hasPermission(
         application: Application,
         myUid: Int,
         callingUid: Int,
         request: PreferenceSetterRequest,
-    ): Boolean = true
+    ) = permissionChecker.hasPermission(application, myUid, callingUid, request)
 
     override suspend fun invoke(
         application: Application,
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt
index d9b9590..1bda277 100644
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt
@@ -16,8 +16,10 @@
 
 package com.android.settingslib.graph
 
+import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.net.Uri
 import android.os.Bundle
 import com.android.settingslib.graph.proto.BundleProto
 import com.android.settingslib.graph.proto.BundleProto.BundleValue
@@ -42,6 +44,20 @@
     this@toProto.type?.let { mimeType = it }
 }
 
+fun IntentProto.toIntent(): Intent? {
+    if (!hasComponent()) return null
+    val componentName = ComponentName.unflattenFromString(component) ?: return null
+    val intent = Intent()
+    intent.component = componentName
+    if (hasAction()) intent.action = action
+    if (hasData()) intent.data = Uri.parse(data)
+    if (hasPkg()) intent.`package` = pkg
+    if (hasFlags()) intent.flags = flags
+    if (hasExtras()) intent.putExtras(extras.toBundle())
+    if (hasMimeType()) intent.setType(mimeType)
+    return intent
+}
+
 fun Bundle.toProto(): BundleProto = bundleProto {
     fun toProto(value: Any): BundleValue = bundleValueProto {
         when (value) {
@@ -61,14 +77,18 @@
     }
 }
 
-fun BundleValue.stringify(): String =
-    when {
-        hasBooleanValue() -> "$valueCase"
-        hasBytesValue() -> "$bytesValue"
-        hasIntValue() -> "$intValue"
-        hasLongValue() -> "$longValue"
-        hasStringValue() -> stringValue
-        hasDoubleValue() -> "$doubleValue"
-        hasBundleValue() -> "$bundleValue"
-        else -> "Unknown"
+fun BundleProto.toBundle(): Bundle =
+    Bundle().apply {
+        for ((key, value) in valuesMap) {
+            when {
+                value.hasBooleanValue() -> putBoolean(key, value.booleanValue)
+                value.hasBytesValue() -> putByteArray(key, value.bytesValue.toByteArray())
+                value.hasIntValue() -> putInt(key, value.intValue)
+                value.hasLongValue() -> putLong(key, value.longValue)
+                value.hasStringValue() -> putString(key, value.stringValue)
+                value.hasDoubleValue() -> putDouble(key, value.doubleValue)
+                value.hasBundleValue() -> putBundle(key, value.bundleValue.toBundle())
+                else -> throw IllegalArgumentException("Unknown type: ${value.javaClass} $value")
+            }
+        }
     }
diff --git a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiHandler.kt b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiHandler.kt
index 802141d..4febd89 100644
--- a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiHandler.kt
+++ b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiHandler.kt
@@ -56,6 +56,27 @@
     val responseCodec: MessageCodec<Response>
 }
 
+/** Permission checker for api. */
+fun interface ApiPermissionChecker<R> {
+    /**
+     * Returns if the request is permitted.
+     *
+     * @param application application context
+     * @param myUid uid of current process
+     * @param callingUid uid of peer process
+     * @param request API request
+     * @return `false` if permission is denied, otherwise `true`
+     */
+    fun hasPermission(application: Application, myUid: Int, callingUid: Int, request: R): Boolean
+
+    companion object {
+        private val ALWAYS_ALLOW = ApiPermissionChecker<Any> { _, _, _, _ -> true }
+
+        @Suppress("UNCHECKED_CAST")
+        fun <T> alwaysAllow(): ApiPermissionChecker<T> = ALWAYS_ALLOW as ApiPermissionChecker<T>
+    }
+}
+
 /**
  * Handler of API.
  *
@@ -64,18 +85,8 @@
  *
  * The implementation must be threadsafe.
  */
-interface ApiHandler<Request, Response> : ApiDescriptor<Request, Response> {
-    /**
-     * Returns if the request is permitted.
-     *
-     * @return `false` if permission is denied, otherwise `true`
-     */
-    fun hasPermission(
-        application: Application,
-        myUid: Int,
-        callingUid: Int,
-        request: Request,
-    ): Boolean
+interface ApiHandler<Request, Response> :
+    ApiDescriptor<Request, Response>, ApiPermissionChecker<Request> {
 
     /**
      * Invokes the API.
diff --git a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerServiceClient.kt b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerServiceClient.kt
index 7ffefed..ef907e1 100644
--- a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerServiceClient.kt
+++ b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerServiceClient.kt
@@ -320,6 +320,11 @@
             }
         }
 
+        override fun onNullBinding(name: ComponentName) {
+            Log.i(TAG, "onNullBinding $name")
+            close(ClientBindServiceException(null))
+        }
+
         internal open fun drainPendingRequests() {
             disposableHandle = null
             if (pendingRequests.isEmpty()) {
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt
index 6e38df1..1823ba6 100644
--- a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt
@@ -19,9 +19,14 @@
 import android.app.Application
 import com.android.settingslib.graph.GetPreferenceGraphApiHandler
 import com.android.settingslib.graph.GetPreferenceGraphRequest
+import com.android.settingslib.ipc.ApiPermissionChecker
+import com.android.settingslib.preference.PreferenceScreenProvider
 
 /** Api to get preference graph. */
-internal class PreferenceGraphApi : GetPreferenceGraphApiHandler() {
+internal class PreferenceGraphApi(
+    preferenceScreenProviders: Set<Class<out PreferenceScreenProvider>>,
+    private val permissionChecker: ApiPermissionChecker<GetPreferenceGraphRequest>,
+) : GetPreferenceGraphApiHandler(preferenceScreenProviders) {
 
     override val id: Int
         get() = API_GET_PREFERENCE_GRAPH
@@ -31,5 +36,5 @@
         myUid: Int,
         callingUid: Int,
         request: GetPreferenceGraphRequest,
-    ) = true
+    ) = permissionChecker.hasPermission(application, myUid, callingUid, request)
 }
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt
index 8ebb145..ed748bb 100644
--- a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt
@@ -16,10 +16,14 @@
 
 package com.android.settingslib.service
 
+import com.android.settingslib.graph.GetPreferenceGraphRequest
 import com.android.settingslib.graph.PreferenceSetterApiHandler
+import com.android.settingslib.graph.PreferenceSetterRequest
 import com.android.settingslib.ipc.ApiHandler
+import com.android.settingslib.ipc.ApiPermissionChecker
 import com.android.settingslib.ipc.MessengerService
 import com.android.settingslib.ipc.PermissionChecker
+import com.android.settingslib.preference.PreferenceScreenProvider
 
 /**
  * Preference service providing a bunch of APIs.
@@ -28,14 +32,21 @@
  * [PREFERENCE_SERVICE_ACTION].
  */
 open class PreferenceService(
-    permissionChecker: PermissionChecker,
     name: String = "PreferenceService",
+    permissionChecker: PermissionChecker = PermissionChecker { _, _, _ -> true },
+    preferenceScreenProviders: Set<Class<out PreferenceScreenProvider>> = setOf(),
+    graphPermissionChecker: ApiPermissionChecker<GetPreferenceGraphRequest>? = null,
+    setterPermissionChecker: ApiPermissionChecker<PreferenceSetterRequest>? = null,
+    vararg apiHandlers: ApiHandler<*, *>,
 ) :
     MessengerService(
-        listOf<ApiHandler<*, *>>(
-            PreferenceGraphApi(),
-            PreferenceSetterApiHandler(API_PREFERENCE_SETTER),
-        ),
+        mutableListOf<ApiHandler<*, *>>().apply {
+            graphPermissionChecker?.let { add(PreferenceGraphApi(preferenceScreenProviders, it)) }
+            setterPermissionChecker?.let {
+                add(PreferenceSetterApiHandler(API_PREFERENCE_SETTER, it))
+            }
+            addAll(apiHandlers)
+        },
         permissionChecker,
         name,
     )
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt
index 1f38a66..7655daa 100644
--- a/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt
@@ -18,5 +18,14 @@
 
 const val PREFERENCE_SERVICE_ACTION = "com.android.settingslib.PREFERENCE_SERVICE"
 
+/** API id for retrieving preference graph. */
 internal const val API_GET_PREFERENCE_GRAPH = 1
+
+/** API id for preference value setter. */
 internal const val API_PREFERENCE_SETTER = 2
+
+/**
+ * The max API id reserved for internal preference service usages. Custom API id should start with
+ * **1000** to avoid conflict.
+ */
+internal const val API_MAX_RESERVED = 999
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 7b6321d..859445e 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -953,6 +953,13 @@
     <uses-permission android:name="android.permission.QUERY_ADVANCED_PROTECTION_MODE"
         android:featureFlag="android.security.aapm_api"/>
 
+    <!-- Permission required for CTS test - ForensicManagerTest -->
+    <uses-permission android:name="android.permission.READ_FORENSIC_STATE"
+        android:featureFlag="android.security.afl_api"/>
+    <uses-permission android:name="android.permission.MANAGE_FORENSIC_STATE"
+        android:featureFlag="android.security.afl_api"/>
+
+
     <!-- Permission required for CTS test - CtsAppTestCases -->
     <uses-permission android:name="android.permission.KILL_UID" />
 
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt
deleted file mode 100644
index 2a2d333..0000000
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AssetLoader.kt
+++ /dev/null
@@ -1,252 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.shared.clocks
-
-import android.content.Context
-import android.content.res.Resources
-import android.graphics.Typeface
-import android.graphics.drawable.Drawable
-import android.util.TypedValue
-import com.android.internal.policy.SystemBarUtils
-import com.android.systemui.log.core.Logger
-import com.android.systemui.log.core.MessageBuffer
-import com.android.systemui.monet.Style as MonetStyle
-import java.io.IOException
-
-class AssetLoader
-private constructor(
-    private val pluginCtx: Context,
-    private val sysuiCtx: Context,
-    private val baseDir: String,
-    var seedColor: Int?,
-    var overrideChroma: Float?,
-    val typefaceCache: TypefaceCache,
-    messageBuffer: MessageBuffer,
-) {
-    val logger = Logger(messageBuffer, TAG)
-    private val resources =
-        listOf(
-            Pair(pluginCtx.resources, pluginCtx.packageName),
-            Pair(sysuiCtx.resources, sysuiCtx.packageName),
-        )
-
-    constructor(
-        pluginCtx: Context,
-        sysuiCtx: Context,
-        baseDir: String,
-        messageBuffer: MessageBuffer,
-    ) : this(
-        pluginCtx,
-        sysuiCtx,
-        baseDir,
-        seedColor = null,
-        overrideChroma = null,
-        typefaceCache =
-            TypefaceCache(messageBuffer) {
-                // TODO(b/364680873): Move constant to config_clockFontFamily when shipping
-                return@TypefaceCache Typeface.create("google-sans-flex-clock", Typeface.NORMAL)
-            },
-        messageBuffer = messageBuffer,
-    )
-
-    fun listAssets(path: String): List<String> {
-        return pluginCtx.resources.assets.list("$baseDir$path")?.toList() ?: emptyList()
-    }
-
-    fun tryReadString(resStr: String): String? = tryRead(resStr, ::readString)
-
-    fun readString(resStr: String): String {
-        val resPair = resolveResourceId(resStr)
-        if (resPair == null) {
-            throw IOException("Failed to parse string: $resStr")
-        }
-
-        val (res, id) = resPair
-        return res.getString(id)
-    }
-
-    fun readFontAsset(resStr: String): Typeface = typefaceCache.getTypeface(resStr)
-
-    fun tryReadTextAsset(path: String?): String? = tryRead(path, ::readTextAsset)
-
-    fun readTextAsset(path: String): String {
-        return pluginCtx.resources.assets.open("$baseDir$path").use { stream ->
-            val buffer = ByteArray(stream.available())
-            stream.read(buffer)
-            String(buffer)
-        }
-    }
-
-    fun tryReadDrawableAsset(path: String?): Drawable? = tryRead(path, ::readDrawableAsset)
-
-    fun readDrawableAsset(path: String): Drawable {
-        var result: Drawable?
-
-        if (path.startsWith("@")) {
-            val pair = resolveResourceId(path)
-            if (pair == null) {
-                throw IOException("Failed to parse $path to an id")
-            }
-            val (res, id) = pair
-            result = res.getDrawable(id)
-        } else if (path.endsWith("xml")) {
-            // TODO(b/248609434): Support xml files in assets
-            throw IOException("Cannot load xml files from assets")
-        } else {
-            // Attempt to load as if it's a bitmap and directly loadable
-            result =
-                pluginCtx.resources.assets.open("$baseDir$path").use { stream ->
-                    Drawable.createFromResourceStream(
-                        pluginCtx.resources,
-                        TypedValue(),
-                        stream,
-                        null,
-                    )
-                }
-        }
-
-        return result ?: throw IOException("Failed to load: $baseDir$path")
-    }
-
-    fun parseResourceId(resStr: String): Triple<String?, String, String> {
-        if (!resStr.startsWith("@")) {
-            throw IOException("Invalid resource id: $resStr; Must start with '@'")
-        }
-
-        // Parse out resource string
-        val parts = resStr.drop(1).split('/', ':')
-        return when (parts.size) {
-            2 -> Triple(null, parts[0], parts[1])
-            3 -> Triple(parts[0], parts[1], parts[2])
-            else -> throw IOException("Failed to parse resource string: $resStr")
-        }
-    }
-
-    fun resolveResourceId(resStr: String): Pair<Resources, Int>? {
-        val (packageName, category, name) = parseResourceId(resStr)
-        return resolveResourceId(packageName, category, name)
-    }
-
-    fun resolveResourceId(
-        packageName: String?,
-        category: String,
-        name: String,
-    ): Pair<Resources, Int>? {
-        for ((res, ctxPkgName) in resources) {
-            val result = res.getIdentifier(name, category, packageName ?: ctxPkgName)
-            if (result != 0) {
-                return Pair(res, result)
-            }
-        }
-        return null
-    }
-
-    private fun <TArg : Any, TRes : Any> tryRead(arg: TArg?, fn: (TArg) -> TRes): TRes? {
-        try {
-            if (arg == null) {
-                return null
-            }
-            return fn(arg)
-        } catch (ex: IOException) {
-            logger.w("Failed to read $arg", ex)
-            return null
-        }
-    }
-
-    fun assetExists(path: String): Boolean {
-        try {
-            if (path.startsWith("@")) {
-                val pair = resolveResourceId(path)
-                return pair != null
-            } else {
-                val stream = pluginCtx.resources.assets.open("$baseDir$path")
-                if (stream == null) {
-                    return false
-                }
-
-                stream.close()
-                return true
-            }
-        } catch (ex: IOException) {
-            return false
-        }
-    }
-
-    fun copy(messageBuffer: MessageBuffer? = null): AssetLoader =
-        AssetLoader(
-            pluginCtx,
-            sysuiCtx,
-            baseDir,
-            seedColor,
-            overrideChroma,
-            typefaceCache,
-            messageBuffer ?: logger.buffer,
-        )
-
-    fun setSeedColor(seedColor: Int?, style: MonetStyle?) {
-        this.seedColor = seedColor
-    }
-
-    fun getClockPaddingStart(): Int {
-        val result = resolveResourceId(null, "dimen", "clock_padding_start")
-        if (result != null) {
-            val (res, id) = result
-            return res.getDimensionPixelSize(id)
-        }
-        return -1
-    }
-
-    fun getStatusBarHeight(): Int {
-        val display = pluginCtx.getDisplayNoVerify()
-        if (display != null) {
-            return SystemBarUtils.getStatusBarHeight(pluginCtx.resources, display.cutout)
-        }
-
-        logger.w("No display available; falling back to android.R.dimen.status_bar_height")
-        val statusBarHeight = resolveResourceId("android", "dimen", "status_bar_height")
-        if (statusBarHeight != null) {
-            val (res, resId) = statusBarHeight
-            return res.getDimensionPixelSize(resId)
-        }
-
-        throw Exception("Could not fetch StatusBarHeight")
-    }
-
-    fun getResourcesId(name: String): Int = getResource("id", name) { _, id -> id }
-
-    fun getDimen(name: String): Int = getResource("dimen", name, Resources::getDimensionPixelSize)
-
-    fun getString(name: String): String = getResource("string", name, Resources::getString)
-
-    private fun <T> getResource(
-        category: String,
-        name: String,
-        getter: (res: Resources, id: Int) -> T,
-    ): T {
-        val result = resolveResourceId(null, category, name)
-        if (result != null) {
-            val (res, id) = result
-            if (id == -1) throw Exception("Cannot find id of $id from $TAG")
-            return getter(res, id)
-        }
-        throw Exception("Cannot find id of $name from $TAG")
-    }
-
-    companion object {
-        private val TAG = AssetLoader::class.simpleName!!
-    }
-}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
index 4ed8fd8..d0a32dc 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
@@ -16,12 +16,9 @@
 
 package com.android.systemui.shared.clocks
 
-import android.content.Context
-import android.content.res.Resources
 import android.graphics.Rect
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.log.core.Logger
-import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.clocks.AlarmData
 import com.android.systemui.plugins.clocks.ClockAnimations
 import com.android.systemui.plugins.clocks.ClockEvents
@@ -37,31 +34,22 @@
 import java.util.TimeZone
 
 class ComposedDigitalLayerController(
-    private val ctx: Context,
-    private val resources: Resources,
-    private val assets: AssetLoader, // TODO(b/364680879): Remove and replace w/ resources
+    private val clockCtx: ClockContext,
     private val layer: ComposedDigitalHandLayer,
-    messageBuffer: MessageBuffer,
 ) : SimpleClockLayerController {
-    private val logger = Logger(messageBuffer, ComposedDigitalLayerController::class.simpleName!!)
+    private val logger =
+        Logger(clockCtx.messageBuffer, ComposedDigitalLayerController::class.simpleName!!)
 
     val layerControllers = mutableListOf<SimpleClockLayerController>()
     val dozeState = DefaultClockController.AnimationState(1F)
 
-    override val view = FlexClockView(ctx, assets, messageBuffer)
+    override val view = FlexClockView(clockCtx)
 
     init {
         layer.digitalLayers.forEach {
-            val childView = SimpleDigitalClockTextView(ctx, messageBuffer)
+            val childView = SimpleDigitalClockTextView(clockCtx)
             val controller =
-                SimpleDigitalHandLayerController(
-                    ctx,
-                    resources,
-                    assets,
-                    it as DigitalHandLayer,
-                    childView,
-                    messageBuffer,
-                )
+                SimpleDigitalHandLayerController(clockCtx, it as DigitalHandLayer, childView)
 
             view.addView(childView)
             layerControllers.add(controller)
@@ -156,8 +144,9 @@
                 val color =
                     when {
                         theme.seedColor != null -> theme.seedColor!!
-                        theme.isDarkTheme -> resources.getColor(android.R.color.system_accent1_100)
-                        else -> resources.getColor(android.R.color.system_accent2_600)
+                        theme.isDarkTheme ->
+                            clockCtx.resources.getColor(android.R.color.system_accent1_100)
+                        else -> clockCtx.resources.getColor(android.R.color.system_accent2_600)
                     }
 
                 view.updateColor(color)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index be4ebdf..935737c 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -15,10 +15,10 @@
 
 import android.content.Context
 import android.content.res.Resources
+import android.graphics.Typeface
 import android.view.LayoutInflater
 import com.android.systemui.customization.R
-import com.android.systemui.log.core.LogLevel
-import com.android.systemui.log.core.LogcatOnlyMessageBuffer
+import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.plugins.clocks.ClockFontAxis
 import com.android.systemui.plugins.clocks.ClockFontAxisSetting
@@ -33,6 +33,15 @@
 private val TAG = DefaultClockProvider::class.simpleName
 const val DEFAULT_CLOCK_ID = "DEFAULT"
 
+data class ClockContext(
+    val context: Context,
+    val resources: Resources,
+    val settings: ClockSettings,
+    val typefaceCache: TypefaceCache,
+    val messageBuffers: ClockMessageBuffers,
+    val messageBuffer: MessageBuffer,
+)
+
 /** Provides the default system clock */
 class DefaultClockProvider(
     val ctx: Context,
@@ -55,18 +64,24 @@
         }
 
         return if (isClockReactiveVariantsEnabled) {
-            val buffer =
-                messageBuffers?.infraMessageBuffer ?: LogcatOnlyMessageBuffer(LogLevel.INFO)
-            val assets = AssetLoader(ctx, ctx, "clocks/", buffer)
-            assets.setSeedColor(settings.seedColor, null)
+            val buffers = messageBuffers ?: ClockMessageBuffers(LogUtil.DEFAULT_MESSAGE_BUFFER)
             val fontAxes = ClockFontAxis.merge(FlexClockController.FONT_AXES, settings.axes)
+            val clockSettings = settings.copy(axes = fontAxes.map { it.toSetting() })
+            val typefaceCache =
+                TypefaceCache(buffers.infraMessageBuffer) {
+                    // TODO(b/364680873): Move constant to config_clockFontFamily when shipping
+                    return@TypefaceCache Typeface.create("google-sans-flex-clock", Typeface.NORMAL)
+                }
             FlexClockController(
-                ctx,
-                resources,
-                settings.copy(axes = fontAxes.map { it.toSetting() }),
-                assets,
+                ClockContext(
+                    ctx,
+                    resources,
+                    clockSettings,
+                    typefaceCache,
+                    buffers,
+                    buffers.infraMessageBuffer,
+                ),
                 FLEX_DESIGN,
-                messageBuffers,
             )
         } else {
             DefaultClockController(
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
index 6c627e2..c7a3f63 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
@@ -16,8 +16,6 @@
 
 package com.android.systemui.shared.clocks
 
-import android.content.Context
-import android.content.res.Resources
 import com.android.systemui.customization.R
 import com.android.systemui.plugins.clocks.AlarmData
 import com.android.systemui.plugins.clocks.AxisType
@@ -26,8 +24,6 @@
 import com.android.systemui.plugins.clocks.ClockEvents
 import com.android.systemui.plugins.clocks.ClockFontAxis
 import com.android.systemui.plugins.clocks.ClockFontAxisSetting
-import com.android.systemui.plugins.clocks.ClockMessageBuffers
-import com.android.systemui.plugins.clocks.ClockSettings
 import com.android.systemui.plugins.clocks.ThemeConfig
 import com.android.systemui.plugins.clocks.WeatherData
 import com.android.systemui.plugins.clocks.ZenData
@@ -38,42 +34,28 @@
 
 /** Controller for the default flex clock */
 class FlexClockController(
-    private val ctx: Context,
-    private val resources: Resources,
-    private val settings: ClockSettings,
-    private val assets: AssetLoader, // TODO(b/364680879): Remove and replace w/ resources
+    private val clockCtx: ClockContext,
     val design: ClockDesign, // TODO(b/364680879): Remove when done inlining
-    val messageBuffers: ClockMessageBuffers?,
 ) : ClockController {
-    override val smallClock = run {
-        val buffer = messageBuffers?.smallClockMessageBuffer ?: LogUtil.DEFAULT_MESSAGE_BUFFER
+    override val smallClock =
         FlexClockFaceController(
-            ctx,
-            resources,
-            assets.copy(messageBuffer = buffer),
+            clockCtx.copy(messageBuffer = clockCtx.messageBuffers.smallClockMessageBuffer),
             design.small ?: design.large!!,
-            false,
-            buffer,
+            isLargeClock = false,
         )
-    }
 
-    override val largeClock = run {
-        val buffer = messageBuffers?.largeClockMessageBuffer ?: LogUtil.DEFAULT_MESSAGE_BUFFER
+    override val largeClock =
         FlexClockFaceController(
-            ctx,
-            resources,
-            assets.copy(messageBuffer = buffer),
+            clockCtx.copy(messageBuffer = clockCtx.messageBuffers.largeClockMessageBuffer),
             design.large ?: design.small!!,
-            true,
-            buffer,
+            isLargeClock = true,
         )
-    }
 
     override val config: ClockConfig by lazy {
         ClockConfig(
             DEFAULT_CLOCK_ID,
-            resources.getString(R.string.clock_default_name),
-            resources.getString(R.string.clock_default_description),
+            clockCtx.resources.getString(R.string.clock_default_name),
+            clockCtx.resources.getString(R.string.clock_default_description),
             isReactiveToTone = true,
         )
     }
@@ -125,8 +107,8 @@
         }
 
     override fun initialize(isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float) {
-        val theme = ThemeConfig(isDarkTheme, assets.seedColor)
-        events.onFontAxesChanged(settings.axes)
+        val theme = ThemeConfig(isDarkTheme, clockCtx.settings.seedColor)
+        events.onFontAxesChanged(clockCtx.settings.axes)
 
         smallClock.run {
             events.onThemeChanged(theme)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
index ee21ea6..a8890e6 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
@@ -16,15 +16,12 @@
 
 package com.android.systemui.shared.clocks
 
-import android.content.Context
-import android.content.res.Resources
 import android.graphics.Rect
 import android.view.Gravity
 import android.view.View
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.widget.FrameLayout
 import com.android.systemui.customization.R
-import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.clocks.AlarmData
 import com.android.systemui.plugins.clocks.ClockAnimations
 import com.android.systemui.plugins.clocks.ClockEvents
@@ -45,22 +42,19 @@
 
 // TODO(b/364680879): Merge w/ ComposedDigitalLayerController
 class FlexClockFaceController(
-    ctx: Context,
-    private val resources: Resources,
-    val assets: AssetLoader, // TODO(b/364680879): Remove and replace w/ resources
+    clockCtx: ClockContext,
     face: ClockFace,
     private val isLargeClock: Boolean,
-    messageBuffer: MessageBuffer,
 ) : ClockFaceController {
     override val view: View
         get() = layerController.view
 
     override val config = ClockFaceConfig(hasCustomPositionUpdatedAnimation = true)
 
-    override var theme = ThemeConfig(true, assets.seedColor)
+    override var theme = ThemeConfig(true, clockCtx.settings.seedColor)
 
     private val keyguardLargeClockTopMargin =
-        resources.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin)
+        clockCtx.resources.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin)
     val layerController: SimpleClockLayerController
     val timespecHandler = DigitalTimespecHandler(DigitalTimespec.TIME_FULL_FORMAT, "hh:mm")
 
@@ -72,23 +66,10 @@
 
         layerController =
             if (isLargeClock) {
-                ComposedDigitalLayerController(
-                    ctx,
-                    resources,
-                    assets,
-                    layer as ComposedDigitalHandLayer,
-                    messageBuffer,
-                )
+                ComposedDigitalLayerController(clockCtx, layer as ComposedDigitalHandLayer)
             } else {
-                val childView = SimpleDigitalClockTextView(ctx, messageBuffer)
-                SimpleDigitalHandLayerController(
-                    ctx,
-                    resources,
-                    assets,
-                    layer as DigitalHandLayer,
-                    childView,
-                    messageBuffer,
-                )
+                val childView = SimpleDigitalClockTextView(clockCtx)
+                SimpleDigitalHandLayerController(clockCtx, layer as DigitalHandLayer, childView)
             }
         layerController.view.layoutParams = lp
     }
@@ -97,7 +78,7 @@
     private fun offsetGlyphsForStepClockAnimation(
         clockStartLeft: Int,
         direction: Int,
-        fraction: Float
+        fraction: Float,
     ) {
         (view as? FlexClockView)?.offsetGlyphsForStepClockAnimation(
             clockStartLeft,
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
index 143b28f..ebac4b24 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
@@ -16,8 +16,6 @@
 
 package com.android.systemui.shared.clocks
 
-import android.content.Context
-import android.content.res.Resources
 import android.graphics.Rect
 import android.view.View
 import android.view.ViewGroup
@@ -25,7 +23,6 @@
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.customization.R
 import com.android.systemui.log.core.Logger
-import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.clocks.AlarmData
 import com.android.systemui.plugins.clocks.ClockAnimations
 import com.android.systemui.plugins.clocks.ClockEvents
@@ -42,14 +39,11 @@
 private val TAG = SimpleDigitalHandLayerController::class.simpleName!!
 
 open class SimpleDigitalHandLayerController<T>(
-    private val ctx: Context,
-    private val resources: Resources,
-    private val assets: AssetLoader, // TODO(b/364680879): Remove and replace w/ resources
+    private val clockCtx: ClockContext,
     private val layer: DigitalHandLayer,
     override val view: T,
-    messageBuffer: MessageBuffer,
 ) : SimpleClockLayerController where T : View, T : SimpleDigitalClockView {
-    private val logger = Logger(messageBuffer, TAG)
+    private val logger = Logger(clockCtx.messageBuffer, TAG)
     val timespec = DigitalTimespecHandler(layer.timespec, layer.dateTimeFormat)
 
     @VisibleForTesting
@@ -75,12 +69,12 @@
             layer.alignment.verticalAlignment?.let { view.verticalAlignment = it }
             layer.alignment.horizontalAlignment?.let { view.horizontalAlignment = it }
         }
-        view.applyStyles(assets, layer.style, layer.aodStyle)
+        view.applyStyles(layer.style, layer.aodStyle)
         view.id =
-            ctx.resources.getIdentifier(
+            clockCtx.resources.getIdentifier(
                 generateDigitalLayerIdString(layer),
                 "id",
-                ctx.getPackageName(),
+                clockCtx.context.getPackageName(),
             )
     }
 
@@ -306,8 +300,9 @@
                 val color =
                     when {
                         theme.seedColor != null -> theme.seedColor!!
-                        theme.isDarkTheme -> resources.getColor(android.R.color.system_accent1_100)
-                        else -> resources.getColor(android.R.color.system_accent2_600)
+                        theme.isDarkTheme ->
+                            clockCtx.resources.getColor(android.R.color.system_accent1_100)
+                        else -> clockCtx.resources.getColor(android.R.color.system_accent2_600)
                     }
 
                 view.updateColor(color)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
index b09332f..d4eb767 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt
@@ -16,24 +16,23 @@
 
 package com.android.systemui.shared.clocks.view
 
-import android.content.Context
 import android.graphics.Canvas
 import android.graphics.Point
 import android.view.View
 import android.widget.FrameLayout
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.log.core.Logger
-import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.clocks.AlarmData
 import com.android.systemui.plugins.clocks.ClockFontAxisSetting
 import com.android.systemui.plugins.clocks.WeatherData
 import com.android.systemui.plugins.clocks.ZenData
+import com.android.systemui.shared.clocks.ClockContext
 import com.android.systemui.shared.clocks.LogUtil
 import java.util.Locale
 
 // TODO(b/364680879): Merge w/ only subclass FlexClockView
-abstract class DigitalClockFaceView(ctx: Context, messageBuffer: MessageBuffer) : FrameLayout(ctx) {
-    protected val logger = Logger(messageBuffer, this::class.simpleName!!)
+abstract class DigitalClockFaceView(clockCtx: ClockContext) : FrameLayout(clockCtx.context) {
+    protected val logger = Logger(clockCtx.messageBuffer, this::class.simpleName!!)
         get() = field ?: LogUtil.FALLBACK_INIT_LOGGER
 
     abstract var digitalClockTextViewMap: MutableMap<Int, SimpleDigitalClockTextView>
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
index 593eba9..27ed099 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.shared.clocks.view
 
-import android.content.Context
 import android.graphics.Canvas
 import android.graphics.Point
 import android.util.MathUtils.constrainedMap
@@ -25,8 +24,7 @@
 import android.widget.RelativeLayout
 import com.android.app.animation.Interpolators
 import com.android.systemui.customization.R
-import com.android.systemui.log.core.MessageBuffer
-import com.android.systemui.shared.clocks.AssetLoader
+import com.android.systemui.shared.clocks.ClockContext
 import com.android.systemui.shared.clocks.DigitTranslateAnimator
 import kotlin.math.abs
 import kotlin.math.max
@@ -34,8 +32,7 @@
 
 fun clamp(value: Float, minVal: Float, maxVal: Float): Float = max(min(value, maxVal), minVal)
 
-class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: MessageBuffer) :
-    DigitalClockFaceView(context, messageBuffer) {
+class FlexClockView(clockCtx: ClockContext) : DigitalClockFaceView(clockCtx) {
     override var digitalClockTextViewMap = mutableMapOf<Int, SimpleDigitalClockTextView>()
     val digitLeftTopMap = mutableMapOf<Int, Point>()
     var maxSingleDigitSize = Point(-1, -1)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index 5c84f2d..bd56dbf 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.shared.clocks.view
 
 import android.annotation.SuppressLint
-import android.content.Context
 import android.graphics.Canvas
 import android.graphics.Color
 import android.graphics.Paint
@@ -37,13 +36,11 @@
 import android.widget.TextView
 import com.android.internal.annotations.VisibleForTesting
 import com.android.systemui.animation.TextAnimator
-import com.android.systemui.animation.TypefaceVariantCache
 import com.android.systemui.customization.R
 import com.android.systemui.log.core.Logger
-import com.android.systemui.log.core.MessageBuffer
 import com.android.systemui.plugins.clocks.ClockFontAxisSetting
-import com.android.systemui.shared.clocks.AssetLoader
 import com.android.systemui.shared.clocks.ClockAnimation
+import com.android.systemui.shared.clocks.ClockContext
 import com.android.systemui.shared.clocks.DigitTranslateAnimator
 import com.android.systemui.shared.clocks.DimensionParser
 import com.android.systemui.shared.clocks.FontTextStyle
@@ -57,18 +54,15 @@
 private val TAG = SimpleDigitalClockTextView::class.simpleName!!
 
 @SuppressLint("AppCompatCustomView")
-open class SimpleDigitalClockTextView(
-    ctx: Context,
-    messageBuffer: MessageBuffer,
-    attrs: AttributeSet? = null,
-) : TextView(ctx, attrs), SimpleDigitalClockView {
+open class SimpleDigitalClockTextView(clockCtx: ClockContext, attrs: AttributeSet? = null) :
+    TextView(clockCtx.context, attrs), SimpleDigitalClockView {
     val lockScreenPaint = TextPaint()
     override lateinit var textStyle: FontTextStyle
     lateinit var aodStyle: FontTextStyle
 
     private var lsFontVariation = ClockFontAxisSetting.toFVar(DEFAULT_LS_VARIATION)
     private var aodFontVariation = ClockFontAxisSetting.toFVar(DEFAULT_AOD_VARIATION)
-    private val parser = DimensionParser(ctx)
+    private val parser = DimensionParser(clockCtx.context)
     var maxSingleDigitHeight = -1
     var maxSingleDigitWidth = -1
     var digitTranslateAnimator: DigitTranslateAnimator? = null
@@ -91,29 +85,19 @@
     private val prevTextBounds = Rect()
     // targetTextBounds holds the state we are interpolating to
     private val targetTextBounds = Rect()
-    protected val logger = Logger(messageBuffer, this::class.simpleName!!)
+    protected val logger = Logger(clockCtx.messageBuffer, this::class.simpleName!!)
         get() = field ?: LogUtil.FALLBACK_INIT_LOGGER
 
     private var aodDozingInterpolator: Interpolator? = null
 
     @VisibleForTesting lateinit var textAnimator: TextAnimator
 
-    lateinit var typefaceCache: TypefaceVariantCache
-        private set
-
-    private fun setTypefaceCache(value: TypefaceVariantCache) {
-        typefaceCache = value
-        if (this::textAnimator.isInitialized) {
-            textAnimator.typefaceCache = value
-        }
-    }
+    private val typefaceCache = clockCtx.typefaceCache.getVariantCache("")
 
     @VisibleForTesting
     var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator = { layout, invalidateCb ->
         TextAnimator(layout, ClockAnimation.NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb).also {
-            if (this::typefaceCache.isInitialized) {
-                it.typefaceCache = typefaceCache
-            }
+            it.typefaceCache = typefaceCache
         }
     }
 
@@ -436,10 +420,9 @@
         return updateXtranslation(localTranslation, interpolatedTextBounds)
     }
 
-    override fun applyStyles(assets: AssetLoader, textStyle: TextStyle, aodStyle: TextStyle?) {
+    override fun applyStyles(textStyle: TextStyle, aodStyle: TextStyle?) {
         this.textStyle = textStyle as FontTextStyle
         val typefaceName = "fonts/" + textStyle.fontFamily
-        setTypefaceCache(assets.typefaceCache.getVariantCache(typefaceName))
         lockScreenPaint.strokeJoin = Paint.Join.ROUND
         lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
         textStyle.fontFeatureSettings?.let {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
index 3d2ed4a1..e8be28f 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt
@@ -18,7 +18,6 @@
 
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.plugins.clocks.ClockFontAxisSetting
-import com.android.systemui.shared.clocks.AssetLoader
 import com.android.systemui.shared.clocks.TextStyle
 
 interface SimpleDigitalClockView {
@@ -29,7 +28,7 @@
     val textStyle: TextStyle
     @VisibleForTesting var isAnimationEnabled: Boolean
 
-    fun applyStyles(assets: AssetLoader, textStyle: TextStyle, aodStyle: TextStyle?)
+    fun applyStyles(textStyle: TextStyle, aodStyle: TextStyle?)
 
     fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean = false)
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
index b72668b..921a8a6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt
@@ -41,8 +41,8 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.largeScreenHeaderHelper
 import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
 import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.android.systemui.statusbar.sysuiStatusBarStateController
 import com.android.systemui.util.animation.DisappearParameters
 import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 01c17bd..94a19c8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -148,7 +148,6 @@
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository;
 import com.android.systemui.statusbar.notification.ConversationNotificationManager;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
@@ -428,7 +427,7 @@
         mShadeInteractor = new ShadeInteractorImpl(
                 mTestScope.getBackgroundScope(),
                 mKosmos.getDeviceProvisioningInteractor(),
-                new FakeDisableFlagsRepository(),
+                mKosmos.getDisableFlagsInteractor(),
                 mDozeParameters,
                 mFakeKeyguardRepository,
                 mKeyguardTransitionInteractor,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
index 443595d..ef132d5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
@@ -149,7 +149,7 @@
     @Mock protected LargeScreenHeaderHelper mLargeScreenHeaderHelper;
 
     protected FakeDisableFlagsRepository mDisableFlagsRepository =
-            new FakeDisableFlagsRepository();
+            mKosmos.getFakeDisableFlagsRepository();
     protected FakeKeyguardRepository mKeyguardRepository = new FakeKeyguardRepository();
     protected FakeShadeRepository mShadeRepository = new FakeShadeRepository();
 
@@ -185,7 +185,7 @@
         mShadeInteractor = new ShadeInteractorImpl(
                 mTestScope.getBackgroundScope(),
                 mKosmos.getDeviceProvisioningInteractor(),
-                mDisableFlagsRepository,
+                mKosmos.getDisableFlagsInteractor(),
                 mDozeParameters,
                 mKeyguardRepository,
                 keyguardTransitionInteractor,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt
index 46961d4..ee3f801 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/QuickSettingsControllerImplWithCoroutinesTest.kt
@@ -19,7 +19,7 @@
 import android.app.StatusBarManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.cancelChildren
@@ -61,7 +61,7 @@
             mDisableFlagsRepository.disableFlags.value =
                 DisableFlagsModel(
                     StatusBarManager.DISABLE_NONE,
-                    StatusBarManager.DISABLE2_QUICK_SETTINGS
+                    StatusBarManager.DISABLE2_QUICK_SETTINGS,
                 )
             runCurrent()
 
@@ -76,7 +76,7 @@
             mDisableFlagsRepository.disableFlags.value =
                 DisableFlagsModel(
                     StatusBarManager.DISABLE_NONE,
-                    StatusBarManager.DISABLE2_NOTIFICATION_SHADE
+                    StatusBarManager.DISABLE2_NOTIFICATION_SHADE,
                 )
             runCurrent()
 
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 19ac0cf..da652c4 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
@@ -37,8 +37,8 @@
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.shade.shadeTestUtil
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
 import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.android.systemui.statusbar.phone.dozeParameters
 import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository
 import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepositoryTest.kt
index 907c684..cd07821 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepositoryTest.kt
@@ -32,7 +32,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt
index 79ff4be..7eac7e8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt
@@ -21,8 +21,8 @@
 import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
 import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.google.common.truth.Truth.assertThat
 import dagger.BindsInstance
 import dagger.Component
@@ -51,10 +51,7 @@
     fun disableFlags_notifAlertsNotDisabled_notifAlertsEnabledTrue() =
         with(testComponent) {
             disableFlags.disableFlags.value =
-                DisableFlagsModel(
-                    StatusBarManager.DISABLE_NONE,
-                    StatusBarManager.DISABLE2_NONE,
-                )
+                DisableFlagsModel(StatusBarManager.DISABLE_NONE, StatusBarManager.DISABLE2_NONE)
             assertThat(underTest.areNotificationAlertsEnabled()).isTrue()
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorTest.kt
index 46f822a..db24d4b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorTest.kt
@@ -21,18 +21,18 @@
 import android.app.StatusBarManager.DISABLE_NONE
 import android.app.StatusBarManager.DISABLE_NOTIFICATION_ICONS
 import android.app.StatusBarManager.DISABLE_SYSTEM_INFO
-import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
 import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
-import org.junit.runner.RunWith;
+import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
index c4d2569..b9cdd06 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/HomeStatusBarViewModelImplTest.kt
@@ -58,8 +58,8 @@
 import com.android.systemui.statusbar.data.model.StatusBarMode
 import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository.Companion.DISPLAY_ID
 import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
 import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.android.systemui.statusbar.events.data.repository.systemStatusEventAnimationRepository
 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingIn
 import com.android.systemui.statusbar.events.shared.model.SystemEventAnimationState.AnimatingOut
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
index 89da465..fb9e96c 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
@@ -139,7 +139,9 @@
 
     /** Message buffer for large clock rendering */
     val largeClockMessageBuffer: MessageBuffer,
-)
+) {
+    constructor(buffer: MessageBuffer) : this(buffer, buffer, buffer) {}
+}
 
 data class AodClockBurnInModel(val scale: Float, val translationX: Float, val translationY: Float)
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
index 9029563..e3de6d5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
@@ -62,6 +62,7 @@
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor
 import com.android.systemui.util.LargeScreenUtils
 import com.android.systemui.util.asIndenting
 import com.android.systemui.util.kotlin.emitOnStart
@@ -93,7 +94,7 @@
     private val footerActionsController: FooterActionsController,
     private val sysuiStatusBarStateController: SysuiStatusBarStateController,
     deviceEntryInteractor: DeviceEntryInteractor,
-    disableFlagsRepository: DisableFlagsRepository,
+    DisableFlagsInteractor: DisableFlagsInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val largeScreenShadeInterpolator: LargeScreenShadeInterpolator,
     @ShadeDisplayAware configurationInteractor: ConfigurationInteractor,
@@ -182,8 +183,8 @@
     val isQsEnabled by
         hydrator.hydratedStateOf(
             traceName = "isQsEnabled",
-            initialValue = disableFlagsRepository.disableFlags.value.isQuickSettingsEnabled(),
-            source = disableFlagsRepository.disableFlags.map { it.isQuickSettingsEnabled() },
+            initialValue = DisableFlagsInteractor.disableFlags.value.isQuickSettingsEnabled(),
+            source = DisableFlagsInteractor.disableFlags.map { it.isQuickSettingsEnabled() },
         )
 
     var isInSplitShade by mutableStateOf(false)
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
index 3a90d2b..503d0bf 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java
@@ -235,8 +235,7 @@
         if (mBrightnessWarningToast.isToastActive()) {
             return;
         }
-        mBrightnessWarningToast.show(mView.getContext(),
-                R.string.quick_settings_brightness_unable_adjust_msg);
+        mBrightnessWarningToast.show(mView.getContext(), resId);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/BrightnessWarningToast.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/BrightnessWarningToast.kt
index 9dc2cba..40260d0 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/BrightnessWarningToast.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/BrightnessWarningToast.kt
@@ -37,6 +37,9 @@
     private var toastView: View? = null
 
     fun show(viewContext: Context, @StringRes resId: Int) {
+        if (isToastActive()) {
+            return
+        }
         val res = viewContext.resources
         // Show the brightness warning toast with passing the toast inflation required context,
         // userId and resId from SystemUI package.
@@ -79,13 +82,15 @@
         val inAnimator = systemUIToast.inAnimation
         inAnimator?.start()
 
-        toastView!!.postDelayed({
+        toastView?.postDelayed({
             val outAnimator = systemUIToast.outAnimation
             if (outAnimator != null) {
                 outAnimator.start()
                 outAnimator.addListener(object : AnimatorListenerAdapter() {
                     override fun onAnimationEnd(animator: Animator) {
-                        windowManager.removeViewImmediate(toastView)
+                        if (isToastActive()) {
+                            windowManager.removeViewImmediate(toastView)
+                        }
                         toastView = null
                     }
                 })
@@ -94,7 +99,7 @@
     }
 
     fun isToastActive(): Boolean {
-        return toastView != null && toastView!!.isAttachedToWindow
+        return toastView?.isAttachedToWindow == true
     }
 
     companion object {
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 460bfbb..a653ca2 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
@@ -25,7 +25,7 @@
 import com.android.systemui.keyguard.shared.model.Edge
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
 import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor
@@ -47,7 +47,7 @@
 constructor(
     @Application val scope: CoroutineScope,
     deviceProvisioningInteractor: DeviceProvisioningInteractor,
-    disableFlagsRepository: DisableFlagsRepository,
+    disableFlagsInteractor: DisableFlagsInteractor,
     dozeParams: DozeParameters,
     keyguardRepository: KeyguardRepository,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
@@ -61,13 +61,13 @@
     BaseShadeInteractor by baseShadeInteractor,
     ShadeModeInteractor by shadeModeInteractor {
     override val isShadeEnabled: StateFlow<Boolean> =
-        disableFlagsRepository.disableFlags
+        disableFlagsInteractor.disableFlags
             .map { it.isShadeEnabled() }
             .flowName("isShadeEnabled")
             .stateIn(scope, SharingStarted.Eagerly, initialValue = false)
 
     override val isQsEnabled: StateFlow<Boolean> =
-        disableFlagsRepository.disableFlags
+        disableFlagsInteractor.disableFlags
             .map { it.isQuickSettingsEnabled() }
             .flowName("isQsEnabled")
             .stateIn(scope, SharingStarted.Eagerly, initialValue = false)
@@ -114,7 +114,7 @@
 
     override val isExpandToQsEnabled: Flow<Boolean> =
         combine(
-            disableFlagsRepository.disableFlags,
+            disableFlagsInteractor.disableFlags,
             isShadeEnabled,
             keyguardRepository.isDozing,
             userSetupRepository.isUserSetUp,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt
index 9004e5d..aeeb042 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/repository/DisableFlagsRepository.kt
@@ -22,7 +22,7 @@
 import com.android.systemui.log.dagger.DisableFlagsRepositoryLog
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractor.kt
new file mode 100644
index 0000000..4f1b978
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractor.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.statusbar.disableflags.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+
+@SysUISingleton
+class DisableFlagsInteractor @Inject constructor(repository: DisableFlagsRepository) {
+    /** A model of the disable flags last received from [IStatusBar]. */
+    val disableFlags: StateFlow<DisableFlagsModel> = repository.disableFlags
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/model/DisableFlagsModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/shared/model/DisableFlagsModel.kt
similarity index 85%
rename from packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/model/DisableFlagsModel.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/disableflags/shared/model/DisableFlagsModel.kt
index ce25cf5..6507237 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/data/model/DisableFlagsModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/disableflags/shared/model/DisableFlagsModel.kt
@@ -1,18 +1,20 @@
 /*
- * 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. You may obtain a copy of the License at
+ * 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.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
  */
 
-package com.android.systemui.statusbar.disableflags.data.model
+package com.android.systemui.statusbar.disableflags.shared.model
 
 import android.app.StatusBarManager.DISABLE2_NONE
 import android.app.StatusBarManager.DISABLE2_NOTIFICATION_SHADE
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractor.kt
index 8079ce5..4ea597a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractor.kt
@@ -17,17 +17,15 @@
 package com.android.systemui.statusbar.notification.domain.interactor
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor
 import javax.inject.Inject
 
 /** Interactor for notification alerting. */
 @SysUISingleton
 class NotificationAlertsInteractor
 @Inject
-constructor(
-    private val disableFlagsRepository: DisableFlagsRepository,
-) {
+constructor(private val disableFlagsInteractor: DisableFlagsInteractor) {
     /** Returns true if notification alerts are allowed. */
     fun areNotificationAlertsEnabled(): Boolean =
-        disableFlagsRepository.disableFlags.value.areNotificationAlertsEnabled()
+        disableFlagsInteractor.disableFlags.value.areNotificationAlertsEnabled()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractor.kt
index 9164da7..b2a0272 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractor.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.pipeline.shared.domain.interactor
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.domain.interactor.DisableFlagsInteractor
 import com.android.systemui.statusbar.pipeline.shared.domain.model.StatusBarDisableFlagsVisibilityModel
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
@@ -30,13 +30,13 @@
 @SysUISingleton
 class CollapsedStatusBarInteractor
 @Inject
-constructor(disableFlagsRepository: DisableFlagsRepository) {
+constructor(DisableFlagsInteractor: DisableFlagsInteractor) {
     /**
      * The visibilities of various status bar child views, based only on the information we received
      * from disable flags.
      */
     val visibilityViaDisableFlags: Flow<StatusBarDisableFlagsVisibilityModel> =
-        disableFlagsRepository.disableFlags.map {
+        DisableFlagsInteractor.disableFlags.map {
             StatusBarDisableFlagsVisibilityModel(
                 isClockAllowed = it.isClockEnabled,
                 areNotificationIconsAllowed = it.areNotificationIconsEnabled,
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 65b6273..2aa6e7b 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -105,8 +105,7 @@
 
     private val mainExecutor = ImmediateExecutor()
     private lateinit var repository: FakeKeyguardRepository
-    private val messageBuffer = LogcatOnlyMessageBuffer(LogLevel.DEBUG)
-    private val clockBuffers = ClockMessageBuffers(messageBuffer, messageBuffer, messageBuffer)
+    private val clockBuffers = ClockMessageBuffers(LogcatOnlyMessageBuffer(LogLevel.DEBUG))
     private lateinit var underTest: ClockEventController
 
     @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
index 8bd8b72..2812bd3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
@@ -15,11 +15,16 @@
  */
 package systemui.shared.clocks.view
 
+import android.graphics.Typeface
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.clocks.ClockMessageBuffers
+import com.android.systemui.plugins.clocks.ClockSettings
+import com.android.systemui.shared.clocks.ClockContext
 import com.android.systemui.shared.clocks.FontTextStyle
 import com.android.systemui.shared.clocks.LogUtil
+import com.android.systemui.shared.clocks.TypefaceCache
 import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
 import org.junit.Assert.assertEquals
 import org.junit.Before
@@ -38,7 +43,23 @@
 
     @Before
     fun setup() {
-        underTest = SimpleDigitalClockTextView(context, messageBuffer)
+        underTest =
+            SimpleDigitalClockTextView(
+                ClockContext(
+                    context,
+                    context.resources,
+                    ClockSettings(),
+                    TypefaceCache(messageBuffer) {
+                        // TODO(b/364680873): Move constant to config_clockFontFamily when shipping
+                        return@TypefaceCache Typeface.create(
+                            "google-sans-flex-clock",
+                            Typeface.NORMAL,
+                        )
+                    },
+                    ClockMessageBuffers(messageBuffer),
+                    messageBuffer,
+                )
+            )
         underTest.textStyle = FontTextStyle()
         underTest.aodStyle = FontTextStyle()
         underTest.text = "0"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index a8618eb..3a46d03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -20,9 +20,8 @@
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor
 import com.android.systemui.shade.domain.interactor.shadeInteractor
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
-import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository
 import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
@@ -31,7 +30,6 @@
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.phone.ScrimController
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
-import com.android.systemui.statusbar.policy.configurationController
 import com.android.systemui.statusbar.policy.fakeConfigurationController
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.any
@@ -144,7 +142,7 @@
                         context = context,
                         configurationController = configurationController,
                         dumpManager = mock(),
-                        splitShadeStateController = ResourcesSplitShadeStateController()
+                        splitShadeStateController = ResourcesSplitShadeStateController(),
                     ),
                 keyguardTransitionControllerFactory = { notificationPanelController ->
                     LockscreenShadeKeyguardTransitionController(
@@ -153,7 +151,7 @@
                         context = context,
                         configurationController = configurationController,
                         dumpManager = mock(),
-                        splitShadeStateController = ResourcesSplitShadeStateController()
+                        splitShadeStateController = ResourcesSplitShadeStateController(),
                     )
                 },
                 depthController = depthController,
@@ -171,7 +169,7 @@
                 splitShadeStateController = ResourcesSplitShadeStateController(),
                 shadeLockscreenInteractorLazy = { shadeLockscreenInteractor },
                 naturalScrollingSettingObserver = naturalScrollingSettingObserver,
-                lazyQSSceneAdapter = { qsSceneAdapter }
+                lazyQSSceneAdapter = { qsSceneAdapter },
             )
 
         transitionController.addCallback(transitionControllerCallback)
@@ -229,7 +227,7 @@
             verify(statusbarStateController).setState(StatusBarState.SHADE_LOCKED)
             assertFalse(
                 "Waking to shade locked when not dozing",
-                transitionController.isWakingToShadeLocked
+                transitionController.isWakingToShadeLocked,
             )
         }
 
@@ -247,9 +245,7 @@
     fun testDontGoWhenShadeDisabled() =
         testScope.runTest {
             disableFlagsRepository.disableFlags.value =
-                DisableFlagsModel(
-                    disable2 = DISABLE2_NOTIFICATION_SHADE,
-                )
+                DisableFlagsModel(disable2 = DISABLE2_NOTIFICATION_SHADE)
             testScope.runCurrent()
             transitionController.goToLockedShade(null)
             verify(statusbarStateController, never()).setState(anyInt())
@@ -454,7 +450,7 @@
             val distance = 10
             context.orCreateTestableResources.addOverride(
                 R.dimen.lockscreen_shade_scrim_transition_distance,
-                distance
+                distance,
             )
             configurationController.notifyConfigurationChanged()
 
@@ -463,7 +459,7 @@
             verify(scrimController)
                 .transitionToFullShadeProgress(
                     progress = eq(0.5f),
-                    lockScreenNotificationsProgress = anyFloat()
+                    lockScreenNotificationsProgress = anyFloat(),
                 )
         }
 
@@ -474,11 +470,11 @@
             val delay = 10
             context.orCreateTestableResources.addOverride(
                 R.dimen.lockscreen_shade_notifications_scrim_transition_distance,
-                distance
+                distance,
             )
             context.orCreateTestableResources.addOverride(
                 R.dimen.lockscreen_shade_notifications_scrim_transition_delay,
-                delay
+                delay,
             )
             configurationController.notifyConfigurationChanged()
 
@@ -487,7 +483,7 @@
             verify(scrimController)
                 .transitionToFullShadeProgress(
                     progress = anyFloat(),
-                    lockScreenNotificationsProgress = eq(0.1f)
+                    lockScreenNotificationsProgress = eq(0.1f),
                 )
         }
 
@@ -498,11 +494,11 @@
             val delay = 50
             context.orCreateTestableResources.addOverride(
                 R.dimen.lockscreen_shade_notifications_scrim_transition_distance,
-                distance
+                distance,
             )
             context.orCreateTestableResources.addOverride(
                 R.dimen.lockscreen_shade_notifications_scrim_transition_delay,
-                delay
+                delay,
             )
             configurationController.notifyConfigurationChanged()
 
@@ -511,7 +507,7 @@
             verify(scrimController)
                 .transitionToFullShadeProgress(
                     progress = anyFloat(),
-                    lockScreenNotificationsProgress = eq(0f)
+                    lockScreenNotificationsProgress = eq(0f),
                 )
         }
 
@@ -522,11 +518,11 @@
             val delay = 50
             context.orCreateTestableResources.addOverride(
                 R.dimen.lockscreen_shade_notifications_scrim_transition_distance,
-                distance
+                distance,
             )
             context.orCreateTestableResources.addOverride(
                 R.dimen.lockscreen_shade_notifications_scrim_transition_delay,
-                delay
+                delay,
             )
             configurationController.notifyConfigurationChanged()
 
@@ -535,7 +531,7 @@
             verify(scrimController)
                 .transitionToFullShadeProgress(
                     progress = anyFloat(),
-                    lockScreenNotificationsProgress = eq(1f)
+                    lockScreenNotificationsProgress = eq(1f),
                 )
         }
 
@@ -627,7 +623,7 @@
      */
     private fun ScrimController.transitionToFullShadeProgress(
         progress: Float,
-        lockScreenNotificationsProgress: Float
+        lockScreenNotificationsProgress: Float,
     ) {
         setTransitionToFullShadeProgress(progress, lockScreenNotificationsProgress)
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index d1303d2..3d60abf 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -71,6 +71,8 @@
 import com.android.systemui.shade.ui.viewmodel.notificationShadeWindowModel
 import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel
 import com.android.systemui.statusbar.data.repository.fakeStatusBarModePerDisplayRepository
+import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor
 import com.android.systemui.statusbar.notification.collection.provider.visualStabilityProvider
 import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
 import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
@@ -183,4 +185,6 @@
     val lockscreenToGlanceableHubTransitionViewModel by lazy {
         kosmos.lockscreenToGlanceableHubTransitionViewModel
     }
+    val disableFlagsInteractor by lazy { kosmos.disableFlagsInteractor }
+    val fakeDisableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
index 4ed49123..45d5b38 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
@@ -33,7 +33,7 @@
 import com.android.systemui.qs.ui.viewmodel.quickSettingsContainerViewModelFactory
 import com.android.systemui.shade.largeScreenHeaderHelper
 import com.android.systemui.shade.transition.largeScreenShadeInterpolator
-import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository
+import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor
 import com.android.systemui.statusbar.sysuiStatusBarStateController
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
@@ -51,7 +51,7 @@
                     footerActionsController,
                     sysuiStatusBarStateController,
                     deviceEntryInteractor,
-                    disableFlagsRepository,
+                    disableFlagsInteractor,
                     keyguardTransitionInteractor,
                     largeScreenShadeInterpolator,
                     configurationInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
index 39f58ae..af6d624 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
@@ -25,7 +25,7 @@
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.shade.ShadeModule
 import com.android.systemui.shade.data.repository.shadeRepository
-import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository
+import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor
 import com.android.systemui.statusbar.phone.dozeParameters
 import com.android.systemui.statusbar.policy.data.repository.userSetupRepository
 import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
@@ -60,7 +60,7 @@
         ShadeInteractorImpl(
             scope = applicationCoroutineScope,
             deviceProvisioningInteractor = deviceProvisioningInteractor,
-            disableFlagsRepository = disableFlagsRepository,
+            disableFlagsInteractor = disableFlagsInteractor,
             dozeParams = dozeParameters,
             keyguardRepository = fakeKeyguardRepository,
             keyguardTransitionInteractor = keyguardTransitionInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt
index 466a3eb..9dbb547 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/data/repository/FakeDisableFlagsRepository.kt
@@ -15,7 +15,7 @@
 package com.android.systemui.statusbar.disableflags.data.repository
 
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
+import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
 import dagger.Binds
 import dagger.Module
 import javax.inject.Inject
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractorKosmos.kt
new file mode 100644
index 0000000..7b4b047
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/disableflags/domain/interactor/DisableFlagsInteractorKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.disableflags.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.disableflags.data.repository.disableFlagsRepository
+
+val Kosmos.disableFlagsInteractor by Fixture {
+    DisableFlagsInteractor(repository = disableFlagsRepository)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorKosmos.kt
index 385a813..13fde96 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/domain/interactor/CollapsedStatusBarInteractorKosmos.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.pipeline.shared.domain.interactor
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
+import com.android.systemui.statusbar.disableflags.domain.interactor.disableFlagsInteractor
 
 val Kosmos.collapsedStatusBarInteractor: CollapsedStatusBarInteractor by
-    Kosmos.Fixture { CollapsedStatusBarInteractor(fakeDisableFlagsRepository) }
+    Kosmos.Fixture { CollapsedStatusBarInteractor(disableFlagsInteractor) }
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index f611c57..88a7d08 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -31,6 +31,7 @@
 import android.hardware.SensorPrivacyManager;
 import android.hardware.SensorPrivacyManagerInternal;
 import android.hardware.contexthub.ErrorCode;
+import android.hardware.contexthub.HubEndpointInfo;
 import android.hardware.contexthub.MessageDeliveryStatus;
 import android.hardware.location.ContextHubInfo;
 import android.hardware.location.ContextHubMessage;
@@ -739,6 +740,14 @@
         return mHubInfoRegistry.getHubs();
     }
 
+    @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+    @Override
+    public List<HubEndpointInfo> findEndpoints(long endpointId) {
+        super.findEndpoints_enforcePermission();
+        // TODO(b/375487784): connect this with mHubInfoRegistry
+        return Collections.emptyList();
+    }
+
     /**
      * Creates an internal load transaction callback to be used for old API clients
      *
diff --git a/services/core/java/com/android/server/security/forensic/BackupTransportConnection.java b/services/core/java/com/android/server/security/forensic/ForensicEventTransportConnection.java
similarity index 69%
rename from services/core/java/com/android/server/security/forensic/BackupTransportConnection.java
rename to services/core/java/com/android/server/security/forensic/ForensicEventTransportConnection.java
index caca011..b85199e 100644
--- a/services/core/java/com/android/server/security/forensic/BackupTransportConnection.java
+++ b/services/core/java/com/android/server/security/forensic/ForensicEventTransportConnection.java
@@ -16,15 +16,19 @@
 
 package com.android.server.security.forensic;
 
+import static android.Manifest.permission.BIND_FORENSIC_EVENT_TRANSPORT_SERVICE;
+
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.security.forensic.ForensicEvent;
-import android.security.forensic.IBackupTransport;
+import android.security.forensic.IForensicEventTransport;
 import android.text.TextUtils;
 import android.util.Slog;
 
@@ -36,20 +40,20 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
-public class BackupTransportConnection implements ServiceConnection {
-    private static final String TAG = "BackupTransportConnection";
+public class ForensicEventTransportConnection implements ServiceConnection {
+    private static final String TAG = "ForensicEventTransportConnection";
     private static final long FUTURE_TIMEOUT_MILLIS = 60 * 1000; // 1 mins
     private final Context mContext;
-    private String mForensicBackupTransportConfig;
-    volatile IBackupTransport mService;
+    private String mForensicEventTransportConfig;
+    volatile IForensicEventTransport mService;
 
-    public BackupTransportConnection(Context context) {
+    public ForensicEventTransportConnection(Context context) {
         mContext = context;
         mService = null;
     }
 
     /**
-     * Initialize the BackupTransport binder service.
+     * Initialize the ForensicEventTransport binder service.
      * @return Whether the initialization succeed.
      */
     public boolean initialize() {
@@ -74,7 +78,7 @@
     }
 
     /**
-     * Add data to the BackupTransport binder service.
+     * Add data to the ForensicEventTransport binder service.
      * @param data List of ForensicEvent.
      * @return Whether the data is added to the binder service.
      */
@@ -109,21 +113,37 @@
             return future.get(FUTURE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
         } catch (InterruptedException | ExecutionException | TimeoutException
                  | CancellationException e) {
-            Slog.w(TAG, "Failed to get result from transport:", e);
+            Slog.e(TAG, "Failed to get result from transport:", e);
             return null;
         }
     }
 
     private boolean bindService() {
-        mForensicBackupTransportConfig = mContext.getString(
-                com.android.internal.R.string.config_forensicBackupTransport);
-        if (TextUtils.isEmpty(mForensicBackupTransportConfig)) {
+        mForensicEventTransportConfig = mContext.getString(
+                com.android.internal.R.string.config_forensicEventTransport);
+        if (TextUtils.isEmpty(mForensicEventTransportConfig)) {
+            Slog.e(TAG, "config_forensicEventTransport is empty");
             return false;
         }
 
         ComponentName serviceComponent =
-                ComponentName.unflattenFromString(mForensicBackupTransportConfig);
+                ComponentName.unflattenFromString(mForensicEventTransportConfig);
         if (serviceComponent == null) {
+            Slog.e(TAG, "Can't get serviceComponent name");
+            return false;
+        }
+
+        try {
+            ServiceInfo serviceInfo = mContext.getPackageManager().getServiceInfo(serviceComponent,
+                    0 /* flags */);
+            if (!BIND_FORENSIC_EVENT_TRANSPORT_SERVICE.equals(serviceInfo.permission)) {
+                Slog.e(TAG, serviceComponent.flattenToShortString()
+                        + " is not declared with the permission "
+                        + "\"" + BIND_FORENSIC_EVENT_TRANSPORT_SERVICE + "\"");
+                return false;
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            Slog.e(TAG, "Unable to find serviceComponent");
             return false;
         }
 
@@ -143,7 +163,7 @@
 
     @Override
     public void onServiceConnected(ComponentName name, IBinder service) {
-        mService = IBackupTransport.Stub.asInterface(service);
+        mService = IForensicEventTransport.Stub.asInterface(service);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/security/forensic/ForensicService.java b/services/core/java/com/android/server/security/forensic/ForensicService.java
index 01f630b..2be068f 100644
--- a/services/core/java/com/android/server/security/forensic/ForensicService.java
+++ b/services/core/java/com/android/server/security/forensic/ForensicService.java
@@ -16,11 +16,16 @@
 
 package com.android.server.security.forensic;
 
+import static android.Manifest.permission.MANAGE_FORENSIC_STATE;
+import static android.Manifest.permission.READ_FORENSIC_STATE;
+
+import android.annotation.EnforcePermission;
 import android.annotation.NonNull;
 import android.content.Context;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.os.PermissionEnforcer;
 import android.os.RemoteException;
 import android.security.forensic.ForensicEvent;
 import android.security.forensic.IForensicService;
@@ -41,16 +46,15 @@
 public class ForensicService extends SystemService {
     private static final String TAG = "ForensicService";
 
-    private static final int MSG_MONITOR_STATE = 0;
-    private static final int MSG_MAKE_VISIBLE = 1;
-    private static final int MSG_MAKE_INVISIBLE = 2;
-    private static final int MSG_ENABLE = 3;
-    private static final int MSG_DISABLE = 4;
-    private static final int MSG_BACKUP = 5;
+    private static final int MAX_STATE_CALLBACK_NUM = 16;
+    private static final int MSG_ADD_STATE_CALLBACK = 0;
+    private static final int MSG_REMOVE_STATE_CALLBACK = 1;
+    private static final int MSG_ENABLE = 2;
+    private static final int MSG_DISABLE = 3;
+    private static final int MSG_TRANSPORT = 4;
 
     private static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN;
-    private static final int STATE_INVISIBLE = IForensicServiceStateCallback.State.INVISIBLE;
-    private static final int STATE_VISIBLE = IForensicServiceStateCallback.State.VISIBLE;
+    private static final int STATE_DISABLED = IForensicServiceStateCallback.State.DISABLED;
     private static final int STATE_ENABLED = IForensicServiceStateCallback.State.ENABLED;
 
     private static final int ERROR_UNKNOWN = IForensicServiceCommandCallback.ErrorCode.UNKNOWN;
@@ -58,19 +62,19 @@
             IForensicServiceCommandCallback.ErrorCode.PERMISSION_DENIED;
     private static final int ERROR_INVALID_STATE_TRANSITION =
             IForensicServiceCommandCallback.ErrorCode.INVALID_STATE_TRANSITION;
-    private static final int ERROR_BACKUP_TRANSPORT_UNAVAILABLE =
-            IForensicServiceCommandCallback.ErrorCode.BACKUP_TRANSPORT_UNAVAILABLE;
+    private static final int ERROR_TRANSPORT_UNAVAILABLE =
+            IForensicServiceCommandCallback.ErrorCode.TRANSPORT_UNAVAILABLE;
     private static final int ERROR_DATA_SOURCE_UNAVAILABLE =
             IForensicServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE;
 
     private final Context mContext;
     private final Handler mHandler;
-    private final BackupTransportConnection mBackupTransportConnection;
+    private final ForensicEventTransportConnection mForensicEventTransportConnection;
     private final DataAggregator mDataAggregator;
     private final BinderService mBinderService;
 
-    private final ArrayList<IForensicServiceStateCallback> mStateMonitors = new ArrayList<>();
-    private volatile int mState = STATE_INVISIBLE;
+    private final ArrayList<IForensicServiceStateCallback> mStateCallbacks = new ArrayList<>();
+    private volatile int mState = STATE_DISABLED;
 
     public ForensicService(@NonNull Context context) {
         this(new InjectorImpl(context));
@@ -81,9 +85,9 @@
         super(injector.getContext());
         mContext = injector.getContext();
         mHandler = new EventHandler(injector.getLooper(), this);
-        mBackupTransportConnection = injector.getBackupTransportConnection();
+        mForensicEventTransportConnection = injector.getForensicEventransportConnection();
         mDataAggregator = injector.getDataAggregator(this);
-        mBinderService = new BinderService(this);
+        mBinderService = new BinderService(this, injector.getPermissionEnforcer());
     }
 
     @VisibleForTesting
@@ -94,32 +98,36 @@
     private static final class BinderService extends IForensicService.Stub {
         final ForensicService mService;
 
-        BinderService(ForensicService service)  {
+        BinderService(ForensicService service, @NonNull PermissionEnforcer permissionEnforcer)  {
+            super(permissionEnforcer);
             mService = service;
         }
 
         @Override
-        public void monitorState(IForensicServiceStateCallback callback) {
-            mService.mHandler.obtainMessage(MSG_MONITOR_STATE, callback).sendToTarget();
+        @EnforcePermission(READ_FORENSIC_STATE)
+        public void addStateCallback(IForensicServiceStateCallback callback) {
+            addStateCallback_enforcePermission();
+            mService.mHandler.obtainMessage(MSG_ADD_STATE_CALLBACK, callback).sendToTarget();
         }
 
         @Override
-        public void makeVisible(IForensicServiceCommandCallback callback) {
-            mService.mHandler.obtainMessage(MSG_MAKE_VISIBLE, callback).sendToTarget();
+        @EnforcePermission(READ_FORENSIC_STATE)
+        public void removeStateCallback(IForensicServiceStateCallback callback) {
+            removeStateCallback_enforcePermission();
+            mService.mHandler.obtainMessage(MSG_REMOVE_STATE_CALLBACK, callback).sendToTarget();
         }
 
         @Override
-        public void makeInvisible(IForensicServiceCommandCallback callback) {
-            mService.mHandler.obtainMessage(MSG_MAKE_INVISIBLE, callback).sendToTarget();
-        }
-
-        @Override
+        @EnforcePermission(MANAGE_FORENSIC_STATE)
         public void enable(IForensicServiceCommandCallback callback) {
+            enable_enforcePermission();
             mService.mHandler.obtainMessage(MSG_ENABLE, callback).sendToTarget();
         }
 
         @Override
+        @EnforcePermission(MANAGE_FORENSIC_STATE)
         public void disable(IForensicServiceCommandCallback callback) {
+            disable_enforcePermission();
             mService.mHandler.obtainMessage(MSG_DISABLE, callback).sendToTarget();
         }
     }
@@ -135,24 +143,18 @@
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case MSG_MONITOR_STATE:
+                case MSG_ADD_STATE_CALLBACK:
                     try {
-                        mService.monitorState(
+                        mService.addStateCallback(
                                 (IForensicServiceStateCallback) msg.obj);
                     } catch (RemoteException e) {
                         Slog.e(TAG, "RemoteException", e);
                     }
                     break;
-                case MSG_MAKE_VISIBLE:
+                case MSG_REMOVE_STATE_CALLBACK:
                     try {
-                        mService.makeVisible((IForensicServiceCommandCallback) msg.obj);
-                    } catch (RemoteException e) {
-                        Slog.e(TAG, "RemoteException", e);
-                    }
-                    break;
-                case MSG_MAKE_INVISIBLE:
-                    try {
-                        mService.makeInvisible((IForensicServiceCommandCallback) msg.obj);
+                        mService.removeStateCallback(
+                                (IForensicServiceStateCallback) msg.obj);
                     } catch (RemoteException e) {
                         Slog.e(TAG, "RemoteException", e);
                     }
@@ -171,8 +173,8 @@
                         Slog.e(TAG, "RemoteException", e);
                     }
                     break;
-                case MSG_BACKUP:
-                    mService.backup((List<ForensicEvent>) msg.obj);
+                case MSG_TRANSPORT:
+                    mService.transport((List<ForensicEvent>) msg.obj);
                     break;
                 default:
                     Slog.w(TAG, "Unknown message: " + msg.what);
@@ -180,103 +182,83 @@
         }
     }
 
-    private void monitorState(IForensicServiceStateCallback callback) throws RemoteException {
-        for (int i = 0; i < mStateMonitors.size(); i++) {
-            if (mStateMonitors.get(i).asBinder() == callback.asBinder()) {
+    private void addStateCallback(IForensicServiceStateCallback callback) throws RemoteException {
+        for (int i = 0; i < mStateCallbacks.size(); i++) {
+            if (mStateCallbacks.get(i).asBinder() == callback.asBinder()) {
                 return;
             }
         }
-        mStateMonitors.add(callback);
+        mStateCallbacks.add(callback);
         callback.onStateChange(mState);
     }
 
-    private void notifyStateMonitors() throws RemoteException {
-        for (int i = 0; i < mStateMonitors.size(); i++) {
-            mStateMonitors.get(i).onStateChange(mState);
+    private void removeStateCallback(IForensicServiceStateCallback callback)
+            throws RemoteException {
+        for (int i = 0; i < mStateCallbacks.size(); i++) {
+            if (mStateCallbacks.get(i).asBinder() == callback.asBinder()) {
+                mStateCallbacks.remove(i);
+                return;
+            }
         }
     }
 
-    private void makeVisible(IForensicServiceCommandCallback callback) throws RemoteException {
-        switch (mState) {
-            case STATE_INVISIBLE:
-                if (!mDataAggregator.initialize()) {
-                    callback.onFailure(ERROR_DATA_SOURCE_UNAVAILABLE);
-                    break;
-                }
-                mState = STATE_VISIBLE;
-                notifyStateMonitors();
-                callback.onSuccess();
-                break;
-            case STATE_VISIBLE:
-                callback.onSuccess();
-                break;
-            default:
-                callback.onFailure(ERROR_INVALID_STATE_TRANSITION);
+    private void notifyStateMonitors() {
+        if (mStateCallbacks.size() >= MAX_STATE_CALLBACK_NUM) {
+            mStateCallbacks.removeFirst();
         }
-    }
 
-    private void makeInvisible(IForensicServiceCommandCallback callback) throws RemoteException {
-        switch (mState) {
-            case STATE_VISIBLE:
-            case STATE_ENABLED:
-                mState = STATE_INVISIBLE;
-                notifyStateMonitors();
-                callback.onSuccess();
-                break;
-            case STATE_INVISIBLE:
-                callback.onSuccess();
-                break;
-            default:
-                callback.onFailure(ERROR_INVALID_STATE_TRANSITION);
+        for (int i = 0; i < mStateCallbacks.size(); i++) {
+            try {
+                mStateCallbacks.get(i).onStateChange(mState);
+            } catch (RemoteException e) {
+                mStateCallbacks.remove(i);
+            }
         }
     }
 
     private void enable(IForensicServiceCommandCallback callback) throws RemoteException {
-        switch (mState) {
-            case STATE_VISIBLE:
-                if (!mBackupTransportConnection.initialize()) {
-                    callback.onFailure(ERROR_BACKUP_TRANSPORT_UNAVAILABLE);
-                    break;
-                }
-                mDataAggregator.enable();
-                mState = STATE_ENABLED;
-                notifyStateMonitors();
-                callback.onSuccess();
-                break;
-            case STATE_ENABLED:
-                callback.onSuccess();
-                break;
-            default:
-                callback.onFailure(ERROR_INVALID_STATE_TRANSITION);
+        if (mState == STATE_ENABLED) {
+            callback.onSuccess();
+            return;
         }
+
+        // TODO: temporarily disable the following for the CTS ForensicManagerTest.
+        //  Enable it when the transport component is ready.
+        // if (!mForensicEventTransportConnection.initialize()) {
+        //     callback.onFailure(ERROR_TRANSPORT_UNAVAILABLE);
+        //   return;
+        // }
+
+        mDataAggregator.enable();
+        mState = STATE_ENABLED;
+        notifyStateMonitors();
+        callback.onSuccess();
     }
 
     private void disable(IForensicServiceCommandCallback callback) throws RemoteException {
-        switch (mState) {
-            case STATE_ENABLED:
-                mBackupTransportConnection.release();
-                mDataAggregator.disable();
-                mState = STATE_VISIBLE;
-                notifyStateMonitors();
-                callback.onSuccess();
-                break;
-            case STATE_VISIBLE:
-                callback.onSuccess();
-                break;
-            default:
-                callback.onFailure(ERROR_INVALID_STATE_TRANSITION);
+        if (mState == STATE_DISABLED) {
+            callback.onSuccess();
+            return;
         }
+
+        // TODO: temporarily disable the following for the CTS ForensicManagerTest.
+        //  Enable it when the transport component is ready.
+        // mForensicEventTransportConnection.release();
+        mDataAggregator.disable();
+        mState = STATE_DISABLED;
+        notifyStateMonitors();
+        callback.onSuccess();
     }
 
     /**
      * Add a list of ForensicEvent.
      */
     public void addNewData(List<ForensicEvent> events) {
-        mHandler.obtainMessage(MSG_BACKUP, events).sendToTarget();
+        mHandler.obtainMessage(MSG_TRANSPORT, events).sendToTarget();
     }
 
-    private void backup(List<ForensicEvent> events) {
-        mBackupTransportConnection.addData(events);
+    private void transport(List<ForensicEvent> events) {
+        mForensicEventTransportConnection.addData(events);
     }
 
     @Override
@@ -296,9 +278,11 @@
     interface Injector {
         Context getContext();
 
+        PermissionEnforcer getPermissionEnforcer();
+
         Looper getLooper();
 
-        BackupTransportConnection getBackupTransportConnection();
+        ForensicEventTransportConnection getForensicEventransportConnection();
 
         DataAggregator getDataAggregator(ForensicService forensicService);
     }
@@ -315,6 +299,10 @@
             return mContext;
         }
 
+        @Override
+        public PermissionEnforcer getPermissionEnforcer() {
+            return PermissionEnforcer.fromContext(mContext);
+        }
 
         @Override
         public Looper getLooper() {
@@ -326,8 +314,8 @@
         }
 
         @Override
-        public BackupTransportConnection getBackupTransportConnection() {
-            return new BackupTransportConnection(mContext);
+        public ForensicEventTransportConnection getForensicEventransportConnection() {
+            return new ForensicEventTransportConnection(mContext);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 6707a27..f50417d 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2856,7 +2856,10 @@
 
     void prepareForShutdown() {
         for (int i = 0; i < getChildCount(); i++) {
-            createSleepToken("shutdown", getChildAt(i).mDisplayId);
+            final int displayId = getChildAt(i).mDisplayId;
+            mWindowManager.mSnapshotController.mTaskSnapshotController
+                    .snapshotForShutdown(displayId);
+            createSleepToken("shutdown", displayId);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
index 1c8c245..bd8e8f4 100644
--- a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
+++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
@@ -64,6 +64,7 @@
     private boolean mStarted;
     private final Object mLock = new Object();
     private final UserManagerInternal mUserManagerInternal;
+    private boolean mShutdown;
 
     SnapshotPersistQueue() {
         mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
@@ -101,6 +102,16 @@
         }
     }
 
+    /**
+     * Write out everything in the queue because of shutdown.
+     */
+    void shutdown() {
+        synchronized (mLock) {
+            mShutdown = true;
+            mLock.notifyAll();
+        }
+    }
+
     @VisibleForTesting
     void waitForQueueEmpty() {
         while (true) {
@@ -193,7 +204,9 @@
                     if (isReadyToWrite) {
                         next.write();
                     }
-                    SystemClock.sleep(DELAY_MS);
+                    if (!mShutdown) {
+                        SystemClock.sleep(DELAY_MS);
+                    }
                 }
                 synchronized (mLock) {
                     final boolean writeQueueEmpty = mWriteQueue.isEmpty();
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java
index 1f82cdb..9fe3f756 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotController.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java
@@ -307,6 +307,28 @@
     }
 
     /**
+     * Record task snapshots before shutdown.
+     */
+    void snapshotForShutdown(int displayId) {
+        if (!com.android.window.flags.Flags.recordTaskSnapshotsBeforeShutdown()) {
+            return;
+        }
+        final DisplayContent displayContent = mService.mRoot.getDisplayContent(displayId);
+        if (displayContent == null) {
+            return;
+        }
+        displayContent.forAllLeafTasks(task -> {
+            if (task.isVisible() && !task.isActivityTypeHome()) {
+                final TaskSnapshot snapshot = captureSnapshot(task);
+                if (snapshot != null) {
+                    mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot);
+                }
+            }
+        }, true /* traverseTopToBottom */);
+        mPersister.mSnapshotPersistQueue.shutdown();
+    }
+
+    /**
      * Called when screen is being turned off.
      */
     void screenTurningOff(int displayId, ScreenOffListener listener) {
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index ead1282..565f75b 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -788,7 +788,9 @@
                 deferResume = false;
                 // Already calls ensureActivityConfig
                 mService.mRootWindowContainer.ensureActivitiesVisible();
-                mService.mRootWindowContainer.resumeFocusedTasksTopActivities();
+                if (!mService.mRootWindowContainer.resumeFocusedTasksTopActivities()) {
+                    mService.mTaskSupervisor.updateTopResumedActivityIfNeeded("endWCT-effects");
+                }
             } else if ((effects & TRANSACT_EFFECTS_CLIENT_CONFIG) != 0) {
                 for (int i = haveConfigChanges.size() - 1; i >= 0; --i) {
                     haveConfigChanges.valueAt(i).forAllActivities(r -> {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 28eec5c..90c3dff 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -376,6 +376,7 @@
 import android.app.compat.CompatChanges;
 import android.app.role.OnRoleHoldersChangedListener;
 import android.app.role.RoleManager;
+import android.app.supervision.SupervisionManagerInternal;
 import android.app.trust.TrustManager;
 import android.app.usage.UsageStatsManagerInternal;
 import android.compat.annotation.ChangeId;
@@ -926,6 +927,8 @@
     final UsageStatsManagerInternal mUsageStatsManagerInternal;
     final TelephonyManager mTelephonyManager;
     final RoleManager mRoleManager;
+    final SupervisionManagerInternal mSupervisionManagerInternal;
+
     private final LockPatternUtils mLockPatternUtils;
     private final LockSettingsInternal mLockSettingsInternal;
     private final DeviceAdminServiceController mDeviceAdminServiceController;
@@ -2082,6 +2085,11 @@
         boolean isAdminInstalledCaCertAutoApproved() {
             return false;
         }
+
+        @Nullable
+        SupervisionManagerInternal getSupervisionManager() {
+            return LocalServices.getService(SupervisionManagerInternal.class);
+        }
     }
 
     /**
@@ -2113,6 +2121,11 @@
         mIPermissionManager = Objects.requireNonNull(injector.getIPermissionManager());
         mTelephonyManager = Objects.requireNonNull(injector.getTelephonyManager());
         mRoleManager = Objects.requireNonNull(injector.getRoleManager());
+        if (Flags.secondaryLockscreenApiEnabled()) {
+            mSupervisionManagerInternal = injector.getSupervisionManager();
+        } else {
+            mSupervisionManagerInternal = null;
+        }
 
         mLocalService = new LocalService();
         mLockPatternUtils = injector.newLockPatternUtils();
@@ -2234,7 +2247,6 @@
         return Collections.unmodifiableSet(packageNames);
     }
 
-
     private @Nullable String getDefaultRoleHolderPackageName(int resId) {
         String packageNameAndSignature = mContext.getString(resId);
 
@@ -14585,34 +14597,76 @@
         }
     }
 
+    private boolean hasActiveSupervisionTestAdminLocked(@UserIdInt int userId) {
+        ensureLocked();
+        if (mConstants.USE_TEST_ADMIN_AS_SUPERVISION_COMPONENT) {
+            final DevicePolicyData policy = getUserData(userId);
+            for (ActiveAdmin admin : policy.mAdminMap.values()) {
+                if (admin != null && admin.testOnlyAdmin) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     @Override
     public void setSecondaryLockscreenEnabled(ComponentName who, boolean enabled,
             PersistableBundle options) {
-        Objects.requireNonNull(who, "ComponentName is null");
-
-        // Check can set secondary lockscreen enabled
-        final CallerIdentity caller = getCallerIdentity(who);
-        Preconditions.checkCallAuthorization(
-                isDefaultDeviceOwner(caller) || isProfileOwner(caller));
-        Preconditions.checkCallAuthorization(!isManagedProfile(caller.getUserId()),
-                "User %d is not allowed to call setSecondaryLockscreenEnabled",
+        if (Flags.secondaryLockscreenApiEnabled()) {
+            final CallerIdentity caller = getCallerIdentity();
+            final boolean isRoleHolder = isCallerSystemSupervisionRoleHolder(caller);
+            synchronized (getLockObject()) {
+                // TODO(b/378102594): Remove access for test admins.
+                final boolean isTestAdmin = hasActiveSupervisionTestAdminLocked(caller.getUserId());
+                Preconditions.checkCallAuthorization(isRoleHolder || isTestAdmin,
+                        "Caller (%d) is not the SYSTEM_SUPERVISION role holder",
                         caller.getUserId());
+            }
 
-        synchronized (getLockObject()) {
-            // Allow testOnly admins to bypass supervision config requirement.
-            Preconditions.checkCallAuthorization(isAdminTestOnlyLocked(who, caller.getUserId())
-                    || isSupervisionComponentLocked(caller.getComponentName()), "Admin %s is not "
-                    + "the default supervision component", caller.getComponentName());
-            DevicePolicyData policy = getUserData(caller.getUserId());
-            policy.mSecondaryLockscreenEnabled = enabled;
-            saveSettingsLocked(caller.getUserId());
+            if (mSupervisionManagerInternal != null) {
+                mSupervisionManagerInternal.setSupervisionLockscreenEnabledForUser(
+                        caller.getUserId(), enabled, options);
+            } else {
+                synchronized (getLockObject()) {
+                    DevicePolicyData policy = getUserData(caller.getUserId());
+                    policy.mSecondaryLockscreenEnabled = enabled;
+                    saveSettingsLocked(caller.getUserId());
+                }
+            }
+        } else {
+            Objects.requireNonNull(who, "ComponentName is null");
+
+            // Check can set secondary lockscreen enabled
+            final CallerIdentity caller = getCallerIdentity(who);
+            Preconditions.checkCallAuthorization(
+                    isDefaultDeviceOwner(caller) || isProfileOwner(caller));
+            Preconditions.checkCallAuthorization(!isManagedProfile(caller.getUserId()),
+                    "User %d is not allowed to call setSecondaryLockscreenEnabled",
+                            caller.getUserId());
+
+            synchronized (getLockObject()) {
+                // Allow testOnly admins to bypass supervision config requirement.
+                Preconditions.checkCallAuthorization(isAdminTestOnlyLocked(who, caller.getUserId())
+                        || isSupervisionComponentLocked(caller.getComponentName()),
+                        "Admin %s is not the default supervision component",
+                        caller.getComponentName());
+                DevicePolicyData policy = getUserData(caller.getUserId());
+                policy.mSecondaryLockscreenEnabled = enabled;
+                saveSettingsLocked(caller.getUserId());
+            }
         }
     }
 
     @Override
     public boolean isSecondaryLockscreenEnabled(@NonNull UserHandle userHandle) {
-        synchronized (getLockObject()) {
-            return getUserData(userHandle.getIdentifier()).mSecondaryLockscreenEnabled;
+        if (Flags.secondaryLockscreenApiEnabled() && mSupervisionManagerInternal != null) {
+            return mSupervisionManagerInternal.isSupervisionLockscreenEnabledForUser(
+                    userHandle.getIdentifier());
+        } else {
+            synchronized (getLockObject()) {
+                return getUserData(userHandle.getIdentifier()).mSecondaryLockscreenEnabled;
+            }
         }
     }
 
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 19b0343..a01ea64 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -251,6 +251,7 @@
 import com.android.server.security.KeyChainSystemService;
 import com.android.server.security.adaptiveauthentication.AdaptiveAuthenticationService;
 import com.android.server.security.advancedprotection.AdvancedProtectionService;
+import com.android.server.security.forensic.ForensicService;
 import com.android.server.security.rkp.RemoteProvisioningService;
 import com.android.server.selinux.SelinuxAuditLogsService;
 import com.android.server.sensorprivacy.SensorPrivacyService;
@@ -1760,6 +1761,13 @@
             mSystemServiceManager.startService(LogcatManagerService.class);
             t.traceEnd();
 
+            if (!isWatch && !isTv && !isAutomotive
+                    && android.security.Flags.aflApi()) {
+                t.traceBegin("StartForensicService");
+                mSystemServiceManager.startService(ForensicService.class);
+                t.traceEnd();
+            }
+
             if (AppFunctionManagerConfiguration.isSupported(context)) {
                 t.traceBegin("StartAppFunctionManager");
                 mSystemServiceManager.startService(AppFunctionManagerService.class);
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java
index 67e2547..53a25dd 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionService.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java
@@ -21,10 +21,11 @@
 import android.annotation.UserIdInt;
 import android.app.admin.DevicePolicyManagerInternal;
 import android.app.supervision.ISupervisionManager;
+import android.app.supervision.SupervisionManagerInternal;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.UserInfo;
-import android.os.Bundle;
+import android.os.PersistableBundle;
 import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
@@ -179,8 +180,15 @@
         }
 
         @Override
+        public boolean isSupervisionLockscreenEnabledForUser(@UserIdInt int userId) {
+            synchronized (getLockObject()) {
+                return getUserDataLocked(userId).supervisionLockScreenEnabled;
+            }
+        }
+
+        @Override
         public void setSupervisionLockscreenEnabledForUser(
-                @UserIdInt int userId, boolean enabled, @Nullable Bundle options) {
+                @UserIdInt int userId, boolean enabled, @Nullable PersistableBundle options) {
             synchronized (getLockObject()) {
                 SupervisionUserData data = getUserDataLocked(userId);
                 data.supervisionLockScreenEnabled = enabled;
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionUserData.java b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
index 5616237..1dd48f5 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
@@ -19,7 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
-import android.os.Bundle;
+import android.os.PersistableBundle;
 import android.util.IndentingPrintWriter;
 
 /** User specific data, used internally by the {@link SupervisionService}. */
@@ -27,7 +27,7 @@
     public final @UserIdInt int userId;
     public boolean supervisionEnabled;
     public boolean supervisionLockScreenEnabled;
-    @Nullable public Bundle supervisionLockScreenOptions;
+    @Nullable public PersistableBundle supervisionLockScreenOptions;
 
     public SupervisionUserData(@UserIdInt int userId) {
         this.userId = userId;
diff --git a/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java b/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java
index 40e0034..0da6db6 100644
--- a/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java
+++ b/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java
@@ -16,9 +16,13 @@
 
 package com.android.server.security.forensic;
 
+import static android.Manifest.permission.MANAGE_FORENSIC_STATE;
+import static android.Manifest.permission.READ_FORENSIC_STATE;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
@@ -29,7 +33,9 @@
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.os.Looper;
+import android.os.PermissionEnforcer;
 import android.os.RemoteException;
+import android.os.test.FakePermissionEnforcer;
 import android.os.test.TestLooper;
 import android.security.forensic.ForensicEvent;
 import android.security.forensic.IForensicServiceCommandCallback;
@@ -41,6 +47,7 @@
 import com.android.server.ServiceThread;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 
@@ -50,34 +57,36 @@
 
 public class ForensicServiceTest {
     private static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN;
-    private static final int STATE_INVISIBLE = IForensicServiceStateCallback.State.INVISIBLE;
-    private static final int STATE_VISIBLE = IForensicServiceStateCallback.State.VISIBLE;
+    private static final int STATE_DISABLED = IForensicServiceStateCallback.State.DISABLED;
     private static final int STATE_ENABLED = IForensicServiceStateCallback.State.ENABLED;
 
     private static final int ERROR_UNKNOWN = IForensicServiceCommandCallback.ErrorCode.UNKNOWN;
     private static final int ERROR_PERMISSION_DENIED =
             IForensicServiceCommandCallback.ErrorCode.PERMISSION_DENIED;
-    private static final int ERROR_INVALID_STATE_TRANSITION =
-            IForensicServiceCommandCallback.ErrorCode.INVALID_STATE_TRANSITION;
-    private static final int ERROR_BACKUP_TRANSPORT_UNAVAILABLE =
-            IForensicServiceCommandCallback.ErrorCode.BACKUP_TRANSPORT_UNAVAILABLE;
+    private static final int ERROR_TRANSPORT_UNAVAILABLE =
+            IForensicServiceCommandCallback.ErrorCode.TRANSPORT_UNAVAILABLE;
     private static final int ERROR_DATA_SOURCE_UNAVAILABLE =
             IForensicServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE;
 
     private Context mContext;
-    private BackupTransportConnection mBackupTransportConnection;
+    private ForensicEventTransportConnection mForensicEventTransportConnection;
     private DataAggregator mDataAggregator;
     private ForensicService mForensicService;
     private TestLooper mTestLooper;
     private Looper mLooper;
     private TestLooper mTestLooperOfDataAggregator;
     private Looper mLooperOfDataAggregator;
+    private FakePermissionEnforcer mPermissionEnforcer;
 
     @SuppressLint("VisibleForTests")
     @Before
     public void setUp() {
         mContext = spy(ApplicationProvider.getApplicationContext());
 
+        mPermissionEnforcer = new FakePermissionEnforcer();
+        mPermissionEnforcer.grant(READ_FORENSIC_STATE);
+        mPermissionEnforcer.grant(MANAGE_FORENSIC_STATE);
+
         mTestLooper = new TestLooper();
         mLooper = mTestLooper.getLooper();
         mTestLooperOfDataAggregator = new TestLooper();
@@ -87,217 +96,101 @@
     }
 
     @Test
-    public void testMonitorState_Invisible() throws RemoteException {
+    public void testAddStateCallback_NoPermission() {
+        mPermissionEnforcer.revoke(READ_FORENSIC_STATE);
         StateCallback scb = new StateCallback();
         assertEquals(STATE_UNKNOWN, scb.mState);
-        mForensicService.getBinderService().monitorState(scb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb.mState);
+        assertThrows(SecurityException.class,
+                () -> mForensicService.getBinderService().addStateCallback(scb));
     }
 
     @Test
-    public void testMonitorState_Invisible_TwoMonitors() throws RemoteException {
+    public void testRemoveStateCallback_NoPermission() {
+        mPermissionEnforcer.revoke(READ_FORENSIC_STATE);
+        StateCallback scb = new StateCallback();
+        assertEquals(STATE_UNKNOWN, scb.mState);
+        assertThrows(SecurityException.class,
+                () -> mForensicService.getBinderService().removeStateCallback(scb));
+    }
+
+    @Test
+    public void testEnable_NoPermission() {
+        mPermissionEnforcer.revoke(MANAGE_FORENSIC_STATE);
+
+        CommandCallback ccb = new CommandCallback();
+        assertThrows(SecurityException.class,
+                () -> mForensicService.getBinderService().enable(ccb));
+    }
+
+    @Test
+    public void testDisable_NoPermission() {
+        mPermissionEnforcer.revoke(MANAGE_FORENSIC_STATE);
+
+        CommandCallback ccb = new CommandCallback();
+        assertThrows(SecurityException.class,
+                () -> mForensicService.getBinderService().disable(ccb));
+    }
+
+    @Test
+    public void testAddStateCallback_Disabled() throws RemoteException {
+        StateCallback scb = new StateCallback();
+        assertEquals(STATE_UNKNOWN, scb.mState);
+        mForensicService.getBinderService().addStateCallback(scb);
+        mTestLooper.dispatchAll();
+        assertEquals(STATE_DISABLED, scb.mState);
+    }
+
+    @Test
+    public void testAddStateCallback_Disabled_TwoStateCallbacks() throws RemoteException {
         StateCallback scb1 = new StateCallback();
         assertEquals(STATE_UNKNOWN, scb1.mState);
-        mForensicService.getBinderService().monitorState(scb1);
+        mForensicService.getBinderService().addStateCallback(scb1);
         mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
+        assertEquals(STATE_DISABLED, scb1.mState);
 
         StateCallback scb2 = new StateCallback();
         assertEquals(STATE_UNKNOWN, scb2.mState);
-        mForensicService.getBinderService().monitorState(scb2);
+        mForensicService.getBinderService().addStateCallback(scb2);
         mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb2.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
     }
 
     @Test
-    public void testMakeVisible_FromInvisible() throws RemoteException {
-        StateCallback scb = new StateCallback();
-        assertEquals(STATE_UNKNOWN, scb.mState);
-        mForensicService.getBinderService().monitorState(scb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb.mState);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().makeVisible(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-    @Test
-    public void testMakeVisible_FromInvisible_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_INVISIBLE);
+    public void testRemoveStateCallback() throws RemoteException {
+        mForensicService.setState(STATE_DISABLED);
         StateCallback scb1 = new StateCallback();
         StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
+        mForensicService.getBinderService().addStateCallback(scb1);
+        mForensicService.getBinderService().addStateCallback(scb2);
         mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
 
         doReturn(true).when(mDataAggregator).initialize();
+        doReturn(true).when(mForensicEventTransportConnection).initialize();
 
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().makeVisible(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-    @Test
-    public void testMakeVisible_FromInvisible_TwoMonitors_DataSourceUnavailable()
-            throws RemoteException {
-        mForensicService.setState(STATE_INVISIBLE);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
-
-        doReturn(false).when(mDataAggregator).initialize();
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().makeVisible(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
-        assertNotNull(ccb.mErrorCode);
-        assertEquals(ERROR_DATA_SOURCE_UNAVAILABLE, ccb.mErrorCode.intValue());
-    }
-
-    @Test
-    public void testMakeVisible_FromVisible_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_VISIBLE);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().makeVisible(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-    @Test
-    public void testMakeVisible_FromEnabled_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_ENABLED);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_ENABLED, scb1.mState);
-        assertEquals(STATE_ENABLED, scb2.mState);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().makeVisible(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_ENABLED, scb1.mState);
-        assertEquals(STATE_ENABLED, scb2.mState);
-        assertNotNull(ccb.mErrorCode);
-        assertEquals(ERROR_INVALID_STATE_TRANSITION, ccb.mErrorCode.intValue());
-    }
-
-    @Test
-    public void testMakeInvisible_FromInvisible_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_INVISIBLE);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().makeInvisible(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-    @Test
-    public void testMakeInvisible_FromVisible_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_VISIBLE);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().makeInvisible(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-    @Test
-    public void testMakeInvisible_FromEnabled_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_ENABLED);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_ENABLED, scb1.mState);
-        assertEquals(STATE_ENABLED, scb2.mState);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().makeInvisible(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
-        assertNull(ccb.mErrorCode);
-    }
-
-
-    @Test
-    public void testEnable_FromInvisible_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_INVISIBLE);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
+        mForensicService.getBinderService().removeStateCallback(scb2);
 
         CommandCallback ccb = new CommandCallback();
         mForensicService.getBinderService().enable(ccb);
         mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
-        assertNotNull(ccb.mErrorCode);
-        assertEquals(ERROR_INVALID_STATE_TRANSITION, ccb.mErrorCode.intValue());
+        assertEquals(STATE_ENABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
+        assertNull(ccb.mErrorCode);
     }
 
     @Test
-    public void testEnable_FromVisible_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_VISIBLE);
+    public void testEnable_FromDisabled_TwoStateCallbacks() throws RemoteException {
+        mForensicService.setState(STATE_DISABLED);
         StateCallback scb1 = new StateCallback();
         StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
+        mForensicService.getBinderService().addStateCallback(scb1);
+        mForensicService.getBinderService().addStateCallback(scb2);
         mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
 
-        doReturn(true).when(mBackupTransportConnection).initialize();
+        doReturn(true).when(mForensicEventTransportConnection).initialize();
 
         CommandCallback ccb = new CommandCallback();
         mForensicService.getBinderService().enable(ccb);
@@ -310,35 +203,13 @@
     }
 
     @Test
-    public void testEnable_FromVisible_TwoMonitors_BackupTransportUnavailable()
+    public void testEnable_FromEnabled_TwoStateCallbacks()
             throws RemoteException {
-        mForensicService.setState(STATE_VISIBLE);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
-
-        doReturn(false).when(mBackupTransportConnection).initialize();
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().enable(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
-        assertNotNull(ccb.mErrorCode);
-        assertEquals(ERROR_BACKUP_TRANSPORT_UNAVAILABLE, ccb.mErrorCode.intValue());
-    }
-
-    @Test
-    public void testEnable_FromEnabled_TwoMonitors() throws RemoteException {
         mForensicService.setState(STATE_ENABLED);
         StateCallback scb1 = new StateCallback();
         StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
+        mForensicService.getBinderService().addStateCallback(scb1);
+        mForensicService.getBinderService().addStateCallback(scb2);
         mTestLooper.dispatchAll();
         assertEquals(STATE_ENABLED, scb1.mState);
         assertEquals(STATE_ENABLED, scb2.mState);
@@ -346,62 +217,44 @@
         CommandCallback ccb = new CommandCallback();
         mForensicService.getBinderService().enable(ccb);
         mTestLooper.dispatchAll();
+
         assertEquals(STATE_ENABLED, scb1.mState);
         assertEquals(STATE_ENABLED, scb2.mState);
         assertNull(ccb.mErrorCode);
     }
 
     @Test
-    public void testDisable_FromInvisible_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_INVISIBLE);
+    public void testDisable_FromDisabled_TwoStateCallbacks() throws RemoteException {
+        mForensicService.setState(STATE_DISABLED);
         StateCallback scb1 = new StateCallback();
         StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
+        mForensicService.getBinderService().addStateCallback(scb1);
+        mForensicService.getBinderService().addStateCallback(scb2);
         mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
 
         CommandCallback ccb = new CommandCallback();
         mForensicService.getBinderService().disable(ccb);
         mTestLooper.dispatchAll();
-        assertEquals(STATE_INVISIBLE, scb1.mState);
-        assertEquals(STATE_INVISIBLE, scb2.mState);
-        assertNotNull(ccb.mErrorCode);
-        assertEquals(ERROR_INVALID_STATE_TRANSITION, ccb.mErrorCode.intValue());
-    }
 
-    @Test
-    public void testDisable_FromVisible_TwoMonitors() throws RemoteException {
-        mForensicService.setState(STATE_VISIBLE);
-        StateCallback scb1 = new StateCallback();
-        StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
-
-        CommandCallback ccb = new CommandCallback();
-        mForensicService.getBinderService().disable(ccb);
-        mTestLooper.dispatchAll();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
         assertNull(ccb.mErrorCode);
     }
 
     @Test
-    public void testDisable_FromEnabled_TwoMonitors() throws RemoteException {
+    public void testDisable_FromEnabled_TwoStateCallbacks() throws RemoteException {
         mForensicService.setState(STATE_ENABLED);
         StateCallback scb1 = new StateCallback();
         StateCallback scb2 = new StateCallback();
-        mForensicService.getBinderService().monitorState(scb1);
-        mForensicService.getBinderService().monitorState(scb2);
+        mForensicService.getBinderService().addStateCallback(scb1);
+        mForensicService.getBinderService().addStateCallback(scb2);
         mTestLooper.dispatchAll();
         assertEquals(STATE_ENABLED, scb1.mState);
         assertEquals(STATE_ENABLED, scb2.mState);
 
-        doNothing().when(mBackupTransportConnection).release();
+        doNothing().when(mForensicEventTransportConnection).release();
 
         ServiceThread mockThread = spy(ServiceThread.class);
         mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread);
@@ -412,11 +265,35 @@
         mTestLooperOfDataAggregator.dispatchAll();
         // TODO: We can verify the data sources once we implement them.
         verify(mockThread, times(1)).quitSafely();
-        assertEquals(STATE_VISIBLE, scb1.mState);
-        assertEquals(STATE_VISIBLE, scb2.mState);
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
         assertNull(ccb.mErrorCode);
     }
 
+    @Ignore("Enable once the ForensicEventTransportConnection is ready")
+    @Test
+    public void testEnable_FromDisable_TwoStateCallbacks_TransportUnavailable()
+            throws RemoteException {
+        mForensicService.setState(STATE_DISABLED);
+        StateCallback scb1 = new StateCallback();
+        StateCallback scb2 = new StateCallback();
+        mForensicService.getBinderService().addStateCallback(scb1);
+        mForensicService.getBinderService().addStateCallback(scb2);
+        mTestLooper.dispatchAll();
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
+
+        doReturn(false).when(mForensicEventTransportConnection).initialize();
+
+        CommandCallback ccb = new CommandCallback();
+        mForensicService.getBinderService().enable(ccb);
+        mTestLooper.dispatchAll();
+        assertEquals(STATE_DISABLED, scb1.mState);
+        assertEquals(STATE_DISABLED, scb2.mState);
+        assertNotNull(ccb.mErrorCode);
+        assertEquals(ERROR_TRANSPORT_UNAVAILABLE, ccb.mErrorCode.intValue());
+    }
+
     @Test
     public void testDataAggregator_AddBatchData() {
         mForensicService.setState(STATE_ENABLED);
@@ -441,14 +318,14 @@
         events.add(eventOne);
         events.add(eventTwo);
 
-        doReturn(true).when(mBackupTransportConnection).addData(any());
+        doReturn(true).when(mForensicEventTransportConnection).addData(any());
 
         mDataAggregator.addBatchData(events);
         mTestLooperOfDataAggregator.dispatchAll();
         mTestLooper.dispatchAll();
 
         ArgumentCaptor<List<ForensicEvent>> captor = ArgumentCaptor.forClass(List.class);
-        verify(mBackupTransportConnection).addData(captor.capture());
+        verify(mForensicEventTransportConnection).addData(captor.capture());
         List<ForensicEvent> receivedEvents = captor.getValue();
         assertEquals(receivedEvents.size(), 2);
 
@@ -476,6 +353,10 @@
             return mContext;
         }
 
+        @Override
+        public PermissionEnforcer getPermissionEnforcer() {
+            return mPermissionEnforcer;
+        }
 
         @Override
         public Looper getLooper() {
@@ -483,9 +364,9 @@
         }
 
         @Override
-        public BackupTransportConnection getBackupTransportConnection() {
-            mBackupTransportConnection = spy(new BackupTransportConnection(mContext));
-            return mBackupTransportConnection;
+        public ForensicEventTransportConnection getForensicEventransportConnection() {
+            mForensicEventTransportConnection = spy(new ForensicEventTransportConnection(mContext));
+            return mForensicEventTransportConnection;
         }
 
         @Override
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
index 698bda3..4c381eb 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java
@@ -24,6 +24,7 @@
 import android.app.admin.DevicePolicyManagerInternal;
 import android.app.admin.DevicePolicyManagerLiteInternal;
 import android.app.backup.IBackupManager;
+import android.app.supervision.SupervisionManagerInternal;
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.Context;
 import android.content.Intent;
@@ -488,6 +489,11 @@
         public Context createContextAsUser(UserHandle user) {
             return context;
         }
+
+        @Override
+        SupervisionManagerInternal getSupervisionManager() {
+            return services.supervisionManagerInternal;
+        }
     }
 
     static class TransferOwnershipMetadataManagerMockInjector extends
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 9524fb2..cf5dc4b 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -109,6 +109,7 @@
 import android.app.admin.PreferentialNetworkServiceConfig;
 import android.app.admin.SystemUpdatePolicy;
 import android.app.admin.WifiSsidPolicy;
+import android.app.admin.flags.Flags;
 import android.app.role.RoleManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -134,6 +135,10 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.platform.test.annotations.Presubmit;
+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.provider.DeviceConfig;
 import android.provider.Settings;
 import android.security.KeyChain;
@@ -165,6 +170,7 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.Mockito;
 import org.mockito.internal.util.collections.Sets;
@@ -207,6 +213,9 @@
     public static final String INVALID_CALLING_IDENTITY_MSG = "Calling identity is not authorized";
     public static final String ONGOING_CALL_MSG = "ongoing call on the device";
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     // TODO replace all instances of this with explicit {@link #mServiceContext}.
     @Deprecated
     private DpmMockContext mContext;
@@ -4903,6 +4912,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED)
     public void testSecondaryLockscreen_profileOwner() throws Exception {
         mContext.binder.callingUid = DpmMockContext.CALLER_UID;
 
@@ -4931,6 +4941,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED)
     public void testSecondaryLockscreen_deviceOwner() throws Exception {
         mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
 
@@ -4949,6 +4960,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED)
     public void testSecondaryLockscreen_nonOwner() throws Exception {
         mContext.binder.callingUid = DpmMockContext.CALLER_UID;
 
@@ -4965,6 +4977,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED)
     public void testSecondaryLockscreen_nonSupervisionApp() throws Exception {
         mContext.binder.callingUid = DpmMockContext.CALLER_UID;
 
@@ -4997,6 +5010,51 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED)
+    public void testIsSecondaryLockscreenEnabled() throws Exception {
+        mContext.binder.callingUid = DpmMockContext.CALLER_UID;
+
+        verifyIsSecondaryLockscreenEnabled(false);
+        verifyIsSecondaryLockscreenEnabled(true);
+    }
+
+    private void verifyIsSecondaryLockscreenEnabled(boolean expected) throws Exception {
+        reset(getServices().supervisionManagerInternal);
+
+        doReturn(expected).when(getServices().supervisionManagerInternal)
+                .isSupervisionLockscreenEnabledForUser(anyInt());
+
+        final boolean enabled = dpm.isSecondaryLockscreenEnabled(UserHandle.of(CALLER_USER_HANDLE));
+        verify(getServices().supervisionManagerInternal)
+                .isSupervisionLockscreenEnabledForUser(CALLER_USER_HANDLE);
+
+        assertThat(enabled).isEqualTo(expected);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_SECONDARY_LOCKSCREEN_API_ENABLED)
+    public void testSetSecondaryLockscreenEnabled() throws Exception {
+        mContext.binder.callingUid = DpmMockContext.CALLER_UID;
+
+        verifySetSecondaryLockscreenEnabled(false);
+        verifySetSecondaryLockscreenEnabled(true);
+    }
+
+    private void verifySetSecondaryLockscreenEnabled(boolean enabled) throws Exception {
+        reset(getServices().supervisionManagerInternal);
+
+        dpm.setSecondaryLockscreenEnabled(admin1, enabled);
+        verify(getServices().supervisionManagerInternal).setSupervisionLockscreenEnabledForUser(
+                CALLER_USER_HANDLE, enabled, null);
+
+        reset(getServices().supervisionManagerInternal);
+
+        dpm.setSecondaryLockscreenEnabled(enabled, new PersistableBundle());
+        verify(getServices().supervisionManagerInternal).setSupervisionLockscreenEnabledForUser(
+                eq(CALLER_USER_HANDLE), eq(enabled), any(PersistableBundle.class));
+    }
+
+    @Test
     public void testIsDeviceManaged() throws Exception {
         mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID;
         setupDeviceOwner();
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java
index 2e200a9..3e4448c1 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java
@@ -35,6 +35,7 @@
 import android.app.admin.DevicePolicyManager;
 import android.app.backup.IBackupManager;
 import android.app.role.RoleManager;
+import android.app.supervision.SupervisionManagerInternal;
 import android.app.usage.UsageStatsManagerInternal;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -77,8 +78,8 @@
 import com.android.internal.widget.LockPatternUtils;
 import com.android.internal.widget.LockSettingsInternal;
 import com.android.server.AlarmManagerInternal;
-import com.android.server.pdb.PersistentDataBlockManagerInternal;
 import com.android.server.net.NetworkPolicyManagerInternal;
+import com.android.server.pdb.PersistentDataBlockManagerInternal;
 import com.android.server.pm.PackageManagerLocal;
 import com.android.server.pm.UserManagerInternal;
 import com.android.server.pm.pkg.PackageState;
@@ -149,6 +150,7 @@
     public final BuildMock buildMock = new BuildMock();
     public final File dataDir;
     public final PolicyPathProvider pathProvider;
+    public final SupervisionManagerInternal supervisionManagerInternal;
 
     private final Map<String, PackageState> mTestPackageStates = new ArrayMap<>();
 
@@ -203,6 +205,7 @@
         roleManager = realContext.getSystemService(RoleManager.class);
         roleManagerForMock = mock(RoleManagerForMock.class);
         subscriptionManager = mock(SubscriptionManager.class);
+        supervisionManagerInternal = mock(SupervisionManagerInternal.class);
 
         // Package manager is huge, so we use a partial mock instead.
         packageManager = spy(realContext.getPackageManager());
diff --git a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
index 79b06236..8290e1c 100644
--- a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
+++ b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
@@ -20,7 +20,7 @@
 import android.content.ComponentName
 import android.content.Context
 import android.content.pm.UserInfo
-import android.os.Bundle
+import android.os.PersistableBundle
 import android.platform.test.annotations.RequiresFlagsEnabled
 import android.platform.test.flag.junit.DeviceFlagsValueProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -139,7 +139,7 @@
         assertThat(userData.supervisionLockScreenEnabled).isFalse()
         assertThat(userData.supervisionLockScreenOptions).isNull()
 
-        service.mInternal.setSupervisionLockscreenEnabledForUser(USER_ID, true, Bundle())
+        service.mInternal.setSupervisionLockscreenEnabledForUser(USER_ID, true, PersistableBundle())
         userData = service.getUserDataLocked(USER_ID)
         assertThat(userData.supervisionLockScreenEnabled).isTrue()
         assertThat(userData.supervisionLockScreenOptions).isNotNull()
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index 69fe7c9..817c368 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -123,6 +123,8 @@
  * Build/Install/Run:
  *  atest WmTests:WindowOrganizerTests
  */
+
+// TODO revert parts of this set to set the flag to test the behavior
 @SmallTest
 @Presubmit
 @RunWith(WindowTestRunner.class)