Merge "Add boolean method for knowing the buildVariableFamily result" into main
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index bd00c03..b897c1a 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -4783,6 +4783,10 @@
      * Returns whether the app holds the {@link Manifest.permission.RUN_BACKUP_JOBS} permission.
      */
     private boolean hasRunBackupJobsPermission(@NonNull String packageName, int packageUid) {
+        // This permission is currently hidden so always return false for now (see b/331272951)
+        return false;
+
+        /**
         if (packageName == null) {
             Slog.wtfStack(TAG,
                     "Expected a non-null package name when calling hasRunBackupJobsPermission");
@@ -4793,6 +4797,7 @@
                 android.Manifest.permission.RUN_BACKUP_JOBS,
                 PermissionChecker.PID_UNKNOWN, packageUid, packageName)
                     == PermissionChecker.PERMISSION_GRANTED;
+        */
     }
 
     /**
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index edd86e3..d643768 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -1213,7 +1213,8 @@
             return ACTIVE_INDEX;
         }
 
-        final boolean isEligibleAsBackupJob = job.getTriggerContentUris() != null
+        final boolean isEligibleAsBackupJob = false // this exemption is being disabled for now.
+                && job.getTriggerContentUris() != null
                 && job.getRequiredNetwork() != null
                 && !job.hasLateConstraint()
                 && mJobSchedulerInternal.hasRunBackupJobsPermission(sourcePackageName, sourceUid);
diff --git a/core/api/current.txt b/core/api/current.txt
index ee13fe4..b8c2d90 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -285,7 +285,6 @@
     field @FlaggedApi("android.companion.flags.device_presence") public static final String REQUEST_OBSERVE_DEVICE_UUID_PRESENCE = "android.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE";
     field public static final String REQUEST_PASSWORD_COMPLEXITY = "android.permission.REQUEST_PASSWORD_COMPLEXITY";
     field @Deprecated public static final String RESTART_PACKAGES = "android.permission.RESTART_PACKAGES";
-    field @FlaggedApi("android.app.job.backup_jobs_exemption") public static final String RUN_BACKUP_JOBS = "android.permission.RUN_BACKUP_JOBS";
     field public static final String RUN_USER_INITIATED_JOBS = "android.permission.RUN_USER_INITIATED_JOBS";
     field public static final String SCHEDULE_EXACT_ALARM = "android.permission.SCHEDULE_EXACT_ALARM";
     field public static final String SEND_RESPOND_VIA_MESSAGE = "android.permission.SEND_RESPOND_VIA_MESSAGE";
@@ -5464,15 +5463,11 @@
     method public int getDeferralPolicy();
     method @Nullable public String getDeliveryGroupMatchingKey();
     method public int getDeliveryGroupPolicy();
-    method @FlaggedApi("android.app.bcast_event_timestamps") public long getEventTriggerTimestampMillis();
-    method @FlaggedApi("android.app.bcast_event_timestamps") public long getRemoteEventTriggerTimestampMillis();
     method public boolean isShareIdentityEnabled();
     method @NonNull public static android.app.BroadcastOptions makeBasic();
     method @NonNull public android.app.BroadcastOptions setDeferralPolicy(int);
     method @NonNull public android.app.BroadcastOptions setDeliveryGroupMatchingKey(@NonNull String, @NonNull String);
     method @NonNull public android.app.BroadcastOptions setDeliveryGroupPolicy(int);
-    method @FlaggedApi("android.app.bcast_event_timestamps") public void setEventTriggerTimestampMillis(long);
-    method @FlaggedApi("android.app.bcast_event_timestamps") public void setRemoteEventTriggerTimestampMillis(long);
     method @NonNull public android.app.BroadcastOptions setShareIdentityEnabled(boolean);
     method @NonNull public android.os.Bundle toBundle();
     field public static final int DEFERRAL_POLICY_DEFAULT = 0; // 0x0
@@ -37289,7 +37284,6 @@
     field public static final String ACTION_APP_USAGE_SETTINGS = "android.settings.action.APP_USAGE_SETTINGS";
     field @FlaggedApi("android.app.modes_api") public static final String ACTION_AUTOMATIC_ZEN_RULE_SETTINGS = "android.settings.AUTOMATIC_ZEN_RULE_SETTINGS";
     field public static final String ACTION_AUTO_ROTATE_SETTINGS = "android.settings.AUTO_ROTATE_SETTINGS";
-    field @FlaggedApi("android.app.app_restrictions_api") public static final String ACTION_BACKGROUND_RESTRICTIONS_SETTINGS = "android.settings.BACKGROUND_RESTRICTIONS_SETTINGS";
     field public static final String ACTION_BATTERY_SAVER_SETTINGS = "android.settings.BATTERY_SAVER_SETTINGS";
     field public static final String ACTION_BIOMETRIC_ENROLL = "android.settings.BIOMETRIC_ENROLL";
     field public static final String ACTION_BLUETOOTH_SETTINGS = "android.settings.BLUETOOTH_SETTINGS";
@@ -37344,7 +37338,6 @@
     field public static final String ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = "android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
     field public static final String ACTION_REQUEST_MANAGE_MEDIA = "android.settings.REQUEST_MANAGE_MEDIA";
     field @FlaggedApi("com.android.media.flags.enable_privileged_routing_for_media_routing_control") public static final String ACTION_REQUEST_MEDIA_ROUTING_CONTROL = "android.settings.REQUEST_MEDIA_ROUTING_CONTROL";
-    field @FlaggedApi("android.provider.backup_tasks_settings_screen") public static final String ACTION_REQUEST_RUN_BACKUP_JOBS = "android.settings.REQUEST_RUN_BACKUP_JOBS";
     field public static final String ACTION_REQUEST_SCHEDULE_EXACT_ALARM = "android.settings.REQUEST_SCHEDULE_EXACT_ALARM";
     field public static final String ACTION_REQUEST_SET_AUTOFILL_SERVICE = "android.settings.REQUEST_SET_AUTOFILL_SERVICE";
     field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String ACTION_SATELLITE_SETTING = "android.settings.SATELLITE_SETTING";
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 702a47e..5547028 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2178,8 +2178,15 @@
 
 package android.app.contextualsearch {
 
+  @FlaggedApi("android.app.contextualsearch.flags.enable_service") public final class CallbackToken implements android.os.Parcelable {
+    ctor public CallbackToken();
+    method public int describeContents();
+    method public void getContextualSearchState(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.contextualsearch.ContextualSearchState,java.lang.Throwable>);
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.app.contextualsearch.CallbackToken> CREATOR;
+  }
+
   @FlaggedApi("android.app.contextualsearch.flags.enable_service") public class ContextualSearchManager {
-    method public void getContextualSearchState(@NonNull android.os.IBinder, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.contextualsearch.ContextualSearchState,java.lang.Throwable>);
     method @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXTUAL_SEARCH) public void startContextualSearch(int);
     field public static final String ACTION_LAUNCH_CONTEXTUAL_SEARCH = "android.app.contextualsearch.action.LAUNCH_CONTEXTUAL_SEARCH";
     field public static final int ENTRYPOINT_LONG_PRESS_HOME = 2; // 0x2
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index ca70f03..0d46688 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -803,6 +803,14 @@
 
 }
 
+package android.app.contextualsearch {
+
+  @FlaggedApi("android.app.contextualsearch.flags.enable_service") public final class CallbackToken implements android.os.Parcelable {
+    method @NonNull public android.os.IBinder getToken();
+  }
+
+}
+
 package android.app.job {
 
   public class JobParameters implements android.os.Parcelable {
@@ -1551,7 +1559,7 @@
 
   public static class BiometricPrompt.Builder {
     method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.TEST_BIOMETRIC, "android.permission.USE_BIOMETRIC_INTERNAL"}) public android.hardware.biometrics.BiometricPrompt.Builder setAllowBackgroundAuthentication(boolean);
-    method @FlaggedApi("android.multiuser.enable_biometrics_to_unlock_private_space") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.TEST_BIOMETRIC, "android.permission.USE_BIOMETRIC_INTERNAL"}) public android.hardware.biometrics.BiometricPrompt.Builder setAllowBackgroundAuthentication(boolean, boolean);
+    method @FlaggedApi("android.os.allow_private_profile") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.TEST_BIOMETRIC, "android.permission.USE_BIOMETRIC_INTERNAL"}) public android.hardware.biometrics.BiometricPrompt.Builder setAllowBackgroundAuthentication(boolean, boolean);
     method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.TEST_BIOMETRIC, "android.permission.USE_BIOMETRIC_INTERNAL"}) public android.hardware.biometrics.BiometricPrompt.Builder setAllowedSensorIds(@NonNull java.util.List<java.lang.Integer>);
   }
 
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 0c54351..7725561 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -5672,9 +5672,11 @@
     }
 
     /**
-     * Return if a given profile is in the foreground.
+     * Returns whether the given user, or its parent (if the user is a profile), is in the
+     * foreground.
      * @param userHandle UserHandle to check
-     * @return Returns the boolean result.
+     * @return whether the user is the foreground user or, if it is a profile, whether its parent
+     *         is the foreground user
      * @hide
      */
     @RequiresPermission(anyOf = {
diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java
index 60d622d..4db3727 100644
--- a/core/java/android/app/BroadcastOptions.java
+++ b/core/java/android/app/BroadcastOptions.java
@@ -18,8 +18,6 @@
 
 import static android.app.ActivityOptions.BackgroundActivityStartMode;
 
-import android.annotation.CurrentTimeMillisLong;
-import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -69,8 +67,6 @@
     private @Nullable BundleMerger mDeliveryGroupExtrasMerger;
     private @Nullable IntentFilter mDeliveryGroupMatchingFilter;
     private @DeferralPolicy int mDeferralPolicy;
-    private @CurrentTimeMillisLong long mEventTriggerTimestampMillis;
-    private @CurrentTimeMillisLong long mRemoteEventTriggerTimestampMillis;
 
     /** @hide */
     @IntDef(flag = true, prefix = { "FLAG_" }, value = {
@@ -196,18 +192,6 @@
             "android:broadcast.idForResponseEvent";
 
     /**
-     * Corresponds to {@link #setEventTriggerTimestampMillis(long)}.
-     */
-    private static final String KEY_EVENT_TRIGGER_TIMESTAMP =
-            "android:broadcast.eventTriggerTimestamp";
-
-    /**
-     * Corresponds to {@link #setRemoteEventTriggerTimestampMillis(long)}.
-     */
-    private static final String KEY_REMOTE_EVENT_TRIGGER_TIMESTAMP =
-            "android:broadcast.remoteEventTriggerTimestamp";
-
-    /**
      * Corresponds to {@link #setDeliveryGroupPolicy(int)}.
      */
     private static final String KEY_DELIVERY_GROUP_POLICY =
@@ -359,8 +343,6 @@
         mRequireNoneOfPermissions = opts.getStringArray(KEY_REQUIRE_NONE_OF_PERMISSIONS);
         mRequireCompatChangeId = opts.getLong(KEY_REQUIRE_COMPAT_CHANGE_ID, CHANGE_INVALID);
         mIdForResponseEvent = opts.getLong(KEY_ID_FOR_RESPONSE_EVENT);
-        mEventTriggerTimestampMillis = opts.getLong(KEY_EVENT_TRIGGER_TIMESTAMP);
-        mRemoteEventTriggerTimestampMillis = opts.getLong(KEY_REMOTE_EVENT_TRIGGER_TIMESTAMP);
         mDeliveryGroupPolicy = opts.getInt(KEY_DELIVERY_GROUP_POLICY,
                 DELIVERY_GROUP_POLICY_ALL);
         mDeliveryGroupMatchingNamespaceFragment = opts.getString(KEY_DELIVERY_GROUP_NAMESPACE);
@@ -807,60 +789,6 @@
     }
 
     /**
-     * Set the timestamp for the event that triggered this broadcast, in
-     * {@link System#currentTimeMillis()} timebase.
-     *
-     * <p> For instance, if this broadcast is for a push message, then this timestamp
-     * could correspond to when the device received the message.
-     *
-     * @param timestampMillis the timestamp in {@link System#currentTimeMillis()} timebase that
-     *                        correspond to the event that triggered this broadcast.
-     */
-    @FlaggedApi(android.app.Flags.FLAG_BCAST_EVENT_TIMESTAMPS)
-    public void setEventTriggerTimestampMillis(@CurrentTimeMillisLong long timestampMillis) {
-        mEventTriggerTimestampMillis = timestampMillis;
-    }
-
-    /**
-     * Return the timestamp for the event that triggered this broadcast, in
-     * {@link System#currentTimeMillis()} timebase.
-     *
-     * @return the timestamp in {@link System#currentTimeMillis()} timebase that was previously
-     *         set using {@link #setEventTriggerTimestampMillis(long)}.
-     */
-    @FlaggedApi(android.app.Flags.FLAG_BCAST_EVENT_TIMESTAMPS)
-    public @CurrentTimeMillisLong long getEventTriggerTimestampMillis() {
-        return mEventTriggerTimestampMillis;
-    }
-
-    /**
-     * Set the timestamp for the remote event, if any, that triggered this broadcast, in
-     * {@link System#currentTimeMillis()} timebase.
-     *
-     * <p> For instance, if this broadcast is for a push message, then this timestamp
-     * could correspond to when the message originated remotely.
-     *
-     * @param timestampMillis the timestamp in {@link System#currentTimeMillis()} timebase that
-     *                        correspond to the remote event that triggered this broadcast.
-     */
-    @FlaggedApi(android.app.Flags.FLAG_BCAST_EVENT_TIMESTAMPS)
-    public void setRemoteEventTriggerTimestampMillis(@CurrentTimeMillisLong long timestampMillis) {
-        mRemoteEventTriggerTimestampMillis = timestampMillis;
-    }
-
-    /**
-     * Return the timestamp for the remote event that triggered this broadcast, in
-     * {@link System#currentTimeMillis()} timebase.
-     *
-     * @return the timestamp in {@link System#currentTimeMillis()} timebase that was previously
-     *         set using {@link #setRemoteEventTriggerTimestampMillis(long)}}.
-     */
-    @FlaggedApi(android.app.Flags.FLAG_BCAST_EVENT_TIMESTAMPS)
-    public @CurrentTimeMillisLong long getRemoteEventTriggerTimestampMillis() {
-        return mRemoteEventTriggerTimestampMillis;
-    }
-
-    /**
      * Sets deferral policy for this broadcast that specifies how this broadcast
      * can be deferred for delivery at some future point.
      */
@@ -1195,12 +1123,6 @@
         if (mIdForResponseEvent != 0) {
             b.putLong(KEY_ID_FOR_RESPONSE_EVENT, mIdForResponseEvent);
         }
-        if (mEventTriggerTimestampMillis > 0) {
-            b.putLong(KEY_EVENT_TRIGGER_TIMESTAMP, mEventTriggerTimestampMillis);
-        }
-        if (mRemoteEventTriggerTimestampMillis > 0) {
-            b.putLong(KEY_REMOTE_EVENT_TRIGGER_TIMESTAMP, mRemoteEventTriggerTimestampMillis);
-        }
         if (mDeliveryGroupPolicy != DELIVERY_GROUP_POLICY_ALL) {
             b.putInt(KEY_DELIVERY_GROUP_POLICY, mDeliveryGroupPolicy);
         }
diff --git a/core/java/android/app/contextualsearch/CallbackToken.java b/core/java/android/app/contextualsearch/CallbackToken.java
new file mode 100644
index 0000000..0bbd1e5
--- /dev/null
+++ b/core/java/android/app/contextualsearch/CallbackToken.java
@@ -0,0 +1,159 @@
+/*
+ * 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.app.contextualsearch;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.app.contextualsearch.flags.Flags;
+import android.content.Context;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.OutcomeReceiver;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.ParcelableException;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Used to share a single use token with the contextual search handling activity via the launch
+ * extras bundle.
+ * The caller can then use this token to get {@link ContextualSearchState} by calling
+ * {@link #getContextualSearchState}.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_ENABLE_SERVICE)
+@SystemApi
+public final class CallbackToken implements Parcelable {
+    private static final boolean DEBUG = true;
+    private static final String TAG = CallbackToken.class.getSimpleName();
+    private final IBinder mToken;
+
+    private boolean mTokenUsed = false;
+
+    public CallbackToken() {
+        mToken = new Binder();
+    }
+
+    private CallbackToken(Parcel in) {
+        mToken = in.readStrongBinder();
+    }
+
+    /**
+     * Returns the {@link ContextualSearchState} to the handler via the provided callback. The
+     * method can only be invoked to provide the {@link OutcomeReceiver} once and all subsequent
+     * invocations of this method will result in {@link OutcomeReceiver#onError} being called with
+     * an {@link IllegalAccessException}.
+     *
+     * @param executor The executor which will be used to invoke the callback.
+     * @param callback The callback which will be used to return {@link ContextualSearchState}
+     *                 if/when it is available via {@link OutcomeReceiver#onResult}. It will also be
+     *                 used to return errors via {@link OutcomeReceiver#onError}.
+     */
+    public void getContextualSearchState(@NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<ContextualSearchState, Throwable> callback) {
+        if (DEBUG) Log.d(TAG, "getContextualSearchState for token:" + mToken);
+        if (mTokenUsed) {
+            callback.onError(new IllegalAccessException("Token already used."));
+        }
+        mTokenUsed = true;
+        try {
+            // Get the service from the system server.
+            IBinder b = ServiceManager.getService(Context.CONTEXTUAL_SEARCH_SERVICE);
+            IContextualSearchManager service = IContextualSearchManager.Stub.asInterface(b);
+            final CallbackWrapper wrapper = new CallbackWrapper(executor, callback);
+            // If the service is not null, hand over the call to the service.
+            if (service != null) {
+                service.getContextualSearchState(mToken, wrapper);
+            } else {
+                Log.w(TAG, "Failed to getContextualSearchState. Service null.");
+            }
+        } catch (RemoteException e) {
+            if (DEBUG) Log.d(TAG, "Failed to call getContextualSearchState", e);
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return the token necessary for validating the caller of {@link #getContextualSearchState}.
+     *
+     * @hide
+     */
+    @TestApi
+    @NonNull
+    public IBinder getToken() {
+        return mToken;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeStrongBinder(mToken);
+    }
+
+    @NonNull
+    public static final Creator<CallbackToken> CREATOR = new Creator<>() {
+        @Override
+        public CallbackToken createFromParcel(Parcel in) {
+            return new CallbackToken(in);
+        }
+
+        @Override
+        public CallbackToken[] newArray(int size) {
+            return new CallbackToken[size];
+        }
+    };
+
+    private static class CallbackWrapper extends IContextualSearchCallback.Stub {
+        private final OutcomeReceiver<ContextualSearchState, Throwable> mCallback;
+        private final Executor mExecutor;
+
+        CallbackWrapper(@NonNull Executor callbackExecutor,
+                @NonNull OutcomeReceiver<ContextualSearchState, Throwable> callback) {
+            mCallback = callback;
+            mExecutor = callbackExecutor;
+        }
+
+        @Override
+        public void onResult(ContextualSearchState state) {
+            Binder.withCleanCallingIdentity(() -> {
+                if (DEBUG) Log.d(TAG, "onResult state:" + state);
+                mExecutor.execute(() -> mCallback.onResult(state));
+            });
+        }
+
+        @Override
+        public void onError(ParcelableException error) {
+            Binder.withCleanCallingIdentity(() -> {
+                if (DEBUG) Log.w(TAG, "onError", error);
+                mExecutor.execute(() -> mCallback.onError(error));
+            });
+        }
+    }
+}
diff --git a/core/java/android/app/contextualsearch/ContextualSearchManager.java b/core/java/android/app/contextualsearch/ContextualSearchManager.java
index 693de21..a894a0e 100644
--- a/core/java/android/app/contextualsearch/ContextualSearchManager.java
+++ b/core/java/android/app/contextualsearch/ContextualSearchManager.java
@@ -18,37 +18,26 @@
 
 import static android.Manifest.permission.ACCESS_CONTEXTUAL_SEARCH;
 
-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.app.contextualsearch.flags.Flags;
 import android.content.Context;
-import android.os.Binder;
-import android.os.Bundle;
 import android.os.IBinder;
-import android.os.OutcomeReceiver;
-import android.os.ParcelableException;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.util.Log;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.concurrent.Executor;
 
 /**
  * {@link ContextualSearchManager} is a system service to facilitate contextual search experience on
  * configured Android devices.
  * <p>
- * This class lets
- * <ul>
- *   <li> a caller start contextual search by calling {@link #startContextualSearch} method.
- *   <li> a handler request {@link ContextualSearchState} by calling the
- *   {@link #getContextualSearchState} method.
- * </ul>
+ * This class lets a caller start contextual search by calling {@link #startContextualSearch}
+ * method.
  *
  * @hide
  */
@@ -92,7 +81,7 @@
 
     /**
      * Key to get the binder token from the extras of the activity launched by contextual search.
-     * This token is needed to invoke {@link #getContextualSearchState} method.
+     * This token is needed to invoke {@link CallbackToken#getContextualSearchState} method.
      * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH.
      */
     public static final String EXTRA_TOKEN = "android.app.contextualsearch.extra.TOKEN";
@@ -174,57 +163,4 @@
             e.rethrowFromSystemServer();
         }
     }
-
-    /**
-     * Returns the {@link ContextualSearchState} to the handler via the provided callback.
-     *
-     * @param token The caller is expected to get the token from the launch extras of the handling
-     *              activity using {@link Bundle#getIBinder} with {@link #EXTRA_TOKEN} key.
-     *              <br>
-     *              <b>Note</b> This token is for one time use only. Subsequent uses will invoke
-     *              callback's {@link OutcomeReceiver#onError}.
-     * @param executor The executor which will be used to invoke the callback.
-     * @param callback The callback which will be used to return {@link ContextualSearchState}
-     *                 if/when it is available via {@link OutcomeReceiver#onResult}. It will also be
-     *                 used to return errors via {@link OutcomeReceiver#onError}.
-     */
-    public void getContextualSearchState(@NonNull IBinder token,
-            @NonNull @CallbackExecutor Executor executor,
-            @NonNull OutcomeReceiver<ContextualSearchState, Throwable> callback) {
-        if (DEBUG) Log.d(TAG, "getContextualSearchState for token:" + token);
-        try {
-            final CallbackWrapper wrapper = new CallbackWrapper(executor, callback);
-            mService.getContextualSearchState(token, wrapper);
-        } catch (RemoteException e) {
-            if (DEBUG) Log.d(TAG, "Failed to getContextualSearchState", e);
-            e.rethrowFromSystemServer();
-        }
-    }
-
-    private static class CallbackWrapper extends IContextualSearchCallback.Stub {
-        private final OutcomeReceiver<ContextualSearchState, Throwable> mCallback;
-        private final Executor mExecutor;
-
-        CallbackWrapper(@NonNull Executor callbackExecutor,
-                        @NonNull OutcomeReceiver<ContextualSearchState, Throwable> callback) {
-            mCallback = callback;
-            mExecutor = callbackExecutor;
-        }
-
-        @Override
-        public void onResult(ContextualSearchState state) {
-            Binder.withCleanCallingIdentity(() -> {
-                if (DEBUG) Log.d(TAG, "onResult state:" + state);
-                mExecutor.execute(() -> mCallback.onResult(state));
-            });
-        }
-
-        @Override
-        public void onError(ParcelableException error) {
-            Binder.withCleanCallingIdentity(() -> {
-                if (DEBUG) Log.w(TAG, "onError", error);
-                mExecutor.execute(() -> mCallback.onError(error));
-            });
-        }
-    }
 }
diff --git a/core/java/android/app/contextualsearch/ContextualSearchState.java b/core/java/android/app/contextualsearch/ContextualSearchState.java
index 5c04bc89..f01ae55 100644
--- a/core/java/android/app/contextualsearch/ContextualSearchState.java
+++ b/core/java/android/app/contextualsearch/ContextualSearchState.java
@@ -32,6 +32,10 @@
  * {@link ContextualSearchState} contains additional data a contextual search handler can request
  * via {@link ContextualSearchManager#getContextualSearchState} method.
  *
+ * It provides the caller of {@link ContextualSearchManager#getContextualSearchState} with an
+ * {@link AssistStructure}, {@link AssistContent} and a {@link Bundle} extras containing any
+ * relevant data added by th system server. When invoked via the Launcher, this bundle is empty.
+ *
  * @hide
  */
 @FlaggedApi(Flags.FLAG_ENABLE_SERVICE)
@@ -41,6 +45,12 @@
     private final @Nullable AssistStructure mStructure;
     private final @Nullable AssistContent mContent;
 
+    /**
+     * {@link ContextualSearchState} contains non-essential data which can be requested by the
+     * Contextual Search activity.
+     * The activity can request an instance of {@link ContextualSearchState} by calling
+     * {@link CallbackToken#getContextualSearchState} and passing a valid token as an argument.
+     */
     public ContextualSearchState(@Nullable AssistStructure structure,
             @Nullable AssistContent content, @NonNull Bundle extras) {
         mStructure = structure;
diff --git a/core/java/android/app/contextualsearch/IContextualSearchManager.aidl b/core/java/android/app/contextualsearch/IContextualSearchManager.aidl
index 1735a71..9b0b8b7 100644
--- a/core/java/android/app/contextualsearch/IContextualSearchManager.aidl
+++ b/core/java/android/app/contextualsearch/IContextualSearchManager.aidl
@@ -1,6 +1,5 @@
 package android.app.contextualsearch;
 
-
 import android.app.contextualsearch.IContextualSearchCallback;
 /**
  * @hide
diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java
index 5e00b7a..2c26389 100644
--- a/core/java/android/companion/CompanionDeviceManager.java
+++ b/core/java/android/companion/CompanionDeviceManager.java
@@ -1086,7 +1086,7 @@
         }
         Objects.requireNonNull(deviceAddress, "address cannot be null");
         try {
-            mService.registerDevicePresenceListenerService(deviceAddress,
+            mService.legacyStartObservingDevicePresence(deviceAddress,
                     mContext.getOpPackageName(), mContext.getUserId());
         } catch (RemoteException e) {
             ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class);
@@ -1128,7 +1128,7 @@
         }
         Objects.requireNonNull(deviceAddress, "address cannot be null");
         try {
-            mService.unregisterDevicePresenceListenerService(deviceAddress,
+            mService.legacyStopObservingDevicePresence(deviceAddress,
                     mContext.getPackageName(), mContext.getUserId());
         } catch (RemoteException e) {
             ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class);
@@ -1328,7 +1328,7 @@
     @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED)
     public void notifyDeviceAppeared(int associationId) {
         try {
-            mService.notifyDeviceAppeared(associationId);
+            mService.notifySelfManagedDeviceAppeared(associationId);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -1350,7 +1350,7 @@
     @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED)
     public void notifyDeviceDisappeared(int associationId) {
         try {
-            mService.notifyDeviceDisappeared(associationId);
+            mService.notifySelfManagedDeviceDisappeared(associationId);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl
index 57d59e5..1b00f90e 100644
--- a/core/java/android/companion/ICompanionDeviceManager.aidl
+++ b/core/java/android/companion/ICompanionDeviceManager.aidl
@@ -59,12 +59,16 @@
         int userId);
 
     @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE")
-    void registerDevicePresenceListenerService(in String deviceAddress, in String callingPackage,
-        int userId);
+    void legacyStartObservingDevicePresence(in String deviceAddress, in String callingPackage, int userId);
 
     @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE")
-    void unregisterDevicePresenceListenerService(in String deviceAddress, in String callingPackage,
-        int userId);
+    void legacyStopObservingDevicePresence(in String deviceAddress, in String callingPackage, int userId);
+
+    @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE")
+    void startObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId);
+
+    @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE")
+    void stopObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId);
 
     boolean canPairWithoutPrompt(in String packageName, in String deviceMacAddress, int userId);
 
@@ -93,9 +97,11 @@
     @EnforcePermission("USE_COMPANION_TRANSPORTS")
     void removeOnMessageReceivedListener(int messageType, IOnMessageReceivedListener listener);
 
-    void notifyDeviceAppeared(int associationId);
+    @EnforcePermission("REQUEST_COMPANION_SELF_MANAGED")
+    void notifySelfManagedDeviceAppeared(int associationId);
 
-    void notifyDeviceDisappeared(int associationId);
+    @EnforcePermission("REQUEST_COMPANION_SELF_MANAGED")
+    void notifySelfManagedDeviceDisappeared(int associationId);
 
     PendingIntent buildPermissionTransferUserConsentIntent(String callingPackage, int userId,
         int associationId);
@@ -135,10 +141,4 @@
     byte[] getBackupPayload(int userId);
 
     void applyRestoredPayload(in byte[] payload, int userId);
-
-    @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE")
-    void startObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId);
-
-    @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE")
-    void stopObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId);
 }
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index d9614d1..90b7869 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -24,7 +24,7 @@
 import static android.hardware.biometrics.Flags.FLAG_ADD_KEY_AGREEMENT_CRYPTO_OBJECT;
 import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT;
 import static android.hardware.biometrics.Flags.FLAG_GET_OP_ID_CRYPTO_OBJECT;
-import static android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE;
+import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE;
 
 import android.annotation.CallbackExecutor;
 import android.annotation.DrawableRes;
@@ -529,7 +529,7 @@
 
         /**
          * Remove {@link Builder#setAllowBackgroundAuthentication(boolean)} once
-         * FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE is enabled.
+         * FLAG_ALLOW_PRIVATE_PROFILE is enabled.
          *
          * @param allow If true, allows authentication when the calling package is not in the
          *              foreground. This is set to false by default.
@@ -538,7 +538,7 @@
          * @return This builder
          * @hide
          */
-        @FlaggedApi(FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE)
+        @FlaggedApi(FLAG_ALLOW_PRIVATE_PROFILE)
         @TestApi
         @NonNull
         @RequiresPermission(anyOf = {TEST_BIOMETRIC, USE_BIOMETRIC_INTERNAL})
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 58aafbc..ec67212 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -498,6 +498,7 @@
         // Overrides the policy for adjusting screen brightness and state while dozing.
         public int dozeScreenState;
         public float dozeScreenBrightness;
+        public int dozeScreenStateReason;
 
         public DisplayPowerRequest() {
             policy = POLICY_BRIGHT;
@@ -508,6 +509,7 @@
             blockScreenOn = false;
             dozeScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
             dozeScreenState = Display.STATE_UNKNOWN;
+            dozeScreenStateReason = Display.STATE_REASON_UNKNOWN;
         }
 
         public DisplayPowerRequest(DisplayPowerRequest other) {
@@ -529,6 +531,7 @@
             boostScreenBrightness = other.boostScreenBrightness;
             dozeScreenBrightness = other.dozeScreenBrightness;
             dozeScreenState = other.dozeScreenState;
+            dozeScreenStateReason = other.dozeScreenStateReason;
         }
 
         @Override
@@ -551,7 +554,8 @@
                     && lowPowerMode == other.lowPowerMode
                     && boostScreenBrightness == other.boostScreenBrightness
                     && floatEquals(dozeScreenBrightness, other.dozeScreenBrightness)
-                    && dozeScreenState == other.dozeScreenState;
+                    && dozeScreenState == other.dozeScreenState
+                    && dozeScreenStateReason == other.dozeScreenStateReason;
         }
 
         private boolean floatEquals(float f1, float f2) {
@@ -575,7 +579,9 @@
                     + ", lowPowerMode=" + lowPowerMode
                     + ", boostScreenBrightness=" + boostScreenBrightness
                     + ", dozeScreenBrightness=" + dozeScreenBrightness
-                    + ", dozeScreenState=" + Display.stateToString(dozeScreenState);
+                    + ", dozeScreenState=" + Display.stateToString(dozeScreenState)
+                    + ", dozeScreenStateReason="
+                            + Display.stateReasonToString(dozeScreenStateReason);
         }
 
         public static String policyToString(int policy) {
diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java
index fec67b9..df353e5 100644
--- a/core/java/android/os/PowerManagerInternal.java
+++ b/core/java/android/os/PowerManagerInternal.java
@@ -136,11 +136,12 @@
      *
      * @param screenState The overridden screen state, or {@link Display#STATE_UNKNOWN}
      * to disable the override.
+     * @param reason The reason for overriding the screen state.
      * @param screenBrightness The overridden screen brightness, or
      * {@link PowerManager#BRIGHTNESS_DEFAULT} to disable the override.
      */
     public abstract void setDozeOverrideFromDreamManager(
-            int screenState, int screenBrightness);
+            int screenState, @Display.StateReason int reason, int screenBrightness);
 
     /**
      * Used by sidekick manager to tell the power manager if it shouldn't change the display state
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index c3b646a..c74d50a 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -696,6 +696,8 @@
      * Output: When a package data uri is passed as input, the activity result is set to
      * {@link android.app.Activity#RESULT_OK} if the permission was granted to the app. Otherwise,
      * the result is set to {@link android.app.Activity#RESULT_CANCELED}.
+     *
+     * @hide
      */
     @FlaggedApi(Flags.FLAG_BACKUP_TASKS_SETTINGS_SCREEN)
     @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
@@ -1569,23 +1571,6 @@
             "android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
 
     /**
-     * Activity Action: Show screen for controlling any background restrictions imposed on
-     * an app. If the system returns true for
-     * {@link android.app.ActivityManager#isBackgroundRestricted()}, and the app is not able to
-     * satisfy user requests due to being restricted in the background, then this intent can be
-     * used to request the user to unrestrict the app.
-     * <p>
-     * Input: The Intent's data URI must specify the application package name
-     *        to be shown, with the "package" scheme, such as "package:com.my.app".
-     * <p>
-     * Output: Nothing.
-     */
-    @FlaggedApi(android.app.Flags.FLAG_APP_RESTRICTIONS_API)
-    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
-    public static final String ACTION_BACKGROUND_RESTRICTIONS_SETTINGS =
-            "android.settings.BACKGROUND_RESTRICTIONS_SETTINGS";
-
-    /**
      * Activity Action: Open the advanced power usage details page of an associated app.
      * <p>
      * Input: Intent's data URI set with an application name, using the
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 4060b24..6dcbc8e 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -263,7 +263,9 @@
     private boolean mCanDoze;
     private boolean mDozing;
     private boolean mWindowless;
+    private boolean mPreviewMode;
     private int mDozeScreenState = Display.STATE_UNKNOWN;
+    private @Display.StateReason int mDozeScreenStateReason = Display.STATE_REASON_UNKNOWN;
     private int mDozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT;
 
     private boolean mDebug = false;
@@ -647,12 +649,13 @@
     }
 
     /**
-     * Marks this dream as keeping the screen bright while dreaming.
+     * Marks this dream as keeping the screen bright while dreaming. In preview mode, the screen
+     * is always allowed to dim and overrides the value specified here.
      *
      * @param screenBright True to keep the screen bright while dreaming.
      */
     public void setScreenBright(boolean screenBright) {
-        if (mScreenBright != screenBright) {
+        if (mScreenBright != screenBright && !mPreviewMode) {
             mScreenBright = screenBright;
             int flag = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
             applyWindowFlags(mScreenBright ? flag : 0, flag);
@@ -661,7 +664,7 @@
 
     /**
      * Returns whether this dream keeps the screen bright while dreaming.
-     * Defaults to false, allowing the screen to dim if necessary.
+     * Defaults to true, preventing the screen from dimming.
      *
      * @see #setScreenBright(boolean)
      */
@@ -748,7 +751,9 @@
 
         if (mDozing) {
             try {
-                mDreamManager.startDozing(mDreamToken, mDozeScreenState, mDozeScreenBrightness);
+                mDreamManager.startDozing(
+                        mDreamToken, mDozeScreenState, mDozeScreenStateReason,
+                        mDozeScreenBrightness);
             } catch (RemoteException ex) {
                 // system server died
             }
@@ -808,6 +813,19 @@
     }
 
     /**
+     * Same as {@link #setDozeScreenState(int, int)}, but with no screen state reason specified.
+     *
+     * <p>Use {@link #setDozeScreenState(int, int)} whenever possible to allow properly accounting
+     * for the screen state reason.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setDozeScreenState(int state) {
+        setDozeScreenState(state, Display.STATE_REASON_UNKNOWN);
+    }
+
+    /**
      * Sets the screen state to use while dozing.
      * <p>
      * The value of this property determines the power state of the primary display
@@ -841,13 +859,15 @@
      * {@link Display#STATE_DOZE}, {@link Display#STATE_DOZE_SUSPEND},
      * {@link Display#STATE_ON_SUSPEND}, {@link Display#STATE_OFF}, or {@link Display#STATE_UNKNOWN}
      * for the default behavior.
+     * @param reason the reason for setting the specified screen state.
      *
      * @hide For use by system UI components only.
      */
     @UnsupportedAppUsage
-    public void setDozeScreenState(int state) {
+    public void setDozeScreenState(int state, @Display.StateReason int reason) {
         if (mDozeScreenState != state) {
             mDozeScreenState = state;
+            mDozeScreenStateReason = reason;
             updateDoze();
         }
     }
@@ -1280,6 +1300,11 @@
 
         mDreamToken = dreamToken;
         mCanDoze = canDoze;
+        mPreviewMode = isPreviewMode;
+        if (mPreviewMode) {
+            // Allow screen to dim when in preview mode.
+            mScreenBright = false;
+        }
         // This is not a security check to prevent malicious dreams but a guard rail to stop
         // third-party dreams from being windowless and not working well as a result.
         if (mWindowless && !mCanDoze && !isCallerSystemUi()) {
diff --git a/core/java/android/service/dreams/IDreamManager.aidl b/core/java/android/service/dreams/IDreamManager.aidl
index c489c58..e45384f 100644
--- a/core/java/android/service/dreams/IDreamManager.aidl
+++ b/core/java/android/service/dreams/IDreamManager.aidl
@@ -40,7 +40,7 @@
     boolean isDreamingOrInPreview();
     boolean canStartDreaming(boolean isScreenOn);
     void finishSelf(in IBinder token, boolean immediate);
-    void startDozing(in IBinder token, int screenState, int screenBrightness);
+    void startDozing(in IBinder token, int screenState, int reason, int screenBrightness);
     void stopDozing(in IBinder token);
     void forceAmbientDisplayEnabled(boolean enabled);
     ComponentName[] getDreamComponentsForUser(int userId);
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 1f2b4fa..4475418 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -503,6 +503,79 @@
      */
     public static final int STATE_ON_SUSPEND = ViewProtoEnums.DISPLAY_STATE_ON_SUSPEND; // 6
 
+    /**
+     * The cause of the display state change is unknown.
+     *
+     * @hide
+     */
+    public static final int STATE_REASON_UNKNOWN = ViewProtoEnums.DISPLAY_STATE_REASON_UNKNOWN;
+
+    /**
+     * The default power and display policy caused the display state.
+     *
+     * @hide
+     */
+    public static final int STATE_REASON_DEFAULT_POLICY =
+            ViewProtoEnums.DISPLAY_STATE_REASON_DEFAULT_POLICY;
+
+    /**
+     * The display state was changed due to acquiring a draw wake lock.
+     *
+     * @hide
+     */
+    public static final int STATE_REASON_DRAW_WAKE_LOCK =
+            ViewProtoEnums.DISPLAY_STATE_REASON_DRAW_WAKE_LOCK;
+
+    /**
+     * The display state was changed due to display offloading.
+     *
+     * @hide
+     */
+    public static final int STATE_REASON_OFFLOAD = ViewProtoEnums.DISPLAY_STATE_REASON_OFFLOAD;
+
+    /**
+     * The display state was changed due to a tilt event.
+     *
+     * @hide
+     */
+    public static final int STATE_REASON_TILT = ViewProtoEnums.DISPLAY_STATE_REASON_TILT;
+
+    /**
+     * The display state was changed due to the dream manager.
+     *
+     * @hide
+     */
+    public static final int STATE_REASON_DREAM_MANAGER =
+            ViewProtoEnums.DISPLAY_STATE_REASON_DREAM_MANAGER;
+
+    /**
+     * The display state was changed due to a {@link KeyEvent}.
+     *
+     * @hide
+     */
+    public static final int STATE_REASON_KEY = ViewProtoEnums.DISPLAY_STATE_REASON_KEY;
+
+    /**
+     * The display state was changed due to a {@link MotionEvent}.
+     *
+     * @hide
+     */
+    public static final int STATE_REASON_MOTION = ViewProtoEnums.DISPLAY_STATE_REASON_MOTION;
+
+    /** @hide */
+    @IntDef(prefix = {"STATE_REASON_"}, value = {
+        STATE_REASON_UNKNOWN,
+        STATE_REASON_DEFAULT_POLICY,
+        STATE_REASON_DRAW_WAKE_LOCK,
+        STATE_REASON_OFFLOAD,
+        STATE_REASON_TILT,
+        STATE_REASON_DREAM_MANAGER,
+        STATE_REASON_KEY,
+        STATE_REASON_MOTION,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface StateReason {}
+
     /* The color mode constants defined below must be kept in sync with the ones in
      * system/core/include/system/graphics-base.h */
 
@@ -1996,6 +2069,30 @@
         }
     }
 
+    /** @hide */
+    public static String stateReasonToString(@StateReason int reason) {
+        switch (reason) {
+            case STATE_REASON_UNKNOWN:
+                return "UNKNOWN";
+            case STATE_REASON_DEFAULT_POLICY:
+                return "DEFAULT_POLICY";
+            case STATE_REASON_DRAW_WAKE_LOCK:
+                return "DRAW_WAKE_LOCK";
+            case STATE_REASON_OFFLOAD:
+                return "OFFLOAD";
+            case STATE_REASON_TILT:
+                return "TILT";
+            case STATE_REASON_DREAM_MANAGER:
+                return "DREAM_MANAGER";
+            case STATE_REASON_KEY:
+                return "KEY";
+            case STATE_REASON_MOTION:
+                return "MOTION";
+            default:
+                return Integer.toString(reason);
+        }
+    }
+
     /**
      * Returns true if display updates may be suspended while in the specified
      * display power state. In SUSPEND states, updates are absolutely forbidden.
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 85d3688..eb9be18 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -1066,11 +1066,6 @@
     // Used to check if there is a message in the message queue
     // for idleness handling.
     private boolean mHasIdledMessage = false;
-    // Used to allow developers to opt out Toolkit dVRR feature.
-    // This feature allows device to adjust refresh rate
-    // as needed and can be useful for power saving.
-    // Should not enable the dVRR feature if the value is false.
-    private boolean mIsFrameRatePowerSavingsBalanced = true;
     // Used to check if there is a conflict between different frame rate voting.
     // Take 24 and 30 as an example, 24 is not a divisor of 30.
     // We consider there is a conflict.
@@ -12777,7 +12772,10 @@
      */
     @VisibleForTesting
     public boolean isFrameRatePowerSavingsBalanced() {
-        return mIsFrameRatePowerSavingsBalanced;
+        if (sToolkitSetFrameRateReadOnlyFlagValue) {
+            return mWindowAttributes.isFrameRatePowerSavingsBalanced();
+        }
+        return true;
     }
 
     /**
@@ -12789,19 +12787,9 @@
         return mIsFrameRateConflicted;
     }
 
-    /**
-     * Set the value of mIsFrameRatePowerSavingsBalanced
-     * Can be used to checked if toolkit dVRR feature is enabled.
-     */
-    public void setFrameRatePowerSavingsBalanced(boolean enabled) {
-        if (sToolkitSetFrameRateReadOnlyFlagValue) {
-            mIsFrameRatePowerSavingsBalanced = enabled;
-        }
-    }
-
     private boolean shouldEnableDvrr() {
         // uncomment this when we are ready for enabling dVRR
-        // return sToolkitSetFrameRateReadOnlyFlagValue && mIsFrameRatePowerSavingsBalanced;
+        // return sToolkitSetFrameRateReadOnlyFlagValue && isFrameRatePowerSavingsBalanced();
         return false;
 
     }
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index 51229a7..1ebced5 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -334,7 +334,12 @@
     private boolean mOverlayWithDecorCaptionEnabled = true;
     private boolean mCloseOnSwipeEnabled = false;
 
-    private static boolean sToolkitSetFrameRateReadOnlyFlagValue =
+    /**
+     * To check if toolkitSetFrameRateReadOnly flag is enabled
+     *
+     * @hide
+     */
+    protected static boolean sToolkitSetFrameRateReadOnlyFlagValue =
                 android.view.flags.Flags.toolkitSetFrameRateReadOnly();
 
     // The current window attributes.
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index e3caf70..f643bd4 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -345,7 +345,7 @@
      *
      * @hide
      */
-    private static final int MAX_ADAPTER_CONVERSION_WAITING_TIME_MS = 5000;
+    private static final int MAX_ADAPTER_CONVERSION_WAITING_TIME_MS = 20_000;
 
     /**
      * Application that hosts the remote views.
diff --git a/core/java/android/widget/RemoteViewsService.java b/core/java/android/widget/RemoteViewsService.java
index d4a5bbd..07d6acb 100644
--- a/core/java/android/widget/RemoteViewsService.java
+++ b/core/java/android/widget/RemoteViewsService.java
@@ -124,6 +124,41 @@
          * @return True if the same id always refers to the same object.
          */
         public boolean hasStableIds();
+
+        /**
+         * @hide
+         */
+        default RemoteViews.RemoteCollectionItems getRemoteCollectionItems(int capSize) {
+            RemoteViews.RemoteCollectionItems items = new RemoteViews.RemoteCollectionItems
+                    .Builder().build();
+            Parcel capSizeTestParcel = Parcel.obtain();
+            capSizeTestParcel.allowSquashing();
+
+            try {
+                RemoteViews.RemoteCollectionItems.Builder itemsBuilder =
+                        new RemoteViews.RemoteCollectionItems.Builder();
+                onDataSetChanged();
+
+                itemsBuilder.setHasStableIds(hasStableIds());
+                final int numOfEntries = getCount();
+
+                for (int i = 0; i < numOfEntries; i++) {
+                    final long currentItemId = getItemId(i);
+                    final RemoteViews currentView = getViewAt(i);
+                    currentView.writeToParcel(capSizeTestParcel, 0);
+                    if (capSizeTestParcel.dataSize() > capSize) {
+                        break;
+                    }
+                    itemsBuilder.addItem(currentItemId, currentView);
+                }
+
+                items = itemsBuilder.build();
+            } finally {
+                // Recycle the parcel
+                capSizeTestParcel.recycle();
+            }
+            return items;
+        }
     }
 
     /**
@@ -232,33 +267,11 @@
         public RemoteViews.RemoteCollectionItems getRemoteCollectionItems(int capSize) {
             RemoteViews.RemoteCollectionItems items = new RemoteViews.RemoteCollectionItems
                     .Builder().build();
-            Parcel capSizeTestParcel = Parcel.obtain();
-
             try {
-                RemoteViews.RemoteCollectionItems.Builder itemsBuilder =
-                        new RemoteViews.RemoteCollectionItems.Builder();
-                mFactory.onDataSetChanged();
-
-                itemsBuilder.setHasStableIds(mFactory.hasStableIds());
-                final int numOfEntries = mFactory.getCount();
-
-                for (int i = 0; i < numOfEntries; i++) {
-                    final long currentItemId = mFactory.getItemId(i);
-                    final RemoteViews currentView = mFactory.getViewAt(i);
-                    currentView.writeToParcel(capSizeTestParcel, 0);
-                    if (capSizeTestParcel.dataSize() > capSize) {
-                        break;
-                    }
-                    itemsBuilder.addItem(currentItemId, currentView);
-                }
-
-                items = itemsBuilder.build();
+                items = mFactory.getRemoteCollectionItems(capSize);
             } catch (Exception ex) {
                 Thread t = Thread.currentThread();
                 Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
-            } finally {
-                // Recycle the parcel
-                capSizeTestParcel.recycle();
             }
             return items;
         }
diff --git a/core/java/android/window/flags/accessibility.aconfig b/core/java/android/window/flags/accessibility.aconfig
index 590e88b..2d1cbb5 100644
--- a/core/java/android/window/flags/accessibility.aconfig
+++ b/core/java/android/window/flags/accessibility.aconfig
@@ -8,7 +8,7 @@
 }
 
 flag {
-  name: "magnification_always_draw_fullscreen_border"
+  name: "always_draw_magnification_fullscreen_border"
   namespace: "accessibility"
   description: "Always draw fullscreen orange border in fullscreen magnification"
   bug: "291891390"
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 9868ceb..02cb53e 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -365,8 +365,6 @@
 
     private boolean mUseDecorContext = false;
 
-    private boolean mIsFrameRatePowerSavingsBalanced = true;
-
     /** @see ViewRootImpl#mActivityConfigCallback */
     private ActivityConfigCallback mActivityConfigCallback;
 
@@ -2216,7 +2214,6 @@
     void onViewRootImplSet(ViewRootImpl viewRoot) {
         viewRoot.setActivityConfigCallback(mActivityConfigCallback);
         viewRoot.getOnBackInvokedDispatcher().updateContext(getContext());
-        viewRoot.setFrameRatePowerSavingsBalanced(mIsFrameRatePowerSavingsBalanced);
         mProxyOnBackInvokedDispatcher.setActualDispatcher(viewRoot.getOnBackInvokedDispatcher());
         applyDecorFitsSystemWindows();
     }
@@ -2566,8 +2563,11 @@
             requestFeature(FEATURE_ACTIVITY_TRANSITIONS);
         }
         if (a.hasValue(R.styleable.Window_windowIsFrameRatePowerSavingsBalanced)) {
-            mIsFrameRatePowerSavingsBalanced =
-                    a.getBoolean(R.styleable.Window_windowIsFrameRatePowerSavingsBalanced, true);
+            if (sToolkitSetFrameRateReadOnlyFlagValue) {
+                setFrameRatePowerSavingsBalanced(
+                        a.getBoolean(R.styleable.Window_windowIsFrameRatePowerSavingsBalanced,
+                                true));
+            }
         }
 
         mIsTranslucent = a.getBoolean(R.styleable.Window_windowIsTranslucent, false);
diff --git a/core/jni/android_view_MotionEvent.cpp b/core/jni/android_view_MotionEvent.cpp
index 1a86363..b801a69 100644
--- a/core/jni/android_view_MotionEvent.cpp
+++ b/core/jni/android_view_MotionEvent.cpp
@@ -85,24 +85,9 @@
 
 ScopedLocalRef<jobject> android_view_MotionEvent_obtainAsCopy(JNIEnv* env,
                                                               const MotionEvent& event) {
-    ScopedLocalRef<jobject> eventObj(env,
-                                     env->CallStaticObjectMethod(gMotionEventClassInfo.clazz,
-                                                                 gMotionEventClassInfo.obtain));
-    if (env->ExceptionCheck() || !eventObj.get()) {
-        ALOGE("An exception occurred while obtaining a motion event.");
-        LOGE_EX(env);
-        env->ExceptionClear();
-        return ScopedLocalRef<jobject>(env);
-    }
-
-    MotionEvent* destEvent = android_view_MotionEvent_getNativePtr(env, eventObj.get());
-    if (!destEvent) {
-        destEvent = new MotionEvent();
-        android_view_MotionEvent_setNativePtr(env, eventObj, destEvent);
-    }
-
+    std::unique_ptr<MotionEvent> destEvent = std::make_unique<MotionEvent>();
     destEvent->copyFrom(&event, true);
-    return eventObj;
+    return android_view_MotionEvent_obtainFromNative(env, std::move(destEvent));
 }
 
 ScopedLocalRef<jobject> android_view_MotionEvent_obtainFromNative(
@@ -117,6 +102,8 @@
         LOGE_EX(env);
         LOG_ALWAYS_FATAL("An exception occurred while obtaining a Java motion event.");
     }
+    MotionEvent* oldEvent = android_view_MotionEvent_getNativePtr(env, eventObj.get());
+    delete oldEvent;
     android_view_MotionEvent_setNativePtr(env, eventObj, event.release());
     return eventObj;
 }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 6431db9..cfe6f73 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7986,6 +7986,7 @@
          content URI trigger and network constraint set.
          <p>This is a special access permission that can be revoked by the system or the user.
          <p>Protection level: signature|privileged|appop
+         @hide
      -->
     <permission android:name="android.permission.RUN_BACKUP_JOBS"
                 android:protectionLevel="signature|privileged|appop"/>
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index fa364e0..d95a039 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -1067,18 +1067,32 @@
     /**
      * Test the IsFrameRatePowerSavingsBalanced values are properly set
      */
-    @UiThreadTest
     @Test
     @Ignore("Can be enabled only after b/330596920 is ready")
     @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public void votePreferredFrameRate_isFrameRatePowerSavingsBalanced() {
-        ViewRootImpl viewRootImpl = new ViewRootImpl(sContext,
-                sContext.getDisplayNoVerify());
-        assertEquals(viewRootImpl.isFrameRatePowerSavingsBalanced(), true);
-        viewRootImpl.setFrameRatePowerSavingsBalanced(false);
-        assertEquals(viewRootImpl.isFrameRatePowerSavingsBalanced(), false);
-        viewRootImpl.setFrameRatePowerSavingsBalanced(true);
-        assertEquals(viewRootImpl.isFrameRatePowerSavingsBalanced(), true);
+        View view = new View(sContext);
+        attachViewToWindow(view);
+        sInstrumentation.waitForIdleSync();
+
+        ViewRootImpl viewRoot = view.getViewRootImpl();
+        final WindowManager.LayoutParams attrs = viewRoot.mWindowAttributes;
+        assertEquals(attrs.isFrameRatePowerSavingsBalanced(), true);
+        assertEquals(viewRoot.isFrameRatePowerSavingsBalanced(),
+                attrs.isFrameRatePowerSavingsBalanced());
+
+        sInstrumentation.runOnMainSync(() -> {
+            attrs.setFrameRatePowerSavingsBalanced(false);
+            viewRoot.setLayoutParams(attrs, false);
+        });
+        sInstrumentation.waitForIdleSync();
+
+        sInstrumentation.runOnMainSync(() -> {
+            final WindowManager.LayoutParams newAttrs = viewRoot.mWindowAttributes;
+            assertEquals(newAttrs.isFrameRatePowerSavingsBalanced(), false);
+            assertEquals(viewRoot.isFrameRatePowerSavingsBalanced(),
+                    newAttrs.isFrameRatePowerSavingsBalanced());
+        });
     }
 
     /**
diff --git a/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java b/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java
index b9841ff..82251b8 100644
--- a/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java
+++ b/core/tests/coretests/src/com/android/internal/policy/PhoneWindowTest.java
@@ -182,7 +182,8 @@
 
         WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
         wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check
-
+        wmlp.setFrameRatePowerSavingsBalanced(
+                    mPhoneWindow.getAttributes().isFrameRatePowerSavingsBalanced());
         sInstrumentation.runOnMainSync(() -> {
             WindowManager wm = mContext.getSystemService(WindowManager.class);
             wm.addView(decorView, wmlp);
@@ -203,7 +204,8 @@
 
         WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
         wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check
-
+        wmlp.setFrameRatePowerSavingsBalanced(
+                mPhoneWindow.getAttributes().isFrameRatePowerSavingsBalanced());
         sInstrumentation.runOnMainSync(() -> {
             WindowManager wm = mContext.getSystemService(WindowManager.class);
             wm.addView(decorView, wmlp);
diff --git a/media/jni/Android.bp b/media/jni/Android.bp
index 94fce79..d6d74e8 100644
--- a/media/jni/Android.bp
+++ b/media/jni/Android.bp
@@ -122,6 +122,9 @@
         "-Wunused",
         "-Wunreachable-code",
     ],
+
+    // TODO(b/330503129) Workaround build breakage.
+    lto_O0: true,
 }
 
 cc_library_shared {
diff --git a/media/jni/audioeffect/Android.bp b/media/jni/audioeffect/Android.bp
index cf5059c..7caa9e4 100644
--- a/media/jni/audioeffect/Android.bp
+++ b/media/jni/audioeffect/Android.bp
@@ -44,4 +44,7 @@
         "-Wunreachable-code",
         "-DANDROID_UTILS_REF_BASE_DISABLE_IMPLICIT_CONSTRUCTION",
     ],
+
+    // TODO(b/330503129) Workaround LTO build breakage.
+    lto_O0: true,
 }
diff --git a/nfc/java/android/nfc/cardemulation/PollingFrame.java b/nfc/java/android/nfc/cardemulation/PollingFrame.java
index 654e8cc..c6861bf 100644
--- a/nfc/java/android/nfc/cardemulation/PollingFrame.java
+++ b/nfc/java/android/nfc/cardemulation/PollingFrame.java
@@ -171,7 +171,7 @@
         byte[] data = frame.getByteArray(KEY_POLLING_LOOP_DATA);
         mData = (data == null) ? new byte[0] : data;
         mGain = frame.getInt(KEY_POLLING_LOOP_GAIN, -1);
-        mTimestamp = frame.getInt(KEY_POLLING_LOOP_TIMESTAMP);
+        mTimestamp = frame.getLong(KEY_POLLING_LOOP_TIMESTAMP);
         mTriggeredAutoTransact = frame.containsKey(KEY_POLLING_LOOP_TRIGGERED_AUTOTRANSACT)
                 && frame.getBoolean(KEY_POLLING_LOOP_TRIGGERED_AUTOTRANSACT);
     }
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
index 3fb91522..7bb08d2 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
@@ -17,6 +17,8 @@
 package com.android.credentialmanager.common.ui
 
 import android.content.Context
+import android.util.Log
+import com.android.credentialmanager.common.Constants
 import android.widget.RemoteViews
 import androidx.core.content.ContextCompat
 import com.android.credentialmanager.model.get.CredentialEntryInfo
@@ -29,7 +31,8 @@
         private const val SET_ADJUST_VIEW_BOUNDS_METHOD_NAME = "setAdjustViewBounds"
         private const val SET_MAX_HEIGHT_METHOD_NAME = "setMaxHeight"
         private const val SET_BACKGROUND_RESOURCE_METHOD_NAME = "setBackgroundResource"
-        private const val BULLET_POINT = "\u2022"
+        private const val SEPARATOR = " " + "\u2022" + " "
+
         // TODO(jbabs): RemoteViews#setViewPadding renders this as 8dp on the display. Debug why.
         private const val END_ITEMS_PADDING = 28
 
@@ -43,20 +46,14 @@
             var layoutId: Int = com.android.credentialmanager.R.layout
                     .credman_dropdown_presentation_layout
             val remoteViews = RemoteViews(context.packageName, layoutId)
-            if (credentialEntryInfo.credentialType == CredentialType.UNKNOWN) {
-                return remoteViews
-            }
             val displayName = credentialEntryInfo.displayName ?: credentialEntryInfo.userName
             remoteViews.setTextViewText(android.R.id.text1, displayName)
-            val secondaryText =
-                if (credentialEntryInfo.displayName != null
-                    && (credentialEntryInfo.displayName != credentialEntryInfo.userName))
-                    (credentialEntryInfo.userName + " " + BULLET_POINT + " "
-                            + credentialEntryInfo.credentialTypeDisplayName
-                            + " " + BULLET_POINT + " " + credentialEntryInfo.providerDisplayName)
-                else (credentialEntryInfo.credentialTypeDisplayName + " " + BULLET_POINT + " "
-                        + credentialEntryInfo.providerDisplayName)
-            remoteViews.setTextViewText(android.R.id.text2, secondaryText)
+            val secondaryText = getSecondaryText(credentialEntryInfo)
+            if (secondaryText.isNullOrBlank()) {
+                Log.w(Constants.LOG_TAG, "Secondary text for dropdown is null")
+            } else {
+                remoteViews.setTextViewText(android.R.id.text2, secondaryText)
+            }
             remoteViews.setImageViewIcon(android.R.id.icon1, icon);
             remoteViews.setBoolean(
                 android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true);
@@ -88,6 +85,26 @@
             return remoteViews
         }
 
+        /**
+         * Computes the secondary text for dropdown presentation based on available fields.
+         *
+         * <p> Format for secondary text is [username] . [credentialType] . [providerDisplayName]
+         * If display name and username are the same, we do not display username
+         * If credential type is missing as in the case with SiwG, we just display
+         * providerDisplayName. Both credential type and provider display name should not be empty.
+         */
+        private fun getSecondaryText(credentialEntryInfo: CredentialEntryInfo): String? {
+            return listOf(if (credentialEntryInfo.displayName != null
+                && (credentialEntryInfo.displayName != credentialEntryInfo.userName))
+                (credentialEntryInfo.userName) else null,
+                credentialEntryInfo.credentialTypeDisplayName,
+                credentialEntryInfo.providerDisplayName).filterNot { it.isNullOrBlank() }
+                    .let { itemsToDisplay ->
+                        if (itemsToDisplay.isEmpty()) null
+                        else itemsToDisplay.joinToString(separator = SEPARATOR)
+                    }
+        }
+
         fun createMoreSignInOptionsPresentation(context: Context): RemoteViews {
             var layoutId: Int = com.android.credentialmanager.R.layout
                     .credman_dropdown_bottom_sheet
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
index b151a53..4e4c22f 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java
@@ -600,14 +600,25 @@
         }
 
         Setting oldState = mSettings.get(name);
+        String previousOwningPackage = (oldState != null) ? oldState.packageName : null;
+        // If the old state doesn't exist, no need to handle the owning package change
+        final boolean owningPackageChanged = previousOwningPackage != null
+                && !previousOwningPackage.equals(packageName);
+
         String oldValue = (oldState != null) ? oldState.value : null;
         String oldDefaultValue = (oldState != null) ? oldState.defaultValue : null;
         String newDefaultValue = makeDefault ? value : oldDefaultValue;
 
-        int newSize = getNewMemoryUsagePerPackageLocked(packageName,
-                oldValue == null ? name.length() : 0 /* deltaKeySize */,
-                oldValue, value, oldDefaultValue, newDefaultValue);
-        checkNewMemoryUsagePerPackageLocked(packageName, newSize);
+        int newSizeForCurrentPackage = getNewMemoryUsagePerPackageLocked(packageName,
+                /* deltaKeyLength= */ (oldState == null || owningPackageChanged) ? name.length() : 0,
+                /* oldValue= */ owningPackageChanged ? null : oldValue,
+                /* newValue= */ value,
+                /* oldDefaultValue= */ owningPackageChanged ? null : oldDefaultValue,
+                /* newDefaultValue = */ newDefaultValue);
+        // Only check the memory usage for the current package. Even if the owning package
+        // has changed, the previous owning package will only have a reduced memory usage, so
+        // there is no need to check its memory usage.
+        checkNewMemoryUsagePerPackageLocked(packageName, newSizeForCurrentPackage);
 
         Setting newState;
 
@@ -629,7 +640,17 @@
 
         addHistoricalOperationLocked(HISTORICAL_OPERATION_UPDATE, newState);
 
-        updateMemoryUsagePerPackageLocked(packageName, newSize);
+        updateMemoryUsagePerPackageLocked(packageName, newSizeForCurrentPackage);
+
+        if (owningPackageChanged) {
+            int newSizeForPreviousPackage = getNewMemoryUsagePerPackageLocked(previousOwningPackage,
+                    /* deltaKeyLength= */ -name.length(),
+                    /* oldValue= */ oldValue,
+                    /* newValue= */ null,
+                    /* oldDefaultValue= */ oldDefaultValue,
+                    /* newDefaultValue = */ null);
+            updateMemoryUsagePerPackageLocked(previousOwningPackage, newSizeForPreviousPackage);
+        }
 
         scheduleWriteIfNeededLocked();
 
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index e0e31d7..5db97c6 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -585,9 +585,9 @@
                 * Character.BYTES;
         assertEquals(expectedMemUsage2, settingsState.getMemoryUsage(testPackage2));
 
-        // Test system package
+        // Let system package take over testKey1 which is no longer subject to memory usage counting
         settingsState.insertSettingLocked(testKey1, testValue1, null, true, SYSTEM_PACKAGE);
-        assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
+        assertEquals(0, settingsState.getMemoryUsage(TEST_PACKAGE));
         assertEquals(expectedMemUsage2, settingsState.getMemoryUsage(testPackage2));
         assertEquals(0, settingsState.getMemoryUsage(SYSTEM_PACKAGE));
 
@@ -599,7 +599,7 @@
         } catch (IllegalStateException ex) {
             assertTrue(ex.getMessage().contains("You are adding too many system settings"));
         }
-        assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
+        assertEquals(0, settingsState.getMemoryUsage(TEST_PACKAGE));
 
         // Test invalid key
         try {
@@ -609,7 +609,7 @@
         } catch (IllegalStateException ex) {
             assertTrue(ex.getMessage().contains("You are adding too many system settings"));
         }
-        assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE));
+        assertEquals(0, settingsState.getMemoryUsage(TEST_PACKAGE));
     }
 
     @Test
@@ -902,4 +902,49 @@
             assertEquals("false", s.getValue());
         }
     }
+
+    @Test
+    public void testMemoryUsagePerPackage_SameSettingUsedByDifferentPackages() {
+        SettingsState settingsState =
+                new SettingsState(
+                        InstrumentationRegistry.getContext(), mLock, mSettingsFile, 1,
+                        SettingsState.MAX_BYTES_PER_APP_PACKAGE_LIMITED, Looper.getMainLooper());
+        final String testKey1 = SETTING_NAME;
+        final String testKey2 = SETTING_NAME + "_2";
+        final String testValue1 = Strings.repeat("A", 100);
+        final String testValue2 = Strings.repeat("A", 50);
+        final String package1 = "p1";
+        final String package2 = "p2";
+
+        settingsState.insertSettingLocked(testKey1, testValue1, null, false, package1);
+        settingsState.insertSettingLocked(testKey2, testValue1, null, true, package2);
+        // Package1's usage should be remain the same Package2 owns a different setting
+        int expectedMemUsageForPackage1 = (testKey1.length() + testValue1.length())
+                * Character.BYTES;
+        int expectedMemUsageForPackage2 = (testKey2.length() + testValue1.length()
+                + testValue1.length() /* size for default */) * Character.BYTES;
+        assertEquals(expectedMemUsageForPackage1, settingsState.getMemoryUsage(package1));
+        assertEquals(expectedMemUsageForPackage2, settingsState.getMemoryUsage(package2));
+
+        settingsState.insertSettingLocked(testKey1, testValue2, null, false, package2);
+        // Package1's usage should be cleared because the setting is taken over by another package
+        expectedMemUsageForPackage1 = 0;
+        assertEquals(expectedMemUsageForPackage1, settingsState.getMemoryUsage(package1));
+        // Package2 now owns two settings
+        expectedMemUsageForPackage2 = (testKey1.length() + testValue2.length()
+                + testKey2.length() + testValue1.length()
+                + testValue1.length() /* size for default */)
+                * Character.BYTES;
+        assertEquals(expectedMemUsageForPackage2, settingsState.getMemoryUsage(package2));
+
+        settingsState.insertSettingLocked(testKey1, testValue1, null, true, package1);
+        // Package1 now owns setting1
+        expectedMemUsageForPackage1 = (testKey1.length() + testValue1.length()
+                + testValue1.length() /* size for default */) * Character.BYTES;
+        assertEquals(expectedMemUsageForPackage1, settingsState.getMemoryUsage(package1));
+        // Package2 now only own setting2
+        expectedMemUsageForPackage2 = (testKey2.length() + testValue1.length()
+                + testValue1.length() /* size for default */) * Character.BYTES;
+        assertEquals(expectedMemUsageForPackage2, settingsState.getMemoryUsage(package2));
+    }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index e1608c4..503779c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -110,6 +110,8 @@
 import androidx.compose.ui.window.Popup
 import androidx.core.view.setPadding
 import androidx.window.layout.WindowMetricsCalculator
+import com.android.compose.modifiers.height
+import com.android.compose.modifiers.padding
 import com.android.compose.modifiers.thenIf
 import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.compose.ui.graphics.painter.rememberDrawablePainter
@@ -154,6 +156,7 @@
         derivedStateOf { selectedKey.value != null || reorderingWidgets }
     }
     var isButtonToEditWidgetsShowing by remember { mutableStateOf(false) }
+    val isEmptyState by viewModel.isEmptyState.collectAsState(initial = false)
 
     val contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize)
     val contentOffset = beforeContentPadding(contentPadding).toOffset()
@@ -178,7 +181,7 @@
                         viewModel.setSelectedKey(key)
                     }
                 }
-                .thenIf(!viewModel.isEditMode) {
+                .thenIf(!viewModel.isEditMode && !isEmptyState) {
                     Modifier.pointerInput(
                             gridState,
                             contentOffset,
@@ -219,26 +222,33 @@
                         .motionEventSpy { onMotionEvent(viewModel) }
                 },
     ) {
-        CommunalHubLazyGrid(
-            communalContent = communalContent,
-            viewModel = viewModel,
-            contentPadding = contentPadding,
-            contentOffset = contentOffset,
-            setGridCoordinates = { gridCoordinates = it },
-            updateDragPositionForRemove = { offset ->
-                isDraggingToRemove =
-                    isPointerWithinCoordinates(
-                        offset = gridCoordinates?.let { it.positionInWindow() + offset },
-                        containerToCheck = removeButtonCoordinates
-                    )
-                isDraggingToRemove
-            },
-            onOpenWidgetPicker = onOpenWidgetPicker,
-            gridState = gridState,
-            contentListState = contentListState,
-            selectedKey = selectedKey,
-            widgetConfigurator = widgetConfigurator,
-        )
+        if (!viewModel.isEditMode && isEmptyState) {
+            EmptyStateCta(
+                contentPadding = contentPadding,
+                viewModel = viewModel,
+            )
+        } else {
+            CommunalHubLazyGrid(
+                communalContent = communalContent,
+                viewModel = viewModel,
+                contentPadding = contentPadding,
+                contentOffset = contentOffset,
+                setGridCoordinates = { gridCoordinates = it },
+                updateDragPositionForRemove = { offset ->
+                    isDraggingToRemove =
+                        isPointerWithinCoordinates(
+                            offset = gridCoordinates?.let { it.positionInWindow() + offset },
+                            containerToCheck = removeButtonCoordinates
+                        )
+                    isDraggingToRemove
+                },
+                onOpenWidgetPicker = onOpenWidgetPicker,
+                gridState = gridState,
+                contentListState = contentListState,
+                selectedKey = selectedKey,
+                widgetConfigurator = widgetConfigurator,
+            )
+        }
 
         // TODO(b/326060686): Remove this once keyguard indication area can persist over hub
         if (viewModel is CommunalViewModel) {
@@ -460,6 +470,67 @@
     }
 }
 
+/**
+ * The empty state displays a fullscreen call-to-action (CTA) tile when no widgets are available.
+ */
+@Composable
+private fun EmptyStateCta(
+    contentPadding: PaddingValues,
+    viewModel: BaseCommunalViewModel,
+) {
+    val colors = LocalAndroidColorScheme.current
+    Card(
+        modifier = Modifier.height(Dimensions.GridHeight).padding(contentPadding),
+        colors = CardDefaults.cardColors(containerColor = Color.Transparent),
+        border = BorderStroke(3.dp, colors.primaryFixedDim),
+        shape = RoundedCornerShape(size = 80.dp)
+    ) {
+        Column(
+            modifier = Modifier.fillMaxSize().padding(horizontal = 110.dp),
+            verticalArrangement =
+                Arrangement.spacedBy(Dimensions.Spacing, Alignment.CenterVertically),
+            horizontalAlignment = Alignment.CenterHorizontally,
+        ) {
+            Text(
+                text = stringResource(R.string.title_for_empty_state_cta),
+                style = MaterialTheme.typography.displaySmall,
+                textAlign = TextAlign.Center,
+                color = colors.secondaryFixed,
+            )
+            Row(
+                modifier = Modifier.fillMaxWidth(),
+                horizontalArrangement = Arrangement.Center,
+            ) {
+                Button(
+                    modifier = Modifier.height(56.dp),
+                    colors =
+                        ButtonDefaults.buttonColors(
+                            containerColor = colors.primaryFixed,
+                            contentColor = colors.onPrimaryFixed,
+                        ),
+                    onClick = {
+                        viewModel.onOpenWidgetEditor(
+                            shouldOpenWidgetPickerOnStart = true,
+                        )
+                    },
+                ) {
+                    Icon(
+                        imageVector = Icons.Default.Add,
+                        contentDescription =
+                            stringResource(R.string.label_for_button_in_empty_state_cta),
+                        modifier = Modifier.size(24.dp)
+                    )
+                    Spacer(Modifier.width(ButtonDefaults.IconSpacing))
+                    Text(
+                        text = stringResource(R.string.label_for_button_in_empty_state_cta),
+                        style = MaterialTheme.typography.titleSmall,
+                    )
+                }
+            }
+        }
+    }
+}
+
 @Composable
 private fun LockStateIcon(
     isUnlocked: Boolean,
@@ -514,7 +585,7 @@
         horizontalArrangement = Arrangement.SpaceBetween,
         verticalAlignment = Alignment.CenterVertically
     ) {
-        val spacerModifier = Modifier.width(Dimensions.ToolbarButtonSpaceBetween)
+        val spacerModifier = Modifier.width(ButtonDefaults.IconSpacing)
         Button(
             onClick = onOpenWidgetPicker,
             colors = filledButtonColors(),
@@ -669,8 +740,6 @@
         is CommunalContentModel.WidgetContent.DisabledWidget ->
             DisabledWidgetPlaceholder(model, viewModel, modifier)
         is CommunalContentModel.CtaTileInViewMode -> CtaTileInViewModeContent(viewModel, modifier)
-        is CommunalContentModel.CtaTileInEditMode ->
-            CtaTileInEditModeContent(modifier, onOpenWidgetPicker)
         is CommunalContentModel.Smartspace -> SmartspaceContent(model, modifier)
         is CommunalContentModel.Tutorial -> TutorialContent(modifier)
         is CommunalContentModel.Umo -> Umo(viewModel, modifier)
@@ -756,45 +825,6 @@
     }
 }
 
-/** Presents a CTA tile at the end of the hub in edit mode, to add more widgets. */
-@Composable
-private fun CtaTileInEditModeContent(
-    modifier: Modifier = Modifier,
-    onOpenWidgetPicker: (() -> Unit)? = null,
-) {
-    if (onOpenWidgetPicker == null) {
-        throw IllegalArgumentException("onOpenWidgetPicker should not be null.")
-    }
-    val colors = LocalAndroidColorScheme.current
-    Card(
-        modifier = modifier,
-        colors = CardDefaults.cardColors(containerColor = Color.Transparent),
-        border = BorderStroke(1.dp, colors.primary),
-        shape = RoundedCornerShape(200.dp),
-        onClick = onOpenWidgetPicker,
-    ) {
-        Column(
-            modifier = Modifier.fillMaxSize(),
-            verticalArrangement =
-                Arrangement.spacedBy(Dimensions.Spacing, Alignment.CenterVertically),
-            horizontalAlignment = Alignment.CenterHorizontally,
-        ) {
-            Icon(
-                imageVector = Icons.Outlined.Widgets,
-                contentDescription = stringResource(R.string.cta_label_to_open_widget_picker),
-                tint = colors.primary,
-                modifier = Modifier.size(Dimensions.IconSize),
-            )
-            Text(
-                text = stringResource(R.string.cta_label_to_open_widget_picker),
-                style = MaterialTheme.typography.titleLarge,
-                color = colors.primary,
-                textAlign = TextAlign.Center,
-            )
-        }
-    }
-}
-
 @Composable
 private fun WidgetContent(
     viewModel: BaseCommunalViewModel,
@@ -1045,7 +1075,6 @@
     val ToolbarPaddingHorizontal = 16.dp
     val ToolbarButtonPaddingHorizontal = 24.dp
     val ToolbarButtonPaddingVertical = 16.dp
-    val ToolbarButtonSpaceBetween = 8.dp
     val ButtonPadding =
         PaddingValues(
             vertical = ToolbarButtonPaddingVertical,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
index 1adb335..e7bfee3 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
@@ -34,10 +34,12 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.pointerInteropFilter
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.toOffset
 import androidx.compose.ui.unit.toSize
@@ -235,6 +237,7 @@
 }
 
 /** Wrap LazyGrid item with additional modifier needed for drag and drop. */
+@OptIn(ExperimentalComposeUiApi::class)
 @ExperimentalFoundationApi
 @Composable
 fun LazyGridItemScope.DraggableItem(
@@ -269,7 +272,11 @@
     Box(modifier) {
         Box(draggingModifier) { content(dragging) }
         AnimatedVisibility(
-            modifier = Modifier.matchParentSize(),
+            modifier =
+                Modifier.matchParentSize()
+                    // Do not consume motion events in the highlighted item and pass them down to
+                    // the content.
+                    .pointerInteropFilter { false },
             visible = (dragging || selected) && !dragDropState.isDraggingToRemove,
             enter = fadeIn(),
             exit = fadeOut()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index 5349acd..4c656b0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -1,7 +1,5 @@
 package com.android.systemui.scene.ui.composable
 
-import androidx.compose.animation.core.Spring
-import androidx.compose.animation.core.spring
 import androidx.compose.foundation.gestures.Orientation
 import com.android.compose.animation.scene.transitions
 import com.android.systemui.bouncer.ui.composable.Bouncer
@@ -32,7 +30,6 @@
  * Please keep the list sorted alphabetically.
  */
 val SceneContainerTransitions = transitions {
-    defaultSwipeSpec = spring(Spring.DampingRatioLowBouncy, Spring.StiffnessLow)
 
     // Scene transitions
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index a5707e1..8e9d769 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -817,6 +817,13 @@
         }
 
     @Test
+    fun showWidgetEditor_openWidgetPickerOnStart_startsActivity() =
+        testScope.runTest {
+            underTest.showWidgetEditor(shouldOpenWidgetPickerOnStart = true)
+            verify(editWidgetsActivityStarter).startActivity(shouldOpenWidgetPickerOnStart = true)
+        }
+
+    @Test
     fun navigateToCommunalWidgetSettings_startsActivity() =
         testScope.runTest {
             underTest.navigateToCommunalWidgetSettings()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index a7e98ea..9aebc30 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -149,13 +149,11 @@
             val communalContent by collectLastValue(underTest.communalContent)
 
             // Only Widgets and CTA tile are shown.
-            assertThat(communalContent?.size).isEqualTo(3)
+            assertThat(communalContent?.size).isEqualTo(2)
             assertThat(communalContent?.get(0))
                 .isInstanceOf(CommunalContentModel.WidgetContent::class.java)
             assertThat(communalContent?.get(1))
                 .isInstanceOf(CommunalContentModel.WidgetContent::class.java)
-            assertThat(communalContent?.get(2))
-                .isInstanceOf(CommunalContentModel.CtaTileInEditMode::class.java)
         }
 
     @Test
@@ -195,24 +193,20 @@
             val communalContent by collectLastValue(underTest.communalContent)
 
             // Widgets and CTA tile are shown.
-            assertThat(communalContent?.size).isEqualTo(3)
+            assertThat(communalContent?.size).isEqualTo(2)
             assertThat(communalContent?.get(0))
                 .isInstanceOf(CommunalContentModel.WidgetContent::class.java)
             assertThat(communalContent?.get(1))
                 .isInstanceOf(CommunalContentModel.WidgetContent::class.java)
-            assertThat(communalContent?.get(2))
-                .isInstanceOf(CommunalContentModel.CtaTileInEditMode::class.java)
 
             underTest.onDeleteWidget(widgets.get(0).appWidgetId)
 
             // Only one widget and CTA tile remain.
-            assertThat(communalContent?.size).isEqualTo(2)
+            assertThat(communalContent?.size).isEqualTo(1)
             val item = communalContent?.get(0)
             val appWidgetId =
                 if (item is CommunalContentModel.WidgetContent) item.appWidgetId else null
             assertThat(appWidgetId).isEqualTo(widgets.get(1).appWidgetId)
-            assertThat(communalContent?.get(1))
-                .isInstanceOf(CommunalContentModel.CtaTileInEditMode::class.java)
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 8f802b8..5be765d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -194,6 +194,41 @@
         }
 
     @Test
+    fun isEmptyState_isTrue_noWidgetButActiveLiveContent() =
+        testScope.runTest {
+            tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
+
+            widgetRepository.setCommunalWidgets(emptyList())
+            // UMO playing
+            mediaRepository.mediaActive()
+            smartspaceRepository.setCommunalSmartspaceTargets(emptyList())
+
+            val isEmptyState by collectLastValue(underTest.isEmptyState)
+            assertThat(isEmptyState).isTrue()
+        }
+
+    @Test
+    fun isEmptyState_isFalse_withWidgets() =
+        testScope.runTest {
+            tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
+
+            widgetRepository.setCommunalWidgets(
+                listOf(
+                    CommunalWidgetContentModel(
+                        appWidgetId = 1,
+                        priority = 1,
+                        providerInfo = providerInfo,
+                    )
+                ),
+            )
+            mediaRepository.mediaInactive()
+            smartspaceRepository.setCommunalSmartspaceTargets(emptyList())
+
+            val isEmptyState by collectLastValue(underTest.isEmptyState)
+            assertThat(isEmptyState).isFalse()
+        }
+
+    @Test
     fun dismissCta_hidesCtaTileAndShowsPopup_thenHidesPopupAfterTimeout() =
         testScope.runTest {
             tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index a9151e8..2195849 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1119,15 +1119,15 @@
     <!-- Indicator on keyguard to start the communal tutorial. [CHAR LIMIT=100] -->
     <string name="communal_tutorial_indicator_text">Swipe left to start the communal tutorial</string>
 
-    <!-- Text for CTA button that launches the hub mode widget editor on click. [CHAR LIMIT=50] -->
+    <!-- Text for call-to-action button that launches the hub mode widget editor on click. [CHAR LIMIT=50] -->
     <string name="cta_tile_button_to_open_widget_editor">Customize</string>
-    <!-- Text for CTA button that dismisses the tile on click. [CHAR LIMIT=50] -->
+    <!-- Text for call-to-action button that dismisses the tile on click. [CHAR LIMIT=50] -->
     <string name="cta_tile_button_to_dismiss">Dismiss</string>
-    <!-- Label for CTA tile to edit the glanceable hub [CHAR LIMIT=100] -->
+    <!-- Label for call-to-action tile to edit the glanceable hub [CHAR LIMIT=100] -->
     <string name="cta_label_to_edit_widget">Add, remove, and reorder your widgets in this space</string>
-    <!-- Label for CTA tile that opens widget picker on click in edit mode [CHAR LIMIT=50] -->
+    <!-- Label for call-to-action tile that opens widget picker on click in edit mode [CHAR LIMIT=50] -->
     <string name="cta_label_to_open_widget_picker">Add more widgets</string>
-    <!-- Text for the popup to be displayed after dismissing the CTA tile. [CHAR LIMIT=50] -->
+    <!-- Text for the popup to be displayed after dismissing the call-to-action tile. [CHAR LIMIT=50] -->
     <string name="popup_on_dismiss_cta_tile_text">Long press to customize widgets</string>
     <!-- Text for the button to configure widgets after long press. [CHAR LIMIT=50] -->
     <string name="button_to_configure_widgets_text">Customize widgets</string>
@@ -1141,6 +1141,10 @@
     <string name="hub_mode_add_widget_button_text">Add widget</string>
     <!-- Text for the button that exits the hub mode editing mode. [CHAR LIMIT=50] -->
     <string name="hub_mode_editing_exit_button_text">Done</string>
+    <!-- Label for the button in the empty state call-to-action tile that will open the widget picker. [CHAR LIMIT=NONE] -->
+    <string name="label_for_button_in_empty_state_cta">Add widgets</string>
+    <!-- Title for the empty state call-to-action when no widgets are available in the hub. [CHAR LIMIT=NONE] -->
+    <string name="title_for_empty_state_cta">Get quick access to your favorite app widgets without unlocking your tablet.</string>
     <!-- Title for the dialog that redirects users to change allowed widget category in settings. [CHAR LIMIT=NONE] -->
     <string name="dialog_title_to_allow_any_widget">Allow any widget on lock screen?</string>
     <!-- Text for the button in the dialog that opens when tapping on disabled widgets. [CHAR LIMIT=NONE] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 455b192..6462d02 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -987,6 +987,11 @@
         <!-- Setting a placeholder will avoid using the SystemUI icon on the splash screen -->
         <item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_blank</item>
         <item name="wallpaperTextColor">@*android:color/primary_text_material_dark</item>
+
+        <!--
+            TODO(b/309578419): Make the activity handle insets properly and then remove this.
+        -->
+        <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
     </style>
 
     <style name="Theme.VolumePanelActivity" parent="@android:style/Theme.DeviceDefault.DayNight">
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 52025b1..ada984d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -264,8 +264,11 @@
     }
 
     /** Show the widget editor Activity. */
-    fun showWidgetEditor(preselectedKey: String? = null) {
-        editWidgetsActivityStarter.startActivity(preselectedKey)
+    fun showWidgetEditor(
+        preselectedKey: String? = null,
+        shouldOpenWidgetPickerOnStart: Boolean = false,
+    ) {
+        editWidgetsActivityStarter.startActivity(preselectedKey, shouldOpenWidgetPickerOnStart)
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
index fc8d658..7061227 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
@@ -91,13 +91,6 @@
         override val size = CommunalContentSize.HALF
     }
 
-    /** A CTA tile in the glanceable hub edit model which remains visible in the grid. */
-    class CtaTileInEditMode : CommunalContentModel {
-        override val key: String = KEY.CTA_TILE_IN_EDIT_MODE_KEY
-        // Same as widget size.
-        override val size = CommunalContentSize.HALF
-    }
-
     class Tutorial(
         id: Int,
         override var size: CommunalContentSize,
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index c913300..531f1987 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -87,6 +87,9 @@
     /** Whether the popup message triggered by dismissing the CTA tile is showing. */
     open val isPopupOnDismissCtaShowing: Flow<Boolean> = flowOf(false)
 
+    /** Whether the communal hub is empty with no widget available. */
+    open val isEmptyState: Flow<Boolean> = flowOf(false)
+
     /** Hide the popup message triggered by dismissing the CTA tile. */
     open fun onHidePopupAfterDismissCta() {}
 
@@ -103,7 +106,10 @@
     open fun onReorderWidgets(widgetIdToPriorityMap: Map<Int, Int>) {}
 
     /** Called as the UI requests opening the widget editor with an optional preselected widget. */
-    open fun onOpenWidgetEditor(preselectedKey: String? = null) {}
+    open fun onOpenWidgetEditor(
+        preselectedKey: String? = null,
+        shouldOpenWidgetPickerOnStart: Boolean = false,
+    ) {}
 
     /** Called as the UI requests to dismiss the CTA tile. */
     open fun onDismissCtaTile() {}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index afa7c37..b3002cd 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -43,7 +43,6 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.withContext
 
@@ -66,11 +65,9 @@
 
     // Only widgets are editable. The CTA tile comes last in the list and remains visible.
     override val communalContent: Flow<List<CommunalContentModel>> =
-        communalInteractor.widgetContent
-            .map { widgets -> widgets + listOf(CommunalContentModel.CtaTileInEditMode()) }
-            .onEach { models ->
-                logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } }
-            }
+        communalInteractor.widgetContent.onEach { models ->
+            logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } }
+        }
 
     private val _reorderingWidgets = MutableStateFlow(false)
 
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 6e69ed7..c73d738 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -41,8 +41,10 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
 
@@ -83,6 +85,12 @@
                 logger.d({ "Content updated: $str1" }) { str1 = models.joinToString { it.key } }
             }
 
+    override val isEmptyState: Flow<Boolean> =
+        communalInteractor.widgetContent
+            .map { it.isEmpty() }
+            .distinctUntilChanged()
+            .onEach { logger.d("isEmptyState: $it") }
+
     private val _isPopupOnDismissCtaShowing: MutableStateFlow<Boolean> = MutableStateFlow(false)
     override val isPopupOnDismissCtaShowing: Flow<Boolean> =
         _isPopupOnDismissCtaShowing.asStateFlow()
@@ -112,8 +120,10 @@
         }
     }
 
-    override fun onOpenWidgetEditor(preselectedKey: String?) =
-        communalInteractor.showWidgetEditor(preselectedKey)
+    override fun onOpenWidgetEditor(
+        preselectedKey: String?,
+        shouldOpenWidgetPickerOnStart: Boolean,
+    ) = communalInteractor.showWidgetEditor(preselectedKey, shouldOpenWidgetPickerOnStart)
 
     override fun onDismissCtaTile() {
         scope.launch {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index ba18f01..902133d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -60,12 +60,15 @@
         private const val TAG = "EditWidgetsActivity"
         private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag"
         const val EXTRA_PRESELECTED_KEY = "preselected_key"
+        const val EXTRA_OPEN_WIDGET_PICKER_ON_START = "open_widget_picker_on_start"
     }
 
     private val logger = Logger(logBuffer, "EditWidgetsActivity")
 
     private val widgetConfigurator by lazy { widgetConfiguratorFactory.create(this) }
 
+    private var shouldOpenWidgetPickerOnStart = false
+
     private val addWidgetActivityLauncher: ActivityResultLauncher<Intent> =
         registerForActivityResult(StartActivityForResult()) { result ->
             when (result.resultCode) {
@@ -112,6 +115,9 @@
         window.setDecorFitsSystemWindows(false)
 
         val preselectedKey = intent.getStringExtra(EXTRA_PRESELECTED_KEY)
+        shouldOpenWidgetPickerOnStart =
+            intent.getBooleanExtra(EXTRA_OPEN_WIDGET_PICKER_ON_START, false)
+
         communalViewModel.setSelectedKey(preselectedKey)
 
         setContent {
@@ -162,6 +168,11 @@
     override fun onStart() {
         super.onStart()
 
+        if (shouldOpenWidgetPickerOnStart) {
+            onOpenWidgetPicker()
+            shouldOpenWidgetPickerOnStart = false
+        }
+
         logger.i("Starting the communal widget editor activity")
         uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_SHOWN)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt
index d1843af..76be005 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivityStarter.kt
@@ -18,13 +18,17 @@
 
 import android.content.Context
 import android.content.Intent
+import com.android.systemui.communal.widgets.EditWidgetsActivity.Companion.EXTRA_OPEN_WIDGET_PICKER_ON_START
 import com.android.systemui.communal.widgets.EditWidgetsActivity.Companion.EXTRA_PRESELECTED_KEY
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.plugins.ActivityStarter
 import javax.inject.Inject
 
 interface EditWidgetsActivityStarter {
-    fun startActivity(preselectedKey: String? = null)
+    fun startActivity(
+        preselectedKey: String? = null,
+        shouldOpenWidgetPickerOnStart: Boolean = false,
+    )
 }
 
 class EditWidgetsActivityStarterImpl
@@ -34,11 +38,14 @@
     private val activityStarter: ActivityStarter,
 ) : EditWidgetsActivityStarter {
 
-    override fun startActivity(preselectedKey: String?) {
+    override fun startActivity(preselectedKey: String?, shouldOpenWidgetPickerOnStart: Boolean) {
         activityStarter.startActivityDismissingKeyguard(
             Intent(applicationContext, EditWidgetsActivity::class.java)
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
-                .apply { preselectedKey?.let { putExtra(EXTRA_PRESELECTED_KEY, preselectedKey) } },
+                .apply {
+                    preselectedKey?.let { putExtra(EXTRA_PRESELECTED_KEY, preselectedKey) }
+                    putExtra(EXTRA_OPEN_WIDGET_PICKER_ON_START, shouldOpenWidgetPickerOnStart)
+                },
             /* onlyProvisioned = */ true,
             /* dismissShade = */ true,
         )
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 182798e..53aee5d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -2520,18 +2520,8 @@
             String message = "";
             switch (msg.what) {
                 case SHOW:
-                    // There is a potential race condition when SysUI starts up. CentralSurfaces
-                    // must invoke #registerCentralSurfaces on this class before any messages can be
-                    // processed. If this happens, repost the message with a small delay and try
-                    // again.
-                    if (mCentralSurfaces == null) {
-                        message = "DELAYING SHOW";
-                        Message newMsg = mHandler.obtainMessage(SHOW, (Bundle) msg.obj);
-                        mHandler.sendMessageDelayed(newMsg, 100);
-                    } else {
-                        message = "SHOW";
-                        handleShow((Bundle) msg.obj);
-                    }
+                    message = "SHOW";
+                    handleShow((Bundle) msg.obj);
                     break;
                 case HIDE:
                     message = "HIDE";
@@ -2618,18 +2608,8 @@
                     Trace.endSection();
                     break;
                 case SYSTEM_READY:
-                    // There is a potential race condition when SysUI starts up. CentralSurfaces
-                    // must invoke #registerCentralSurfaces on this class before any messages can be
-                    // processed. If this happens, repost the message with a small delay and try
-                    // again.
-                    if (mCentralSurfaces == null) {
-                        message = "DELAYING SYSTEM_READY";
-                        Message newMsg = mHandler.obtainMessage(SYSTEM_READY);
-                        mHandler.sendMessageDelayed(newMsg, 100);
-                    } else {
-                        message = "SYSTEM_READY";
-                        handleSystemReady();
-                    }
+                    message = "SYSTEM_READY";
+                    handleSystemReady();
                     break;
             }
             Log.d(TAG, "KeyguardViewMediator queue processing message: " + message);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
index 82c28ff..65b42e6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
@@ -74,6 +74,14 @@
      * exit bouncer.
      */
     fun dispatchKeyEventPreIme(event: KeyEvent): Boolean {
+        when (event.action) {
+            KeyEvent.ACTION_DOWN -> {
+                val device = event.getDevice()
+                if (device != null && device.isFullKeyboard() && device.isExternal()) {
+                    powerInteractor.onUserTouch()
+                }
+            }
+        }
         when (event.keyCode) {
             KeyEvent.KEYCODE_BACK ->
                 if (
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
index 4cb342b..1eea556 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
@@ -19,14 +19,16 @@
 import android.graphics.PixelFormat
 import android.view.Gravity
 import android.view.LayoutInflater
+import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
+import android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
 import androidx.constraintlayout.widget.ConstraintLayout
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.CoreStartable
-import com.android.systemui.biometrics.Utils
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
@@ -70,8 +72,9 @@
                     WindowManager.LayoutParams.MATCH_PARENT,
                     WindowManager.LayoutParams.MATCH_PARENT,
                     WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG,
-                    Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS,
-                    PixelFormat.TRANSLUCENT
+                    WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
+                        WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
+                    PixelFormat.TRANSLUCENT,
                 )
                 .apply {
                     title = "AlternateBouncerView"
@@ -113,8 +116,36 @@
         }
 
         windowManager.get().removeView(alternateBouncerView)
+        alternateBouncerView!!.removeOnAttachStateChangeListener(onAttachAddBackGestureHandler)
+        alternateBouncerView = null
     }
 
+    private val onAttachAddBackGestureHandler =
+        object : View.OnAttachStateChangeListener {
+            private val onBackInvokedCallback: OnBackInvokedCallback = OnBackInvokedCallback {
+                onBackRequested()
+            }
+
+            override fun onViewAttachedToWindow(view: View) {
+                view
+                    .findOnBackInvokedDispatcher()
+                    ?.registerOnBackInvokedCallback(
+                        OnBackInvokedDispatcher.PRIORITY_OVERLAY,
+                        onBackInvokedCallback,
+                    )
+            }
+
+            override fun onViewDetachedFromWindow(view: View) {
+                view
+                    .findOnBackInvokedDispatcher()
+                    ?.unregisterOnBackInvokedCallback(onBackInvokedCallback)
+            }
+
+            fun onBackRequested() {
+                alternateBouncerDependencies.get().viewModel.hideAlternateBouncer()
+            }
+        }
+
     private fun addViewToWindowManager() {
         if (alternateBouncerView?.isAttachedToWindow == true) {
             return
@@ -125,6 +156,7 @@
                 as ConstraintLayout
 
         windowManager.get().addView(alternateBouncerView, layoutParams)
+        alternateBouncerView!!.addOnAttachStateChangeListener(onAttachAddBackGestureHandler)
     }
 
     /** Binds the view to the view-model, continuing to update the former based on the latter. */
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index a4b32a7..c93ef65 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -69,8 +69,6 @@
 import android.util.Log;
 import android.util.MathUtils;
 import android.view.HapticFeedbackConstants;
-import android.view.InputDevice;
-import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
@@ -169,7 +167,6 @@
 import com.android.systemui.shade.data.repository.FlingInfo;
 import com.android.systemui.shade.data.repository.ShadeRepository;
 import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor;
-import com.android.systemui.shade.transition.ShadeTransitionController;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.GestureRecorder;
@@ -357,7 +354,6 @@
     private final QuickSettingsControllerImpl mQsController;
     private final NaturalScrollingSettingObserver mNaturalScrollingSettingObserver;
     private final TouchHandler mTouchHandler = new TouchHandler();
-    private final KeyHandler mKeyHandler = new KeyHandler();
 
     private long mDownTime;
     private boolean mTouchSlopExceededBeforeDown;
@@ -746,7 +742,6 @@
             NotificationListContainer notificationListContainer,
             NotificationStackSizeCalculator notificationStackSizeCalculator,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
-            ShadeTransitionController shadeTransitionController,
             SystemClock systemClock,
             KeyguardBottomAreaViewModel keyguardBottomAreaViewModel,
             KeyguardBottomAreaInteractor keyguardBottomAreaInteractor,
@@ -818,7 +813,6 @@
 
         mView.addOnLayoutChangeListener(new ShadeLayoutChangeListener());
         mView.setOnTouchListener(getTouchHandler());
-        mView.setOnKeyListener(getKeyHandler());
         mView.setOnConfigurationChangedListener(config -> loadDimens());
 
         mResources = mView.getResources();
@@ -903,7 +897,6 @@
         mKeyguardBypassController = bypassController;
         mUpdateMonitor = keyguardUpdateMonitor;
         mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
-        shadeTransitionController.setShadeViewController(this);
         dynamicPrivacyController.addListener(this::onDynamicPrivacyChanged);
         quickSettingsController.setExpansionHeightListener(this::onQsSetExpansionHeightCalled);
         quickSettingsController.setQsStateUpdateListener(this::onQsStateUpdated);
@@ -3586,11 +3579,6 @@
         return mTouchHandler;
     }
 
-    @VisibleForTesting
-    KeyHandler getKeyHandler() {
-        return mKeyHandler;
-    }
-
     @Override
     public boolean closeUserSwitcherIfOpen() {
         if (mKeyguardUserSwitcherController != null) {
@@ -5245,21 +5233,6 @@
         }
     }
 
-    /** Handles KeyEvents for the Shade. */
-    public final class KeyHandler implements View.OnKeyListener {
-        @Override
-        public boolean onKey(View v, int keyCode, KeyEvent event) {
-            if (event.getAction() == KeyEvent.ACTION_DOWN) {
-                final InputDevice d = event.getDevice();
-                // Trigger user activity if the event comes from a full external keyboard
-                if (d != null && d.isFullKeyboard() && d.isExternal()) {
-                    mCentralSurfaces.userActivity();
-                }
-            }
-            return false;
-        }
-    }
-
     private final class HeadsUpNotificationViewControllerImpl implements
             HeadsUpTouchHelper.HeadsUpNotificationViewController {
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index 3a0e167..7525184 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -62,7 +62,6 @@
 import com.android.internal.logging.nano.MetricsProto;
 import com.android.internal.policy.ScreenDecorationsUtils;
 import com.android.internal.policy.SystemBarUtils;
-import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.DejankUtils;
 import com.android.systemui.Dumpable;
 import com.android.systemui.classifier.Classifier;
@@ -80,7 +79,6 @@
 import com.android.systemui.screenrecord.RecordingController;
 import com.android.systemui.shade.data.repository.ShadeRepository;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
-import com.android.systemui.shade.transition.ShadeTransitionController;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
@@ -133,9 +131,7 @@
     private final FrameLayout mQsFrame;
 
     private final QsFrameTranslateController mQsFrameTranslateController;
-    private final ShadeTransitionController mShadeTransitionController;
     private final PulseExpansionHandler mPulseExpansionHandler;
-    private final ShadeExpansionStateManager mShadeExpansionStateManager;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private final LightBarController mLightBarController;
     private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
@@ -147,7 +143,6 @@
     private final KeyguardBypassController mKeyguardBypassController;
     private final NotificationRemoteInputManager mRemoteInputManager;
     private VelocityTracker mQsVelocityTracker;
-    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     private final ScrimController mScrimController;
     private final MediaDataManager mMediaDataManager;
     private final MediaHierarchyManager mMediaHierarchyManager;
@@ -309,10 +304,8 @@
             Lazy<NotificationPanelViewController> panelViewControllerLazy,
             NotificationPanelView panelView,
             QsFrameTranslateController qsFrameTranslateController,
-            ShadeTransitionController shadeTransitionController,
             PulseExpansionHandler pulseExpansionHandler,
             NotificationRemoteInputManager remoteInputManager,
-            ShadeExpansionStateManager shadeExpansionStateManager,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             LightBarController lightBarController,
             NotificationStackScrollLayoutController notificationStackScrollLayoutController,
@@ -322,7 +315,6 @@
             StatusBarTouchableRegionManager statusBarTouchableRegionManager,
             KeyguardStateController keyguardStateController,
             KeyguardBypassController keyguardBypassController,
-            KeyguardUpdateMonitor keyguardUpdateMonitor,
             ScrimController scrimController,
             MediaDataManager mediaDataManager,
             MediaHierarchyManager mediaHierarchyManager,
@@ -353,7 +345,6 @@
         mSplitShadeStateController = splitShadeStateController;
         mSplitShadeEnabled = mSplitShadeStateController.shouldUseSplitNotificationShade(mResources);
         mQsFrameTranslateController = qsFrameTranslateController;
-        mShadeTransitionController = shadeTransitionController;
         mPulseExpansionHandler = pulseExpansionHandler;
         pulseExpansionHandler.setPulseExpandAbortListener(() -> {
             if (mQs != null) {
@@ -361,7 +352,6 @@
             }
         });
         mRemoteInputManager = remoteInputManager;
-        mShadeExpansionStateManager = shadeExpansionStateManager;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mLightBarController = lightBarController;
         mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
@@ -371,7 +361,6 @@
         mStatusBarTouchableRegionManager = statusBarTouchableRegionManager;
         mKeyguardStateController = keyguardStateController;
         mKeyguardBypassController = keyguardBypassController;
-        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mScrimController = scrimController;
         mMediaDataManager = mediaDataManager;
         mMediaHierarchyManager = mediaHierarchyManager;
@@ -2180,7 +2169,6 @@
                 }
             });
             mLockscreenShadeTransitionController.setQS(mQs);
-            mShadeTransitionController.setQs(mQs);
             mNotificationStackScrollLayoutController.setQsHeader((ViewGroup) mQs.getHeader());
             mQs.setScrollListener(mQsScrollListener);
             updateExpansion();
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
index e40bcd5..df5ff5a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeExpansionStateManager.kt
@@ -56,11 +56,6 @@
         return ShadeExpansionChangeEvent(fraction, expanded, tracking, dragDownPxAmount)
     }
 
-    /** Removes an expansion listener. */
-    fun removeExpansionListener(listener: ShadeExpansionListener) {
-        expansionListeners.remove(listener)
-    }
-
     /** Adds a listener that will be notified when the panel state has changed. */
     @Deprecated("Use ShadeInteractor instead")
     fun addStateListener(listener: ShadeStateListener) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
index 1fb0fb6..60810a0 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.shade.TouchLogger.Companion.logTouchesTo
 import com.android.systemui.shade.data.repository.ShadeRepository
 import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.shade.transition.ScrimShadeTransitionController
 import com.android.systemui.statusbar.policy.SplitShadeStateController
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -47,12 +48,14 @@
     private val controller: SplitShadeStateController,
     private val shadeController: ShadeController,
     private val shadeHeaderController: ShadeHeaderController,
+    private val scrimShadeTransitionController: ScrimShadeTransitionController,
 ) : CoreStartable {
 
     override fun start() {
         hydrateShadeMode()
         logTouchesTo(touchLog)
         initHeaderController()
+        scrimShadeTransitionController.init()
     }
 
     private fun initHeaderController() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
index abb69f6..151e289 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
@@ -17,27 +17,55 @@
 package com.android.systemui.shade.transition
 
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.shade.PanelState
 import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
 import com.android.systemui.statusbar.phone.ScrimController
+import dagger.Lazy
 import java.io.PrintWriter
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
 
 /** Controls the scrim properties during the shade expansion transition on non-lockscreen. */
 @SysUISingleton
 class ScrimShadeTransitionController
 @Inject
 constructor(
-    dumpManager: DumpManager,
+    @Application private val applicationScope: CoroutineScope,
+    private val shadeExpansionStateManager: ShadeExpansionStateManager,
+    private val panelExpansionInteractor: Lazy<PanelExpansionInteractor>,
+    private val dumpManager: DumpManager,
     private val scrimController: ScrimController,
 ) {
-
     private var lastExpansionFraction: Float? = null
     private var lastExpansionEvent: ShadeExpansionChangeEvent? = null
     private var currentPanelState: Int? = null
 
-    init {
+    fun init() {
+        if (SceneContainerFlag.isEnabled) {
+            applicationScope.launch {
+                panelExpansionInteractor.get().legacyPanelExpansion.collect { panelExpansion ->
+                    onPanelExpansionChanged(
+                        ShadeExpansionChangeEvent(
+                            fraction = panelExpansion,
+                            expanded = panelExpansion > 0f,
+                            tracking = true,
+                            dragDownPxAmount = 0f,
+                        )
+                    )
+                }
+            }
+        } else {
+            val currentState =
+                shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged)
+            onPanelExpansionChanged(currentState)
+            shadeExpansionStateManager.addStateListener(this::onPanelStateChanged)
+        }
         dumpManager.registerDumpable(
             ScrimShadeTransitionController::class.java.simpleName,
             this::dump
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
deleted file mode 100644
index 3a5c5e1..0000000
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.shade.transition
-
-import android.content.Context
-import android.content.res.Configuration
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.plugins.qs.QS
-import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
-import com.android.systemui.scene.shared.flag.SceneContainerFlag
-import com.android.systemui.shade.PanelState
-import com.android.systemui.shade.ShadeExpansionChangeEvent
-import com.android.systemui.shade.ShadeExpansionStateManager
-import com.android.systemui.shade.ShadeViewController
-import com.android.systemui.shade.panelStateToString
-import com.android.systemui.statusbar.StatusBarState
-import com.android.systemui.statusbar.SysuiStatusBarStateController
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.policy.ConfigurationController
-import com.android.systemui.statusbar.policy.SplitShadeStateController
-import dagger.Lazy
-import java.io.PrintWriter
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-
-/** Controls the shade expansion transition on non-lockscreen. */
-@SysUISingleton
-class ShadeTransitionController
-@Inject
-constructor(
-    @Application private val applicationScope: CoroutineScope,
-    configurationController: ConfigurationController,
-    shadeExpansionStateManager: ShadeExpansionStateManager,
-    dumpManager: DumpManager,
-    private val context: Context,
-    private val scrimShadeTransitionController: ScrimShadeTransitionController,
-    private val statusBarStateController: SysuiStatusBarStateController,
-    private val splitShadeStateController: SplitShadeStateController,
-    private val panelExpansionInteractor: Lazy<PanelExpansionInteractor>,
-) {
-
-    lateinit var shadeViewController: ShadeViewController
-    lateinit var notificationStackScrollLayoutController: NotificationStackScrollLayoutController
-    lateinit var qs: QS
-
-    private var inSplitShade = false
-    private var currentPanelState: Int? = null
-    private var lastShadeExpansionChangeEvent: ShadeExpansionChangeEvent? = null
-
-    init {
-        updateResources()
-        configurationController.addCallback(
-            object : ConfigurationController.ConfigurationListener {
-                override fun onConfigChanged(newConfig: Configuration?) {
-                    updateResources()
-                }
-            }
-        )
-        if (SceneContainerFlag.isEnabled) {
-            applicationScope.launch {
-                panelExpansionInteractor.get().legacyPanelExpansion.collect { panelExpansion ->
-                    onPanelExpansionChanged(
-                        ShadeExpansionChangeEvent(
-                            fraction = panelExpansion,
-                            expanded = panelExpansion > 0f,
-                            tracking = true,
-                            dragDownPxAmount = 0f,
-                        )
-                    )
-                }
-            }
-        } else {
-            val currentState =
-                shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged)
-            onPanelExpansionChanged(currentState)
-            shadeExpansionStateManager.addStateListener(this::onPanelStateChanged)
-        }
-        dumpManager.registerCriticalDumpable("ShadeTransitionController") { printWriter, _ ->
-            dump(printWriter)
-        }
-    }
-
-    private fun updateResources() {
-        inSplitShade = splitShadeStateController.shouldUseSplitNotificationShade(context.resources)
-    }
-
-    private fun onPanelStateChanged(@PanelState state: Int) {
-        currentPanelState = state
-        scrimShadeTransitionController.onPanelStateChanged(state)
-    }
-
-    private fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
-        lastShadeExpansionChangeEvent = event
-        scrimShadeTransitionController.onPanelExpansionChanged(event)
-    }
-
-    private fun dump(pw: PrintWriter) {
-        pw.println(
-            """
-            ShadeTransitionController:
-                inSplitShade: $inSplitShade
-                isScreenUnlocked: ${isScreenUnlocked()}
-                currentPanelState: ${currentPanelState?.panelStateToString()}
-                lastPanelExpansionChangeEvent: $lastShadeExpansionChangeEvent
-                qs.isInitialized: ${this::qs.isInitialized}
-                npvc.isInitialized: ${this::shadeViewController.isInitialized}
-                nssl.isInitialized: ${this::notificationStackScrollLayoutController.isInitialized}
-            """
-                .trimIndent()
-        )
-    }
-
-    private fun isScreenUnlocked() =
-        statusBarStateController.currentOrUpcomingState == StatusBarState.SHADE
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java
index 80c3551..321b608 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java
@@ -71,6 +71,15 @@
     @NonNull
     public StatusBarNotification rebuildForCanceledSmartReplies(
             NotificationEntry entry) {
+        return rebuildWithExistingReplies(entry);
+    }
+
+    /**
+     * Rebuilds to include any previously-added remote input replies.
+     * For when the app cancels a notification that has already been lifetime extended.
+     */
+    @NonNull
+    public StatusBarNotification rebuildWithExistingReplies(NotificationEntry entry) {
         return rebuildWithRemoteInputInserted(entry, null /* remoteInputText */,
                 false /* showSpinner */, null /* mimeType */, null /* uri */);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
index 28fff15..fe59d73 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
@@ -127,6 +127,15 @@
                             mSmartReplyController.stopSending(entry)
                             mNotifUpdater.onInternalNotificationUpdate(newSbn,
                                     "Extending lifetime of notification with smart reply")
+                        } else {
+                            // The app may have re-cancelled a notification after it had already
+                            // been lifetime extended.
+                            // Rebuild the notification with the replies it already had to ensure
+                            // those replies continue to be displayed.
+                            val newSbn = mRebuilder.rebuildWithExistingReplies(entry)
+                            mNotifUpdater.onInternalNotificationUpdate(newSbn,
+                                    "Extending lifetime of notification that has already been " +
+                                            "lifetime extended.")
                         }
                     } else {
                         // Notifications updated without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
index e4db4c7..ac2a0d8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
@@ -16,12 +16,14 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator
 
+import android.app.Notification
 import android.os.UserHandle
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.server.notification.Flags.screenshareNotificationHiding
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
 import com.android.systemui.statusbar.notification.DynamicPrivacyController
 import com.android.systemui.statusbar.notification.collection.GroupEntry
 import com.android.systemui.statusbar.notification.collection.ListEntry
@@ -67,6 +69,17 @@
         invalidateList("onSensitiveStateChanged")
     }
 
+    private val screenshareSecretFilter = object : NotifFilter("ScreenshareSecretFilter") {
+        val NotificationEntry.isSecret
+            get() = channel?.lockscreenVisibility == Notification.VISIBILITY_SECRET ||
+                sbn.notification?.visibility == Notification.VISIBILITY_SECRET
+        override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean {
+            return screenshareNotificationHiding() &&
+                sensitiveNotificationProtectionController.isSensitiveStateActive &&
+                entry.isSecret
+        }
+    }
+
     override fun attach(pipeline: NotifPipeline) {
         dynamicPrivacyController.addListener(this)
         if (screenshareNotificationHiding()) {
@@ -75,6 +88,9 @@
         }
         pipeline.addOnBeforeRenderListListener(this)
         pipeline.addPreRenderInvalidator(this)
+        if (screenshareNotificationHiding()) {
+            pipeline.addFinalizeFilter(screenshareSecretFilter)
+        }
     }
 
     override fun onDynamicPrivacyChanged(): Unit = invalidateList("onDynamicPrivacyChanged")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
index 6836816..c4d9ab7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
@@ -207,11 +207,6 @@
     override fun shouldSuppress(entry: NotificationEntry) = !entry.canBubble()
 }
 
-class BubbleAppSuspendedSuppressor :
-    VisualInterruptionFilter(types = setOf(BUBBLE), reason = "app is suspended") {
-    override fun shouldSuppress(entry: NotificationEntry) = entry.ranking.isSuspended
-}
-
 class BubbleNoMetadataSuppressor() :
     VisualInterruptionFilter(types = setOf(BUBBLE), reason = "has no or invalid bubble metadata") {
 
@@ -221,6 +216,11 @@
     override fun shouldSuppress(entry: NotificationEntry) = !isValidMetadata(entry.bubbleMetadata)
 }
 
+class AlertAppSuspendedSuppressor :
+    VisualInterruptionFilter(types = setOf(PEEK, PULSE, BUBBLE), reason = "app is suspended") {
+    override fun shouldSuppress(entry: NotificationEntry) = entry.ranking.isSuspended
+}
+
 class AlertKeyguardVisibilitySuppressor(
     private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider
 ) : VisualInterruptionFilter(types = setOf(PEEK, PULSE, BUBBLE), reason = "hidden on keyguard") {
@@ -228,11 +228,11 @@
         keyguardNotificationVisibilityProvider.shouldHideNotification(entry)
 }
 
-
 class AvalancheSuppressor(
     private val avalancheProvider: AvalancheProvider,
     private val systemClock: SystemClock,
-) : VisualInterruptionFilter(
+) :
+    VisualInterruptionFilter(
         types = setOf(PEEK, PULSE),
         reason = "avalanche",
     ) {
@@ -261,7 +261,7 @@
         return suppress
     }
 
-    private fun calculateState(entry: NotificationEntry): State  {
+    private fun calculateState(entry: NotificationEntry): State {
         if (
             entry.ranking.isConversation &&
                 entry.sbn.notification.`when` > avalancheProvider.startTime
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
index 334e08d..a2f97bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
@@ -56,11 +56,11 @@
         }
     }
 
-    fun logSuspendedAppBubble(entry: NotificationEntry) {
+    fun logNoAlertingAppSuspended(entry: NotificationEntry) {
         buffer.log(TAG, DEBUG, {
             str1 = entry.logKey
         }, {
-            "No bubble up: notification: app $str1 is suspended"
+            "No alerting: app is suspended: $str1"
         })
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index a655c72..d591114 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -204,11 +204,6 @@
             return false;
         }
 
-        if (entry.getRanking().isSuspended()) {
-            mLogger.logSuspendedAppBubble(entry);
-            return false;
-        }
-
         if (entry.getBubbleMetadata() == null
                 || (entry.getBubbleMetadata().getShortcutId() == null
                     && entry.getBubbleMetadata().getIntent() == null)) {
@@ -559,6 +554,13 @@
             }
         }
 
+        if (entry.getRanking().isSuspended()) {
+            if (log) {
+                mLogger.logNoAlertingAppSuspended(entry);
+            }
+            return false;
+        }
+
         if (mKeyguardNotificationVisibilityProvider.shouldHideNotification(entry)) {
             if (log) mLogger.logNoAlertingNotificationHidden(entry);
             return false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
index dabb18b..375b6e5c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
@@ -46,23 +46,22 @@
 class VisualInterruptionDecisionProviderImpl
 @Inject
 constructor(
-        private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
-        private val batteryController: BatteryController,
-        deviceProvisionedController: DeviceProvisionedController,
-        private val eventLog: EventLog,
-        private val globalSettings: GlobalSettings,
-        private val headsUpManager: HeadsUpManager,
-        private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
-        keyguardStateController: KeyguardStateController,
-        private val logger: VisualInterruptionDecisionLogger,
-        @Main private val mainHandler: Handler,
-        private val powerManager: PowerManager,
-        private val statusBarStateController: StatusBarStateController,
-        private val systemClock: SystemClock,
-        private val uiEventLogger: UiEventLogger,
-        private val userTracker: UserTracker,
-        private val avalancheProvider: AvalancheProvider
-
+    private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
+    private val batteryController: BatteryController,
+    deviceProvisionedController: DeviceProvisionedController,
+    private val eventLog: EventLog,
+    private val globalSettings: GlobalSettings,
+    private val headsUpManager: HeadsUpManager,
+    private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
+    keyguardStateController: KeyguardStateController,
+    private val logger: VisualInterruptionDecisionLogger,
+    @Main private val mainHandler: Handler,
+    private val powerManager: PowerManager,
+    private val statusBarStateController: StatusBarStateController,
+    private val systemClock: SystemClock,
+    private val uiEventLogger: UiEventLogger,
+    private val userTracker: UserTracker,
+    private val avalancheProvider: AvalancheProvider
 ) : VisualInterruptionDecisionProvider {
 
     init {
@@ -164,10 +163,10 @@
         addFilter(PulseLockscreenVisibilityPrivateSuppressor())
         addFilter(PulseLowImportanceSuppressor())
         addFilter(BubbleNotAllowedSuppressor())
-        addFilter(BubbleAppSuspendedSuppressor())
         addFilter(BubbleNoMetadataSuppressor())
         addFilter(HunGroupAlertBehaviorSuppressor())
         addFilter(HunJustLaunchedFsiSuppressor())
+        addFilter(AlertAppSuspendedSuppressor())
         addFilter(AlertKeyguardVisibilitySuppressor(keyguardNotificationVisibilityProvider))
 
         if (NotificationAvalancheSuppression.isEnabled) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index f76de04c..710cdcd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -836,7 +836,6 @@
         mLightRevealScrimViewModelLazy = lightRevealScrimViewModelLazy;
         mLightRevealScrim = lightRevealScrim;
 
-        // Based on teamfood flag, turn predictive back dispatch on at runtime.
         if (predictiveBackSysui()) {
             mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true);
         }
@@ -3060,6 +3059,9 @@
         public void onConfigChanged(Configuration newConfig) {
             updateResources();
             updateDisplaySize(); // populates mDisplayMetrics
+            if (predictiveBackSysui()) {
+                mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true);
+            }
 
             if (DEBUG) {
                 Log.v(TAG, "configuration changed: " + mContext.getResources().getConfiguration());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 272b488..11ec417f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -306,28 +306,6 @@
 
     @Test
     @TestableLooper.RunWithLooper(setAsMainLooper = true)
-    public void testRaceCondition_doNotRegisterCentralSurfacesImmediately() {
-        create(false);
-
-        // GIVEN central surfaces is not registered with KeyguardViewMediator, but a call to enable
-        // keyguard comes in
-        mViewMediator.onSystemReady();
-        mViewMediator.setKeyguardEnabled(true);
-        TestableLooper.get(this).processAllMessages();
-
-        // If this step has been reached, then system ui has not crashed. Now register
-        // CentralSurfaces
-        assertFalse(mViewMediator.isShowingAndNotOccluded());
-        register();
-        TestableLooper.get(this).moveTimeForward(100);
-        TestableLooper.get(this).processAllMessages();
-
-        // THEN keyguard is shown
-        assertTrue(mViewMediator.isShowingAndNotOccluded());
-    }
-
-    @Test
-    @TestableLooper.RunWithLooper(setAsMainLooper = true)
     public void onLockdown_showKeyguard_evenIfKeyguardIsNotEnabledExternally() {
         // GIVEN keyguard is not enabled and isn't showing
         mViewMediator.onSystemReady();
@@ -1206,11 +1184,6 @@
     }
 
     private void createAndStartViewMediator(boolean orderUnlockAndWake) {
-        create(orderUnlockAndWake);
-        register();
-    }
-
-    private void create(boolean orderUnlockAndWake) {
         mContext.getOrCreateTestableResources().addOverride(
                 com.android.internal.R.bool.config_orderUnlockAndWake, orderUnlockAndWake);
 
@@ -1262,9 +1235,7 @@
                 mKeyguardInteractor,
                 mock(WindowManagerOcclusionManager.class));
         mViewMediator.start();
-    }
 
-    private void register() {
         mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null, null);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 24c9d6b..56e61e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -136,7 +136,6 @@
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl;
 import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl;
-import com.android.systemui.shade.transition.ShadeTransitionController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
@@ -307,7 +306,6 @@
     @Mock protected NotificationListContainer mNotificationListContainer;
     @Mock protected NotificationStackSizeCalculator mNotificationStackSizeCalculator;
     @Mock protected UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
-    @Mock protected ShadeTransitionController mShadeTransitionController;
     @Mock protected QS mQs;
     @Mock protected QSFragmentLegacy mQSFragment;
     @Mock protected ViewGroup mQsHeader;
@@ -725,7 +723,6 @@
                 mNotificationListContainer,
                 mNotificationStackSizeCalculator,
                 mUnlockedScreenOffAnimationController,
-                mShadeTransitionController,
                 systemClock,
                 mKeyguardBottomAreaViewModel,
                 mKeyguardBottomAreaInteractor,
@@ -792,10 +789,8 @@
                 mNotificationPanelViewControllerLazy,
                 mView,
                 mQsFrameTranslateController,
-                mShadeTransitionController,
                 expansionHandler,
                 mNotificationRemoteInputManager,
-                mShadeExpansionStateManager,
                 mStatusBarKeyguardViewManager,
                 mLightBarController,
                 mNotificationStackScrollLayoutController,
@@ -805,7 +800,6 @@
                 mStatusBarTouchableRegionManager,
                 mKeyguardStateController,
                 mKeyguardBypassController,
-                mUpdateMonitor,
                 mScrimController,
                 mMediaDataManager,
                 mMediaHierarchyManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
index ee279d8..20d877e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
@@ -69,7 +69,6 @@
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl;
 import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl;
-import com.android.systemui.shade.transition.ShadeTransitionController;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
@@ -134,7 +133,6 @@
     @Mock protected ViewGroup mQsHeader;
     @Mock protected ViewParent mPanelViewParent;
     @Mock protected QsFrameTranslateController mQsFrameTranslateController;
-    @Mock protected ShadeTransitionController mShadeTransitionController;
     @Mock protected PulseExpansionHandler mPulseExpansionHandler;
     @Mock protected NotificationRemoteInputManager mNotificationRemoteInputManager;
     @Mock protected StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@@ -307,10 +305,8 @@
                 mPanelViewControllerLazy,
                 mPanelView,
                 mQsFrameTranslateController,
-                mShadeTransitionController,
                 mPulseExpansionHandler,
                 mNotificationRemoteInputManager,
-                mShadeExpansionStateManager,
                 mStatusBarKeyguardViewManager,
                 mLightBarController,
                 mNotificationStackScrollLayoutController,
@@ -320,7 +316,6 @@
                 mStatusBarTouchableRegionManager,
                 mKeyguardStateController,
                 mKeyguardBypassController,
-                mKeyguardUpdateMonitor,
                 mScrimController,
                 mMediaDataManager,
                 mMediaHierarchyManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt
index a5bd2ae..dea905a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ScrimShadeTransitionControllerTest.kt
@@ -1,12 +1,41 @@
 package com.android.systemui.shade.transition
 
+import android.platform.test.annotations.DisableFlags
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.shade.STATE_OPENING
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.FakeSceneDataSource
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.fakeSceneDataSource
 import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
+import com.android.systemui.shade.domain.interactor.panelExpansionInteractor
 import com.android.systemui.statusbar.phone.ScrimController
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -21,26 +50,140 @@
     @Mock private lateinit var scrimController: ScrimController
     @Mock private lateinit var dumpManager: DumpManager
 
-    private lateinit var controller: ScrimShadeTransitionController
+    private val shadeExpansionStateManager = ShadeExpansionStateManager()
+    private val kosmos = testKosmos()
+    private lateinit var testScope: TestScope
+    private lateinit var applicationScope: CoroutineScope
+    private lateinit var panelExpansionInteractor: PanelExpansionInteractor
+    private lateinit var deviceEntryRepository: FakeDeviceEntryRepository
+    private lateinit var deviceUnlockedInteractor: DeviceUnlockedInteractor
+    private lateinit var sceneInteractor: SceneInteractor
+    private lateinit var fakeSceneDataSource: FakeSceneDataSource
+
+    private lateinit var underTest: ScrimShadeTransitionController
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         context.ensureTestableResources()
-        controller = ScrimShadeTransitionController(dumpManager, scrimController)
-
-        controller.onPanelStateChanged(STATE_OPENING)
+        testScope = kosmos.testScope
+        applicationScope = kosmos.applicationCoroutineScope
+        panelExpansionInteractor = kosmos.panelExpansionInteractor
+        deviceEntryRepository = kosmos.fakeDeviceEntryRepository
+        deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor
+        sceneInteractor = kosmos.sceneInteractor
+        fakeSceneDataSource = kosmos.fakeSceneDataSource
+        underTest = ScrimShadeTransitionController(
+            applicationScope,
+            shadeExpansionStateManager,
+            { panelExpansionInteractor },
+            dumpManager,
+            scrimController,
+            )
+        underTest.init()
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_SCENE_CONTAINER)
     fun onPanelExpansionChanged_setsFractionEqualToEventFraction() {
-        controller.onPanelExpansionChanged(EXPANSION_EVENT)
+        underTest.onPanelExpansionChanged(DEFAULT_EXPANSION_EVENT)
 
-        verify(scrimController).setRawPanelExpansionFraction(EXPANSION_EVENT.fraction)
+        verify(scrimController).setRawPanelExpansionFraction(DEFAULT_EXPANSION_EVENT.fraction)
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_SCENE_CONTAINER)
+    fun onPanelStateChanged_forwardsToScrimTransitionController() {
+        startLegacyPanelExpansion()
+
+        verify(scrimController).setRawPanelExpansionFraction(DEFAULT_EXPANSION_EVENT.fraction)
+    }
+
+    @Test
+    @EnableSceneContainer
+    fun sceneChanges_forwardsToScrimTransitionController() =
+        testScope.runTest {
+            var rawExpansion: Float? = null
+            whenever(scrimController.setRawPanelExpansionFraction(any())).then {
+                (it.arguments[0] as Float?).also { rawExp -> rawExpansion = rawExp }
+            }
+            setUnlocked(true)
+            val transitionState =
+                MutableStateFlow<ObservableTransitionState>(
+                    ObservableTransitionState.Idle(Scenes.Gone)
+                )
+            sceneInteractor.setTransitionState(transitionState)
+
+            changeScene(Scenes.Gone, transitionState)
+            val currentScene by collectLastValue(sceneInteractor.currentScene)
+            Truth.assertThat(currentScene).isEqualTo(Scenes.Gone)
+
+            Truth.assertThat(rawExpansion)
+                    .isEqualTo(0f)
+
+            changeScene(Scenes.Shade, transitionState) { progress ->
+                Truth.assertThat(rawExpansion)
+                        .isEqualTo(progress)
+            }
+        }
+
+    private fun startLegacyPanelExpansion() {
+        shadeExpansionStateManager.onPanelExpansionChanged(
+            DEFAULT_EXPANSION_EVENT.fraction,
+            DEFAULT_EXPANSION_EVENT.expanded,
+            DEFAULT_EXPANSION_EVENT.tracking,
+            DEFAULT_EXPANSION_EVENT.dragDownPxAmount,
+        )
+    }
+
+    private fun TestScope.setUnlocked(isUnlocked: Boolean) {
+        val isDeviceUnlocked by collectLastValue(deviceUnlockedInteractor.isDeviceUnlocked)
+        deviceEntryRepository.setUnlocked(isUnlocked)
+        runCurrent()
+
+        Truth.assertThat(isDeviceUnlocked).isEqualTo(isUnlocked)
+    }
+
+    private fun TestScope.changeScene(
+        toScene: SceneKey,
+        transitionState: MutableStateFlow<ObservableTransitionState>,
+        assertDuringProgress: ((progress: Float) -> Unit) = {},
+    ) {
+        val currentScene by collectLastValue(sceneInteractor.currentScene)
+        val progressFlow = MutableStateFlow(0f)
+        transitionState.value =
+            ObservableTransitionState.Transition(
+                fromScene = checkNotNull(currentScene),
+                toScene = toScene,
+                progress = progressFlow,
+                isInitiatedByUserInput = true,
+                isUserInputOngoing = flowOf(true),
+            )
+        runCurrent()
+        assertDuringProgress(progressFlow.value)
+
+        progressFlow.value = 0.2f
+        runCurrent()
+        assertDuringProgress(progressFlow.value)
+
+        progressFlow.value = 0.6f
+        runCurrent()
+        assertDuringProgress(progressFlow.value)
+
+        progressFlow.value = 1f
+        runCurrent()
+        assertDuringProgress(progressFlow.value)
+
+        transitionState.value = ObservableTransitionState.Idle(toScene)
+        fakeSceneDataSource.changeScene(toScene)
+        runCurrent()
+        assertDuringProgress(progressFlow.value)
+
+        Truth.assertThat(currentScene).isEqualTo(toScene)
     }
 
     companion object {
-        val EXPANSION_EVENT =
+        val DEFAULT_EXPANSION_EVENT =
             ShadeExpansionChangeEvent(
                 fraction = 0.5f,
                 expanded = true,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt
deleted file mode 100644
index 0a9541a..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt
+++ /dev/null
@@ -1,217 +0,0 @@
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
-package com.android.systemui.shade.transition
-
-import android.platform.test.annotations.DisableFlags
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.compose.animation.scene.SceneKey
-import com.android.systemui.Flags.FLAG_SCENE_CONTAINER
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
-import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
-import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
-import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.EnableSceneContainer
-import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor
-import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.domain.interactor.sceneInteractor
-import com.android.systemui.scene.shared.model.FakeSceneDataSource
-import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.scene.shared.model.fakeSceneDataSource
-import com.android.systemui.shade.STATE_OPENING
-import com.android.systemui.shade.ShadeExpansionChangeEvent
-import com.android.systemui.shade.ShadeExpansionStateManager
-import com.android.systemui.shade.domain.interactor.panelExpansionInteractor
-import com.android.systemui.statusbar.SysuiStatusBarStateController
-import com.android.systemui.statusbar.policy.FakeConfigurationController
-import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
-import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@RunWith(AndroidTestingRunner::class)
-@SmallTest
-class ShadeTransitionControllerTest : SysuiTestCase() {
-
-    @Mock private lateinit var scrimShadeTransitionController: ScrimShadeTransitionController
-    @Mock private lateinit var dumpManager: DumpManager
-    @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
-
-    private lateinit var controller: ShadeTransitionController
-
-    private val configurationController = FakeConfigurationController()
-    private val shadeExpansionStateManager = ShadeExpansionStateManager()
-    private val kosmos = testKosmos()
-    private lateinit var testScope: TestScope
-    private lateinit var applicationScope: CoroutineScope
-    private lateinit var panelExpansionInteractor: PanelExpansionInteractor
-    private lateinit var deviceEntryRepository: FakeDeviceEntryRepository
-    private lateinit var deviceUnlockedInteractor: DeviceUnlockedInteractor
-    private lateinit var sceneInteractor: SceneInteractor
-    private lateinit var fakeSceneDataSource: FakeSceneDataSource
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        testScope = kosmos.testScope
-        applicationScope = kosmos.applicationCoroutineScope
-        panelExpansionInteractor = kosmos.panelExpansionInteractor
-        deviceEntryRepository = kosmos.fakeDeviceEntryRepository
-        deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor
-        sceneInteractor = kosmos.sceneInteractor
-        fakeSceneDataSource = kosmos.fakeSceneDataSource
-
-        controller =
-            ShadeTransitionController(
-                applicationScope,
-                configurationController,
-                shadeExpansionStateManager,
-                dumpManager,
-                context,
-                scrimShadeTransitionController,
-                statusBarStateController,
-                ResourcesSplitShadeStateController(),
-            ) {
-                panelExpansionInteractor
-            }
-    }
-
-    @Test
-    @DisableFlags(FLAG_SCENE_CONTAINER)
-    fun onPanelStateChanged_forwardsToScrimTransitionController() {
-        startLegacyPanelExpansion()
-
-        verify(scrimShadeTransitionController).onPanelStateChanged(STATE_OPENING)
-        verify(scrimShadeTransitionController).onPanelExpansionChanged(DEFAULT_EXPANSION_EVENT)
-    }
-
-    @Test
-    @EnableSceneContainer
-    fun sceneChanges_forwardsToScrimTransitionController() =
-        testScope.runTest {
-            var latestChangeEvent: ShadeExpansionChangeEvent? = null
-            whenever(scrimShadeTransitionController.onPanelExpansionChanged(any())).thenAnswer {
-                latestChangeEvent = it.arguments[0] as ShadeExpansionChangeEvent
-                Unit
-            }
-            setUnlocked(true)
-            val transitionState =
-                MutableStateFlow<ObservableTransitionState>(
-                    ObservableTransitionState.Idle(Scenes.Gone)
-                )
-            sceneInteractor.setTransitionState(transitionState)
-
-            changeScene(Scenes.Gone, transitionState)
-            val currentScene by collectLastValue(sceneInteractor.currentScene)
-            assertThat(currentScene).isEqualTo(Scenes.Gone)
-
-            assertThat(latestChangeEvent)
-                .isEqualTo(
-                    ShadeExpansionChangeEvent(
-                        fraction = 0f,
-                        expanded = false,
-                        tracking = true,
-                        dragDownPxAmount = 0f,
-                    )
-                )
-
-            changeScene(Scenes.Shade, transitionState) { progress ->
-                assertThat(latestChangeEvent)
-                    .isEqualTo(
-                        ShadeExpansionChangeEvent(
-                            fraction = progress,
-                            expanded = progress > 0,
-                            tracking = true,
-                            dragDownPxAmount = 0f,
-                        )
-                    )
-            }
-        }
-
-    private fun startLegacyPanelExpansion() {
-        shadeExpansionStateManager.onPanelExpansionChanged(
-            DEFAULT_EXPANSION_EVENT.fraction,
-            DEFAULT_EXPANSION_EVENT.expanded,
-            DEFAULT_EXPANSION_EVENT.tracking,
-            DEFAULT_EXPANSION_EVENT.dragDownPxAmount,
-        )
-    }
-
-    private fun TestScope.setUnlocked(isUnlocked: Boolean) {
-        val isDeviceUnlocked by collectLastValue(deviceUnlockedInteractor.isDeviceUnlocked)
-        deviceEntryRepository.setUnlocked(isUnlocked)
-        runCurrent()
-
-        assertThat(isDeviceUnlocked).isEqualTo(isUnlocked)
-    }
-
-    private fun TestScope.changeScene(
-        toScene: SceneKey,
-        transitionState: MutableStateFlow<ObservableTransitionState>,
-        assertDuringProgress: ((progress: Float) -> Unit) = {},
-    ) {
-        val currentScene by collectLastValue(sceneInteractor.currentScene)
-        val progressFlow = MutableStateFlow(0f)
-        transitionState.value =
-            ObservableTransitionState.Transition(
-                fromScene = checkNotNull(currentScene),
-                toScene = toScene,
-                progress = progressFlow,
-                isInitiatedByUserInput = true,
-                isUserInputOngoing = flowOf(true),
-            )
-        runCurrent()
-        assertDuringProgress(progressFlow.value)
-
-        progressFlow.value = 0.2f
-        runCurrent()
-        assertDuringProgress(progressFlow.value)
-
-        progressFlow.value = 0.6f
-        runCurrent()
-        assertDuringProgress(progressFlow.value)
-
-        progressFlow.value = 1f
-        runCurrent()
-        assertDuringProgress(progressFlow.value)
-
-        transitionState.value = ObservableTransitionState.Idle(toScene)
-        fakeSceneDataSource.changeScene(toScene)
-        runCurrent()
-        assertDuringProgress(progressFlow.value)
-
-        assertThat(currentScene).isEqualTo(toScene)
-    }
-
-    companion object {
-        private const val DEFAULT_DRAG_DOWN_AMOUNT = 123f
-        private val DEFAULT_EXPANSION_EVENT =
-            ShadeExpansionChangeEvent(
-                fraction = 0.5f,
-                expanded = true,
-                tracking = true,
-                dragDownPxAmount = DEFAULT_DRAG_DOWN_AMOUNT
-            )
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
index 85b8b03..d3df48e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
@@ -98,6 +98,7 @@
         `when`(rebuilder.rebuildForCanceledSmartReplies(any())).thenReturn(sbn)
         `when`(rebuilder.rebuildForRemoteInputReply(any())).thenReturn(sbn)
         `when`(rebuilder.rebuildForSendingSmartReply(any(), any())).thenReturn(sbn)
+        `when`(rebuilder.rebuildWithExistingReplies(any())).thenReturn(sbn)
     }
 
     val remoteInputActiveExtender get() = coordinator.mRemoteInputActiveExtender
@@ -208,13 +209,30 @@
             it.onEntryUpdated(entry, true)
         }
 
-
         verify(rebuilder, times(1)).rebuildForCanceledSmartReplies(entry)
         verify(smartReplyController, times(1)).stopSending(entry)
     }
 
     @Test
     @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
+    fun testRepeatedUpdateTriggersRebuild() {
+        // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
+        val entry = NotificationEntryBuilder()
+                .setId(3)
+                .setTag("entry")
+                .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true)
+                .build()
+        `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false)
+        `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false)
+        collectionListeners.forEach {
+            it.onEntryUpdated(entry, true)
+        }
+
+        verify(rebuilder, times(1)).rebuildWithExistingReplies(entry)
+    }
+
+    @Test
+    @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
     fun testLifetimeExtensionListenerClearsRemoteInputs() {
         // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
         val entry = NotificationEntryBuilder()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
index 018a571..b161f84 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
@@ -16,7 +16,11 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator
 
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
 import android.os.UserHandle
+import android.platform.test.annotations.DisableFlags
 import android.platform.test.annotations.EnableFlags
 import android.service.notification.StatusBarNotification
 import androidx.test.filters.SmallTest
@@ -25,14 +29,17 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.RankingBuilder
 import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.notification.DynamicPrivacyController
 import com.android.systemui.statusbar.notification.collection.ListEntry
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
 import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -44,6 +51,8 @@
 import com.android.systemui.util.mockito.withArgCaptor
 import dagger.BindsInstance
 import dagger.Component
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Test
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
@@ -114,6 +123,49 @@
     }
 
     @Test
+    @DisableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    fun screenshareSecretFilter_flagDisabled_filterNoAdded() {
+        coordinator.attach(pipeline)
+
+        verify(pipeline, never()).addFinalizeFilter(any(NotifFilter::class.java))
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    fun screenshareSecretFilter_sensitiveInctive_noFiltersSecret() {
+        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive)
+            .thenReturn(false)
+
+        coordinator.attach(pipeline)
+        val filter = withArgCaptor<NotifFilter> { verify(pipeline).addFinalizeFilter(capture()) }
+
+        val defaultNotification = createNotificationEntry("test", false, false)
+        val notificationWithSecretVisibility = createNotificationEntry("test", true, false)
+        val notificationOnSecretChannel = createNotificationEntry("test", false, true)
+
+        assertFalse(filter.shouldFilterOut(defaultNotification, 0))
+        assertFalse(filter.shouldFilterOut(notificationWithSecretVisibility, 0))
+        assertFalse(filter.shouldFilterOut(notificationOnSecretChannel, 0))
+    }
+
+    @Test
+    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+    fun screenshareSecretFilter_sensitiveActive_filtersSecret() {
+        whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+
+        coordinator.attach(pipeline)
+        val filter = withArgCaptor<NotifFilter> { verify(pipeline).addFinalizeFilter(capture()) }
+
+        val defaultNotification = createNotificationEntry("test", false, false)
+        val notificationWithSecretVisibility = createNotificationEntry("test", true, false)
+        val notificationOnSecretChannel = createNotificationEntry("test", false, true)
+
+        assertFalse(filter.shouldFilterOut(defaultNotification, 0))
+        assertTrue(filter.shouldFilterOut(notificationWithSecretVisibility, 0))
+        assertTrue(filter.shouldFilterOut(notificationOnSecretChannel, 0))
+    }
+
+    @Test
     fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction() {
         coordinator.attach(pipeline)
         val onBeforeRenderListListener =
@@ -638,6 +690,32 @@
             override fun getRepresentativeEntry(): NotificationEntry = mockEntry
         }
     }
+
+    private fun createNotificationEntry(
+        packageName: String,
+        secretVisibility: Boolean = false,
+        secretChannelVisibility: Boolean = false,
+    ): NotificationEntry {
+        val notification = Notification()
+        if (secretVisibility) {
+            // Developer has marked notification as public
+            notification.visibility = Notification.VISIBILITY_SECRET
+        }
+        val notificationEntry =
+            NotificationEntryBuilder().setNotification(notification).setPkg(packageName).build()
+        val channel = NotificationChannel("1", "1", NotificationManager.IMPORTANCE_HIGH)
+        if (secretChannelVisibility) {
+            // User doesn't allow notifications at the channel level
+            channel.lockscreenVisibility = Notification.VISIBILITY_SECRET
+        }
+        notificationEntry.setRanking(
+            RankingBuilder(notificationEntry.ranking)
+                .setChannel(channel)
+                .setVisibilityOverride(NotificationManager.VISIBILITY_NO_OVERRIDE)
+                .build()
+        )
+        return notificationEntry
+    }
 }
 
 @CoordinatorScope
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
index 66a306e..24f6708 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
@@ -327,6 +327,13 @@
     }
 
     @Test
+    fun testShouldNotPeek_appSuspended() {
+        ensurePeekState()
+        assertShouldNotBubble(buildPeekEntry { packageSuspended = true })
+        assertNoEventsLogged()
+    }
+
+    @Test
     fun testShouldNotPeek_hiddenOnKeyguard() {
         ensurePeekState({ keyguardShouldHideNotification = true })
         assertShouldNotHeadsUp(buildPeekEntry())
@@ -412,6 +419,13 @@
     }
 
     @Test
+    fun testShouldNotPulse_appSuspended() {
+        ensurePulseState()
+        assertShouldNotHeadsUp(buildPulseEntry { packageSuspended = true })
+        assertNoEventsLogged()
+    }
+
+    @Test
     fun testShouldNotPulse_hiddenOnKeyguard() {
         ensurePulseState({ keyguardShouldHideNotification = true })
         assertShouldNotHeadsUp(buildPulseEntry())
@@ -595,16 +609,16 @@
     }
 
     @Test
-    fun testShouldNotBubble_hiddenOnKeyguard() {
-        ensureBubbleState({ keyguardShouldHideNotification = true })
-        assertShouldNotBubble(buildBubbleEntry())
+    fun testShouldNotBubble_appSuspended() {
+        ensureBubbleState()
+        assertShouldNotBubble(buildBubbleEntry { packageSuspended = true })
         assertNoEventsLogged()
     }
 
     @Test
-    fun testShouldNotBubble_bubbleAppSuspended() {
-        ensureBubbleState()
-        assertShouldNotBubble(buildBubbleEntry { packageSuspended = true })
+    fun testShouldNotBubble_hiddenOnKeyguard() {
+        ensureBubbleState({ keyguardShouldHideNotification = true })
+        assertShouldNotBubble(buildBubbleEntry())
         assertNoEventsLogged()
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/startable/ShadeStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/startable/ShadeStartableKosmos.kt
index bbdb872..65e04f4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/startable/ShadeStartableKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/startable/ShadeStartableKosmos.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.shade.ShadeHeaderController
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.shade.shadeController
+import com.android.systemui.shade.transition.ScrimShadeTransitionController
 import com.android.systemui.statusbar.policy.splitShadeStateController
 import com.android.systemui.util.mockito.mock
 
@@ -37,6 +38,7 @@
         shadeRepository = shadeRepository,
         controller = splitShadeStateController,
         shadeHeaderController = mock<ShadeHeaderController>(),
+        scrimShadeTransitionController = mock<ScrimShadeTransitionController>(),
         shadeController = shadeController
     )
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 21cc8da..9403796 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -3550,7 +3550,7 @@
                     && userState.isMagnificationTwoFingerTripleTapEnabledLocked()));
 
         final boolean createConnectionForCurrentCapability =
-                com.android.window.flags.Flags.magnificationAlwaysDrawFullscreenBorder()
+                com.android.window.flags.Flags.alwaysDrawMagnificationFullscreenBorder()
                         || (userState.getMagnificationCapabilitiesLocked()
                                 != Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN);
 
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
index 0d5fd14..20bec59 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java
@@ -587,7 +587,7 @@
 
     @Override
     public void onFullScreenMagnificationActivationState(int displayId, boolean activated) {
-        if (Flags.magnificationAlwaysDrawFullscreenBorder()) {
+        if (Flags.alwaysDrawMagnificationFullscreenBorder()) {
             getMagnificationConnectionManager()
                     .onFullscreenMagnificationActivationChanged(displayId, activated);
         }
diff --git a/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java
index 5e52e06..82e9a26 100644
--- a/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java
+++ b/services/companion/java/com/android/server/companion/BackupRestoreProcessor.java
@@ -111,6 +111,11 @@
         Slog.i(TAG, "applyRestoredPayload() userId=[" + userId + "], payload size=["
                 + payload.length + "].");
 
+        if (payload.length == 0) {
+            Slog.i(TAG, "CDM backup payload was empty.");
+            return;
+        }
+
         ByteBuffer buffer = ByteBuffer.wrap(payload);
 
         // Make sure that payload version matches current version to ensure proper deserialization
@@ -120,15 +125,26 @@
             return;
         }
 
-        // Read the bytes containing backed-up associations
-        byte[] associationsPayload = new byte[buffer.getInt()];
-        buffer.get(associationsPayload);
+        // Pre-load the bytes into memory before processing them to ensure payload mal-formatting
+        // error is caught early on.
+        final byte[] associationsPayload;
+        final byte[] requestsPayload;
+        try {
+            // Read the bytes containing backed-up associations
+            associationsPayload = new byte[buffer.getInt()];
+            buffer.get(associationsPayload);
+
+            // Read the bytes containing backed-up system data transfer requests user consent
+            requestsPayload = new byte[buffer.getInt()];
+            buffer.get(requestsPayload);
+        } catch (Exception bufferException) {
+            Slog.e(TAG, "CDM backup payload was mal-formatted.", bufferException);
+            return;
+        }
+
         final Associations restoredAssociations = readAssociationsFromPayload(
                 associationsPayload, userId);
 
-        // Read the bytes containing backed-up system data transfer requests user consent
-        byte[] requestsPayload = new byte[buffer.getInt()];
-        buffer.get(requestsPayload);
         List<SystemDataTransferRequest> restoredRequestsForUser =
                 mSystemDataTransferRequestStore.readRequestsFromPayload(requestsPayload, userId);
 
diff --git a/services/companion/java/com/android/server/companion/CompanionApplicationController.java b/services/companion/java/com/android/server/companion/CompanionApplicationController.java
deleted file mode 100644
index 0a41485..0000000
--- a/services/companion/java/com/android/server/companion/CompanionApplicationController.java
+++ /dev/null
@@ -1,567 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.companion;
-
-import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.SuppressLint;
-import android.annotation.UserIdInt;
-import android.companion.AssociationInfo;
-import android.companion.CompanionDeviceService;
-import android.companion.DevicePresenceEvent;
-import android.content.ComponentName;
-import android.content.Context;
-import android.hardware.power.Mode;
-import android.os.Handler;
-import android.os.ParcelUuid;
-import android.os.PowerManagerInternal;
-import android.util.Log;
-import android.util.Slog;
-import android.util.SparseArray;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.infra.PerUser;
-import com.android.server.companion.association.AssociationStore;
-import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
-import com.android.server.companion.presence.ObservableUuid;
-import com.android.server.companion.presence.ObservableUuidStore;
-import com.android.server.companion.utils.PackageUtils;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Manages communication with companion applications via
- * {@link android.companion.ICompanionDeviceService} interface, including "connecting" (binding) to
- * the services, maintaining the connection (the binding), and invoking callback methods such as
- * {@link CompanionDeviceService#onDeviceAppeared(AssociationInfo)},
- * {@link CompanionDeviceService#onDeviceDisappeared(AssociationInfo)} and
- * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} in the
- * application process.
- *
- * <p>
- * The following is the list of the APIs provided by {@link CompanionApplicationController} (to be
- * utilized by {@link CompanionDeviceManagerService}):
- * <ul>
- * <li> {@link #bindCompanionApplication(int, String, boolean)}
- * <li> {@link #unbindCompanionApplication(int, String)}
- * <li> {@link #notifyCompanionDevicePresenceEvent(AssociationInfo, int)}
- * <li> {@link #isCompanionApplicationBound(int, String)}
- * <li> {@link #isRebindingCompanionApplicationScheduled(int, String)}
- * </ul>
- *
- * @see CompanionDeviceService
- * @see android.companion.ICompanionDeviceService
- * @see CompanionDeviceServiceConnector
- */
-@SuppressLint("LongLogTag")
-public class CompanionApplicationController {
-    static final boolean DEBUG = false;
-    private static final String TAG = "CDM_CompanionApplicationController";
-
-    private static final long REBIND_TIMEOUT = 10 * 1000; // 10 sec
-
-    private final @NonNull Context mContext;
-    private final @NonNull AssociationStore mAssociationStore;
-    private final @NonNull ObservableUuidStore mObservableUuidStore;
-    private final @NonNull CompanionDevicePresenceMonitor mDevicePresenceMonitor;
-    private final @NonNull CompanionServicesRegister mCompanionServicesRegister;
-
-    private final PowerManagerInternal mPowerManagerInternal;
-
-    @GuardedBy("mBoundCompanionApplications")
-    private final @NonNull AndroidPackageMap<List<CompanionDeviceServiceConnector>>
-            mBoundCompanionApplications;
-    @GuardedBy("mScheduledForRebindingCompanionApplications")
-    private final @NonNull AndroidPackageMap<Boolean> mScheduledForRebindingCompanionApplications;
-
-    CompanionApplicationController(Context context, AssociationStore associationStore,
-            ObservableUuidStore observableUuidStore,
-            CompanionDevicePresenceMonitor companionDevicePresenceMonitor,
-            PowerManagerInternal powerManagerInternal) {
-        mContext = context;
-        mAssociationStore = associationStore;
-        mObservableUuidStore =  observableUuidStore;
-        mDevicePresenceMonitor = companionDevicePresenceMonitor;
-        mPowerManagerInternal = powerManagerInternal;
-        mCompanionServicesRegister = new CompanionServicesRegister();
-        mBoundCompanionApplications = new AndroidPackageMap<>();
-        mScheduledForRebindingCompanionApplications = new AndroidPackageMap<>();
-    }
-
-    void onPackagesChanged(@UserIdInt int userId) {
-        mCompanionServicesRegister.invalidate(userId);
-    }
-
-    /**
-     * CDM binds to the companion app.
-     */
-    public void bindCompanionApplication(@UserIdInt int userId, @NonNull String packageName,
-            boolean isSelfManaged) {
-        if (DEBUG) {
-            Log.i(TAG, "bind() u" + userId + "/" + packageName
-                    + " isSelfManaged=" + isSelfManaged);
-        }
-
-        final List<ComponentName> companionServices =
-                mCompanionServicesRegister.forPackage(userId, packageName);
-        if (companionServices.isEmpty()) {
-            Slog.w(TAG, "Can not bind companion applications u" + userId + "/" + packageName + ": "
-                    + "eligible CompanionDeviceService not found.\n"
-                    + "A CompanionDeviceService should declare an intent-filter for "
-                    + "\"android.companion.CompanionDeviceService\" action and require "
-                    + "\"android.permission.BIND_COMPANION_DEVICE_SERVICE\" permission.");
-            return;
-        }
-
-        final List<CompanionDeviceServiceConnector> serviceConnectors = new ArrayList<>();
-        synchronized (mBoundCompanionApplications) {
-            if (mBoundCompanionApplications.containsValueForPackage(userId, packageName)) {
-                if (DEBUG) Log.e(TAG, "u" + userId + "/" + packageName + " is ALREADY bound.");
-                return;
-            }
-
-            for (int i = 0; i < companionServices.size(); i++) {
-                boolean isPrimary = i == 0;
-                serviceConnectors.add(CompanionDeviceServiceConnector.newInstance(mContext, userId,
-                        companionServices.get(i), isSelfManaged, isPrimary));
-            }
-
-            mBoundCompanionApplications.setValueForPackage(userId, packageName, serviceConnectors);
-        }
-
-        // Set listeners for both Primary and Secondary connectors.
-        for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) {
-            serviceConnector.setListener(this::onBinderDied);
-        }
-
-        // Now "bind" all the connectors: the primary one and the rest of them.
-        for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) {
-            serviceConnector.connect();
-        }
-    }
-
-    /**
-     * CDM unbinds the companion app.
-     */
-    public void unbindCompanionApplication(@UserIdInt int userId, @NonNull String packageName) {
-        if (DEBUG) Log.i(TAG, "unbind() u" + userId + "/" + packageName);
-
-        final List<CompanionDeviceServiceConnector> serviceConnectors;
-
-        synchronized (mBoundCompanionApplications) {
-            serviceConnectors = mBoundCompanionApplications.removePackage(userId, packageName);
-        }
-
-        synchronized (mScheduledForRebindingCompanionApplications) {
-            mScheduledForRebindingCompanionApplications.removePackage(userId, packageName);
-        }
-
-        if (serviceConnectors == null) {
-            if (DEBUG) {
-                Log.e(TAG, "unbindCompanionApplication(): "
-                        + "u" + userId + "/" + packageName + " is NOT bound");
-                Log.d(TAG, "Stacktrace", new Throwable());
-            }
-            return;
-        }
-
-        for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) {
-            serviceConnector.postUnbind();
-        }
-    }
-
-    /**
-     * @return whether the companion application is bound now.
-     */
-    public boolean isCompanionApplicationBound(@UserIdInt int userId, @NonNull String packageName) {
-        synchronized (mBoundCompanionApplications) {
-            return mBoundCompanionApplications.containsValueForPackage(userId, packageName);
-        }
-    }
-
-    private void scheduleRebinding(@UserIdInt int userId, @NonNull String packageName,
-            CompanionDeviceServiceConnector serviceConnector) {
-        Slog.i(TAG, "scheduleRebinding() " + userId + "/" + packageName);
-
-        if (isRebindingCompanionApplicationScheduled(userId, packageName)) {
-            if (DEBUG) {
-                Log.i(TAG, "CompanionApplication rebinding has been scheduled, skipping "
-                        + serviceConnector.getComponentName());
-            }
-            return;
-        }
-
-        if (serviceConnector.isPrimary()) {
-            synchronized (mScheduledForRebindingCompanionApplications) {
-                mScheduledForRebindingCompanionApplications.setValueForPackage(
-                        userId, packageName, true);
-            }
-        }
-
-        // Rebinding in 10 seconds.
-        Handler.getMain().postDelayed(() ->
-                onRebindingCompanionApplicationTimeout(userId, packageName, serviceConnector),
-                REBIND_TIMEOUT);
-    }
-
-    private boolean isRebindingCompanionApplicationScheduled(
-            @UserIdInt int userId, @NonNull String packageName) {
-        synchronized (mScheduledForRebindingCompanionApplications) {
-            return mScheduledForRebindingCompanionApplications.containsValueForPackage(
-                    userId, packageName);
-        }
-    }
-
-    private void onRebindingCompanionApplicationTimeout(
-            @UserIdInt int userId, @NonNull String packageName,
-            @NonNull CompanionDeviceServiceConnector serviceConnector) {
-        // Re-mark the application is bound.
-        if (serviceConnector.isPrimary()) {
-            synchronized (mBoundCompanionApplications) {
-                if (!mBoundCompanionApplications.containsValueForPackage(userId, packageName)) {
-                    List<CompanionDeviceServiceConnector> serviceConnectors =
-                            Collections.singletonList(serviceConnector);
-                    mBoundCompanionApplications.setValueForPackage(userId, packageName,
-                            serviceConnectors);
-                }
-            }
-
-            synchronized (mScheduledForRebindingCompanionApplications) {
-                mScheduledForRebindingCompanionApplications.removePackage(userId, packageName);
-            }
-        }
-
-        serviceConnector.connect();
-    }
-
-    /**
-     * Notify the app that the device appeared.
-     *
-     * @deprecated use {@link #notifyCompanionDevicePresenceEvent(AssociationInfo, int)} instead
-     */
-    @Deprecated
-    public void notifyCompanionApplicationDeviceAppeared(AssociationInfo association) {
-        final int userId = association.getUserId();
-        final String packageName = association.getPackageName();
-
-        Slog.i(TAG, "notifyDevice_Appeared() id=" + association.getId() + " u" + userId
-                    + "/" + packageName);
-
-        final CompanionDeviceServiceConnector primaryServiceConnector =
-                getPrimaryServiceConnector(userId, packageName);
-        if (primaryServiceConnector == null) {
-            Slog.e(TAG, "notify_CompanionApplicationDevice_Appeared(): "
-                        + "u" + userId + "/" + packageName + " is NOT bound.");
-            Slog.e(TAG, "Stacktrace", new Throwable());
-            return;
-        }
-
-        Log.i(TAG, "Calling onDeviceAppeared to userId=[" + userId + "] package=["
-                + packageName + "] associationId=[" + association.getId() + "]");
-
-        primaryServiceConnector.postOnDeviceAppeared(association);
-    }
-
-    /**
-     * Notify the app that the device disappeared.
-     *
-     * @deprecated use {@link #notifyCompanionDevicePresenceEvent(AssociationInfo, int)} instead
-     */
-    @Deprecated
-    public void notifyCompanionApplicationDeviceDisappeared(AssociationInfo association) {
-        final int userId = association.getUserId();
-        final String packageName = association.getPackageName();
-
-        Slog.i(TAG, "notifyDevice_Disappeared() id=" + association.getId() + " u" + userId
-                + "/" + packageName);
-
-        final CompanionDeviceServiceConnector primaryServiceConnector =
-                getPrimaryServiceConnector(userId, packageName);
-        if (primaryServiceConnector == null) {
-            Slog.e(TAG, "notify_CompanionApplicationDevice_Disappeared(): "
-                        + "u" + userId + "/" + packageName + " is NOT bound.");
-            Slog.e(TAG, "Stacktrace", new Throwable());
-            return;
-        }
-
-        Log.i(TAG, "Calling onDeviceDisappeared to userId=[" + userId + "] package=["
-                + packageName + "] associationId=[" + association.getId() + "]");
-
-        primaryServiceConnector.postOnDeviceDisappeared(association);
-    }
-
-    /**
-     * Notify the app that the device appeared.
-     */
-    public void notifyCompanionDevicePresenceEvent(AssociationInfo association, int event) {
-        final int userId = association.getUserId();
-        final String packageName = association.getPackageName();
-        final CompanionDeviceServiceConnector primaryServiceConnector =
-                getPrimaryServiceConnector(userId, packageName);
-        final DevicePresenceEvent devicePresenceEvent =
-                new DevicePresenceEvent(association.getId(), event, null);
-
-        if (primaryServiceConnector == null) {
-            Slog.e(TAG, "notifyCompanionApplicationDevicePresenceEvent(): "
-                        + "u" + userId + "/" + packageName
-                        + " event=[ " + event  + " ] is NOT bound.");
-            Slog.e(TAG, "Stacktrace", new Throwable());
-            return;
-        }
-
-        Slog.i(TAG, "Calling onDevicePresenceEvent() to userId=[" + userId + "] package=["
-                + packageName + "] associationId=[" + association.getId()
-                + "] event=[" + event + "]");
-
-        primaryServiceConnector.postOnDevicePresenceEvent(devicePresenceEvent);
-    }
-
-    /**
-     * Notify the app that the device disappeared.
-     */
-    public void notifyUuidDevicePresenceEvent(ObservableUuid uuid, int event) {
-        final int userId = uuid.getUserId();
-        final ParcelUuid parcelUuid = uuid.getUuid();
-        final String packageName = uuid.getPackageName();
-        final CompanionDeviceServiceConnector primaryServiceConnector =
-                getPrimaryServiceConnector(userId, packageName);
-        final DevicePresenceEvent devicePresenceEvent =
-                new DevicePresenceEvent(DevicePresenceEvent.NO_ASSOCIATION, event, parcelUuid);
-
-        if (primaryServiceConnector == null) {
-            Slog.e(TAG, "notifyApplicationDevicePresenceChanged(): "
-                    + "u" + userId + "/" + packageName
-                    + " event=[ " + event  + " ] is NOT bound.");
-            Slog.e(TAG, "Stacktrace", new Throwable());
-            return;
-        }
-
-        Slog.i(TAG, "Calling onDevicePresenceEvent() to userId=[" + userId + "] package=["
-                + packageName + "]" + "event= [" + event + "]");
-
-        primaryServiceConnector.postOnDevicePresenceEvent(devicePresenceEvent);
-    }
-
-    void dump(@NonNull PrintWriter out) {
-        out.append("Companion Device Application Controller: \n");
-
-        synchronized (mBoundCompanionApplications) {
-            out.append("  Bound Companion Applications: ");
-            if (mBoundCompanionApplications.size() == 0) {
-                out.append("<empty>\n");
-            } else {
-                out.append("\n");
-                mBoundCompanionApplications.dump(out);
-            }
-        }
-
-        out.append("  Companion Applications Scheduled For Rebinding: ");
-        if (mScheduledForRebindingCompanionApplications.size() == 0) {
-            out.append("<empty>\n");
-        } else {
-            out.append("\n");
-            mScheduledForRebindingCompanionApplications.dump(out);
-        }
-    }
-
-    /**
-     * Rebinding for Self-Managed secondary services OR Non-Self-Managed services.
-     */
-    private void onBinderDied(@UserIdInt int userId, @NonNull String packageName,
-            @NonNull CompanionDeviceServiceConnector serviceConnector) {
-
-        boolean isPrimary = serviceConnector.isPrimary();
-        Slog.i(TAG, "onBinderDied() u" + userId + "/" + packageName + " isPrimary: " + isPrimary);
-
-        // First, disable hint mode for Auto profile and mark not BOUND for primary service ONLY.
-        if (isPrimary) {
-            final List<AssociationInfo> associations =
-                    mAssociationStore.getActiveAssociationsByPackage(userId, packageName);
-
-            for (AssociationInfo association : associations) {
-                final String deviceProfile = association.getDeviceProfile();
-                if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) {
-                    Slog.i(TAG, "Disable hint mode for device profile: " + deviceProfile);
-                    mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false);
-                    break;
-                }
-            }
-
-            synchronized (mBoundCompanionApplications) {
-                mBoundCompanionApplications.removePackage(userId, packageName);
-            }
-        }
-
-        // Second: schedule rebinding if needed.
-        final boolean shouldScheduleRebind = shouldScheduleRebind(userId, packageName, isPrimary);
-
-        if (shouldScheduleRebind) {
-            scheduleRebinding(userId, packageName, serviceConnector);
-        }
-    }
-
-    private @Nullable CompanionDeviceServiceConnector getPrimaryServiceConnector(
-            @UserIdInt int userId, @NonNull String packageName) {
-        final List<CompanionDeviceServiceConnector> connectors;
-        synchronized (mBoundCompanionApplications) {
-            connectors = mBoundCompanionApplications.getValueForPackage(userId, packageName);
-        }
-        return connectors != null ? connectors.get(0) : null;
-    }
-
-    private boolean shouldScheduleRebind(int userId, String packageName, boolean isPrimary) {
-        // Make sure do not schedule rebind for the case ServiceConnector still gets callback after
-        // app is uninstalled.
-        boolean stillAssociated = false;
-        // Make sure to clean up the state for all the associations
-        // that associate with this package.
-        boolean shouldScheduleRebind = false;
-        boolean shouldScheduleRebindForUuid = false;
-        final List<ObservableUuid> uuids =
-                mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
-
-        for (AssociationInfo ai :
-                mAssociationStore.getActiveAssociationsByPackage(userId, packageName)) {
-            final int associationId = ai.getId();
-            stillAssociated = true;
-            if (ai.isSelfManaged()) {
-                // Do not rebind if primary one is died for selfManaged application.
-                if (isPrimary
-                        && mDevicePresenceMonitor.isDevicePresent(associationId)) {
-                    mDevicePresenceMonitor.onSelfManagedDeviceReporterBinderDied(associationId);
-                    shouldScheduleRebind = false;
-                }
-                // Do not rebind if both primary and secondary services are died for
-                // selfManaged application.
-                shouldScheduleRebind = isCompanionApplicationBound(userId, packageName);
-            } else if (ai.isNotifyOnDeviceNearby()) {
-                // Always rebind for non-selfManaged devices.
-                shouldScheduleRebind = true;
-            }
-        }
-
-        for (ObservableUuid uuid : uuids) {
-            if (mDevicePresenceMonitor.isDeviceUuidPresent(uuid.getUuid())) {
-                shouldScheduleRebindForUuid = true;
-                break;
-            }
-        }
-
-        return (stillAssociated && shouldScheduleRebind) || shouldScheduleRebindForUuid;
-    }
-
-    private class CompanionServicesRegister extends PerUser<Map<String, List<ComponentName>>> {
-        @Override
-        public synchronized @NonNull Map<String, List<ComponentName>> forUser(
-                @UserIdInt int userId) {
-            return super.forUser(userId);
-        }
-
-        synchronized @NonNull List<ComponentName> forPackage(
-                @UserIdInt int userId, @NonNull String packageName) {
-            return forUser(userId).getOrDefault(packageName, Collections.emptyList());
-        }
-
-        synchronized void invalidate(@UserIdInt int userId) {
-            remove(userId);
-        }
-
-        @Override
-        protected final @NonNull Map<String, List<ComponentName>> create(@UserIdInt int userId) {
-            return PackageUtils.getCompanionServicesForUser(mContext, userId);
-        }
-    }
-
-    /**
-     * Associates an Android package (defined by userId + packageName) with a value of type T.
-     */
-    private static class AndroidPackageMap<T> extends SparseArray<Map<String, T>> {
-
-        void setValueForPackage(
-                @UserIdInt int userId, @NonNull String packageName, @NonNull T value) {
-            Map<String, T> forUser = get(userId);
-            if (forUser == null) {
-                forUser = /* Map<String, T> */ new HashMap();
-                put(userId, forUser);
-            }
-
-            forUser.put(packageName, value);
-        }
-
-        boolean containsValueForPackage(@UserIdInt int userId, @NonNull String packageName) {
-            final Map<String, ?> forUser = get(userId);
-            return forUser != null && forUser.containsKey(packageName);
-        }
-
-        T getValueForPackage(@UserIdInt int userId, @NonNull String packageName) {
-            final Map<String, T> forUser = get(userId);
-            return forUser != null ? forUser.get(packageName) : null;
-        }
-
-        T removePackage(@UserIdInt int userId, @NonNull String packageName) {
-            final Map<String, T> forUser = get(userId);
-            if (forUser == null) return null;
-            return forUser.remove(packageName);
-        }
-
-        void dump() {
-            if (size() == 0) {
-                Log.d(TAG, "<empty>");
-                return;
-            }
-
-            for (int i = 0; i < size(); i++) {
-                final int userId = keyAt(i);
-                final Map<String, T> forUser = get(userId);
-                if (forUser.isEmpty()) {
-                    Log.d(TAG, "u" + userId + ": <empty>");
-                }
-
-                for (Map.Entry<String, T> packageValue : forUser.entrySet()) {
-                    final String packageName = packageValue.getKey();
-                    final T value = packageValue.getValue();
-                    Log.d(TAG, "u" + userId + "\\" + packageName + " -> " + value);
-                }
-            }
-        }
-
-        private void dump(@NonNull PrintWriter out) {
-            for (int i = 0; i < size(); i++) {
-                final int userId = keyAt(i);
-                final Map<String, T> forUser = get(userId);
-                if (forUser.isEmpty()) {
-                    out.append("    u").append(String.valueOf(userId)).append(": <empty>\n");
-                }
-
-                for (Map.Entry<String, T> packageValue : forUser.entrySet()) {
-                    final String packageName = packageValue.getKey();
-                    final T value = packageValue.getValue();
-                    out.append("    u").append(String.valueOf(userId)).append("\\")
-                            .append(packageName).append(" -> ")
-                            .append(value.toString()).append('\n');
-                }
-            }
-        }
-    }
-}
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 712162b..edf9fd1 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -20,15 +20,10 @@
 import static android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES;
 import static android.Manifest.permission.DELIVER_COMPANION_MESSAGES;
 import static android.Manifest.permission.MANAGE_COMPANION_DEVICES;
+import static android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED;
 import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE;
 import static android.Manifest.permission.USE_COMPANION_TRANSPORTS;
-import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
-import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED;
-import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED;
 import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED;
-import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED;
-import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED;
-import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED;
 import static android.content.pm.PackageManager.CERT_INPUT_SHA256;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Process.SYSTEM_UID;
@@ -42,13 +37,10 @@
 import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed;
 import static com.android.server.companion.utils.PermissionsUtils.checkCallerCanManageCompanionDevice;
 import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage;
-import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanObservingDevicePresenceByUuid;
 import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOr;
 import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId;
-import static com.android.server.companion.utils.PermissionsUtils.sanitizeWithCallerChecks;
 
 import static java.util.Objects.requireNonNull;
-import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.MINUTES;
 
 import android.annotation.EnforcePermission;
@@ -64,7 +56,6 @@
 import android.bluetooth.BluetoothDevice;
 import android.companion.AssociationInfo;
 import android.companion.AssociationRequest;
-import android.companion.DeviceNotAssociatedException;
 import android.companion.IAssociationRequestCallback;
 import android.companion.ICompanionDeviceManager;
 import android.companion.IOnAssociationsChangedListener;
@@ -79,7 +70,6 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
-import android.hardware.power.Mode;
 import android.net.MacAddress;
 import android.net.NetworkPolicyManager;
 import android.os.Binder;
@@ -91,7 +81,6 @@
 import android.os.PowerManagerInternal;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.ArraySet;
@@ -118,7 +107,8 @@
 import com.android.server.companion.datatransfer.contextsync.CrossDeviceCall;
 import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController;
 import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncControllerCallback;
-import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
+import com.android.server.companion.presence.CompanionAppBinder;
+import com.android.server.companion.presence.DevicePresenceProcessor;
 import com.android.server.companion.presence.ObservableUuid;
 import com.android.server.companion.presence.ObservableUuidStore;
 import com.android.server.companion.transport.CompanionTransportManager;
@@ -131,10 +121,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 @SuppressLint("LongLogTag")
@@ -146,10 +133,6 @@
 
     private static final String PREF_FILE_NAME = "companion_device_preferences.xml";
     private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done";
-    private static final String SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW =
-            "debug.cdm.cdmservice.removal_time_window";
-
-    private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90);
     private static final int MAX_CN_LENGTH = 500;
 
     private final ActivityTaskManagerInternal mAtmInternal;
@@ -165,8 +148,8 @@
     private final AssociationRequestsProcessor mAssociationRequestsProcessor;
     private final SystemDataTransferProcessor mSystemDataTransferProcessor;
     private final BackupRestoreProcessor mBackupRestoreProcessor;
-    private final CompanionDevicePresenceMonitor mDevicePresenceMonitor;
-    private final CompanionApplicationController mCompanionAppController;
+    private final DevicePresenceProcessor mDevicePresenceProcessor;
+    private final CompanionAppBinder mCompanionAppBinder;
     private final CompanionTransportManager mTransportManager;
     private final DisassociationProcessor mDisassociationProcessor;
     private final CrossDeviceSyncController mCrossDeviceSyncController;
@@ -185,7 +168,7 @@
         mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
 
         final AssociationDiskStore associationDiskStore = new AssociationDiskStore();
-        mAssociationStore = new AssociationStore(userManager, associationDiskStore);
+        mAssociationStore = new AssociationStore(context, userManager, associationDiskStore);
         mSystemDataTransferRequestStore = new SystemDataTransferRequestStore();
         mObservableUuidStore = new ObservableUuidStore();
 
@@ -196,18 +179,17 @@
                 mAssociationStore, associationDiskStore, mSystemDataTransferRequestStore,
                 mAssociationRequestsProcessor);
 
-        mDevicePresenceMonitor = new CompanionDevicePresenceMonitor(userManager,
-                mAssociationStore, mObservableUuidStore, mDevicePresenceCallback);
+        mCompanionAppBinder = new CompanionAppBinder(context);
 
-        mCompanionAppController = new CompanionApplicationController(
-                context, mAssociationStore, mObservableUuidStore, mDevicePresenceMonitor,
+        mDevicePresenceProcessor = new DevicePresenceProcessor(context,
+                mCompanionAppBinder, userManager, mAssociationStore, mObservableUuidStore,
                 mPowerManagerInternal);
 
         mTransportManager = new CompanionTransportManager(context, mAssociationStore);
 
         mDisassociationProcessor = new DisassociationProcessor(context, activityManager,
-                mAssociationStore, mPackageManagerInternal, mDevicePresenceMonitor,
-                mCompanionAppController, mSystemDataTransferRequestStore, mTransportManager);
+                mAssociationStore, mPackageManagerInternal, mDevicePresenceProcessor,
+                mCompanionAppBinder, mSystemDataTransferRequestStore, mTransportManager);
 
         mSystemDataTransferProcessor = new SystemDataTransferProcessor(this,
                 mPackageManagerInternal, mAssociationStore,
@@ -242,7 +224,7 @@
             // delays (even in case of the Main Thread). It may be fine overall, but would require
             // updating the tests (adding a delay there).
             mPackageMonitor.register(context, FgThread.get().getLooper(), UserHandle.ALL, true);
-            mDevicePresenceMonitor.init(context);
+            mDevicePresenceProcessor.init(context);
         } else if (phase == PHASE_BOOT_COMPLETED) {
             // Run the Inactive Association Removal job service daily.
             InactiveAssociationsRemovalService.schedule(getContext());
@@ -271,7 +253,7 @@
         // Notify and bind the app after the phone is unlocked.
         final int userId = user.getUserIdentifier();
         final Set<BluetoothDevice> blueToothDevices =
-                mDevicePresenceMonitor.getPendingConnectedDevices().get(userId);
+                mDevicePresenceProcessor.getPendingConnectedDevices().get(userId);
 
         final List<ObservableUuid> observableUuids =
                 mObservableUuidStore.getObservableUuidsForUser(userId);
@@ -287,14 +269,14 @@
                         mAssociationStore.getActiveAssociationsByAddress(
                                 bluetoothDevice.getAddress())) {
                     Slog.i(TAG, "onUserUnlocked, device id( " + ai.getId() + " ) is connected");
-                    mDevicePresenceMonitor.onBluetoothCompanionDeviceConnected(ai.getId());
+                    mDevicePresenceProcessor.onBluetoothCompanionDeviceConnected(ai.getId());
                 }
 
                 for (ObservableUuid observableUuid : observableUuids) {
                     if (deviceUuids.contains(observableUuid.getUuid())) {
                         Slog.i(TAG, "onUserUnlocked, UUID( "
                                 + observableUuid.getUuid() + " ) is connected");
-                        mDevicePresenceMonitor.onDevicePresenceEventByUuid(
+                        mDevicePresenceProcessor.onDevicePresenceEventByUuid(
                                 observableUuid, EVENT_BT_CONNECTED);
                     }
                 }
@@ -302,181 +284,6 @@
         }
     }
 
-    @NonNull
-    AssociationInfo getAssociationWithCallerChecks(
-            @UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) {
-        AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(
-                userId, packageName, macAddress);
-        association = sanitizeWithCallerChecks(getContext(), association);
-        if (association != null) {
-            return association;
-        } else {
-            throw new IllegalArgumentException("Association does not exist "
-                    + "or the caller does not have permissions to manage it "
-                    + "(ie. it belongs to a different package or a different user).");
-        }
-    }
-
-    @NonNull
-    AssociationInfo getAssociationWithCallerChecks(int associationId) {
-        AssociationInfo association = mAssociationStore.getAssociationById(associationId);
-        association = sanitizeWithCallerChecks(getContext(), association);
-        if (association != null) {
-            return association;
-        } else {
-            throw new IllegalArgumentException("Association does not exist "
-                    + "or the caller does not have permissions to manage it "
-                    + "(ie. it belongs to a different package or a different user).");
-        }
-    }
-
-    private void onDeviceAppearedInternal(int associationId) {
-        if (DEBUG) Log.i(TAG, "onDevice_Appeared_Internal() id=" + associationId);
-
-        final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
-        if (DEBUG) Log.d(TAG, "  association=" + association);
-
-        if (!association.shouldBindWhenPresent()) return;
-
-        bindApplicationIfNeeded(association);
-
-        mCompanionAppController.notifyCompanionApplicationDeviceAppeared(association);
-    }
-
-    private void onDeviceDisappearedInternal(int associationId) {
-        if (DEBUG) Log.i(TAG, "onDevice_Disappeared_Internal() id=" + associationId);
-
-        final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
-        if (DEBUG) Log.d(TAG, "  association=" + association);
-
-        final int userId = association.getUserId();
-        final String packageName = association.getPackageName();
-
-        if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
-            if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound");
-            return;
-        }
-
-        if (association.shouldBindWhenPresent()) {
-            mCompanionAppController.notifyCompanionApplicationDeviceDisappeared(association);
-        }
-    }
-
-    private void onDevicePresenceEventInternal(int associationId, int event) {
-        Slog.i(TAG, "onDevicePresenceEventInternal() id=" + associationId + " event= " + event);
-        final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
-        final String packageName = association.getPackageName();
-        final int userId = association.getUserId();
-        switch (event) {
-            case EVENT_BLE_APPEARED:
-            case EVENT_BT_CONNECTED:
-            case EVENT_SELF_MANAGED_APPEARED:
-                if (!association.shouldBindWhenPresent()) return;
-
-                bindApplicationIfNeeded(association);
-
-                mCompanionAppController.notifyCompanionDevicePresenceEvent(
-                        association, event);
-                break;
-            case EVENT_BLE_DISAPPEARED:
-            case EVENT_BT_DISCONNECTED:
-            case EVENT_SELF_MANAGED_DISAPPEARED:
-                if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
-                    if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound");
-                    return;
-                }
-                if (association.shouldBindWhenPresent()) {
-                    mCompanionAppController.notifyCompanionDevicePresenceEvent(
-                            association, event);
-                }
-                // Check if there are other devices associated to the app that are present.
-                if (shouldBindPackage(userId, packageName)) return;
-                mCompanionAppController.unbindCompanionApplication(userId, packageName);
-                break;
-            default:
-                Slog.e(TAG, "Event: " + event + "is not supported");
-                break;
-        }
-    }
-
-    private void onDevicePresenceEventByUuidInternal(ObservableUuid uuid, int event) {
-        Slog.i(TAG, "onDevicePresenceEventByUuidInternal() id=" + uuid.getUuid()
-                + "for package=" + uuid.getPackageName() + " event=" + event);
-        final String packageName = uuid.getPackageName();
-        final int userId = uuid.getUserId();
-
-        switch (event) {
-            case EVENT_BT_CONNECTED:
-                if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
-                    mCompanionAppController.bindCompanionApplication(
-                            userId, packageName, /*bindImportant*/ false);
-
-                } else if (DEBUG) {
-                    Log.i(TAG, "u" + userId + "\\" + packageName + " is already bound");
-                }
-
-                mCompanionAppController.notifyUuidDevicePresenceEvent(uuid, event);
-
-                break;
-            case EVENT_BT_DISCONNECTED:
-                if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
-                    if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound");
-                    return;
-                }
-
-                mCompanionAppController.notifyUuidDevicePresenceEvent(uuid, event);
-                // Check if there are other devices associated to the app or the UUID to be
-                // observed are present.
-                if (shouldBindPackage(userId, packageName)) return;
-
-                mCompanionAppController.unbindCompanionApplication(userId, packageName);
-
-                break;
-            default:
-                Slog.e(TAG, "Event: " + event + "is not supported");
-                break;
-        }
-    }
-
-    private void bindApplicationIfNeeded(AssociationInfo association) {
-        final String packageName = association.getPackageName();
-        final int userId = association.getUserId();
-        // Set bindImportant to true when the association is self-managed to avoid the target
-        // service being killed.
-        final boolean bindImportant = association.isSelfManaged();
-        if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
-            mCompanionAppController.bindCompanionApplication(
-                    userId, packageName, bindImportant);
-        } else if (DEBUG) {
-            Log.i(TAG, "u" + userId + "\\" + packageName + " is already bound");
-        }
-    }
-
-    /**
-     * @return whether the package should be bound (i.e. at least one of the devices associated with
-     * the package is currently present OR the UUID to be observed by this package is
-     * currently present).
-     */
-    private boolean shouldBindPackage(@UserIdInt int userId, @NonNull String packageName) {
-        final List<AssociationInfo> packageAssociations =
-                mAssociationStore.getActiveAssociationsByPackage(userId, packageName);
-        final List<ObservableUuid> observableUuids =
-                mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
-
-        for (AssociationInfo association : packageAssociations) {
-            if (!association.shouldBindWhenPresent()) continue;
-            if (mDevicePresenceMonitor.isDevicePresent(association.getId())) return true;
-        }
-
-        for (ObservableUuid uuid : observableUuids) {
-            if (mDevicePresenceMonitor.isDeviceUuidPresent(uuid.getUuid())) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
     private void onPackageRemoveOrDataClearedInternal(
             @UserIdInt int userId, @NonNull String packageName) {
         if (DEBUG) {
@@ -502,7 +309,7 @@
             mObservableUuidStore.removeObservableUuid(userId, uuid.getUuid(), packageName);
         }
 
-        mCompanionAppController.onPackagesChanged(userId);
+        mCompanionAppBinder.onPackagesChanged(userId);
     }
 
     private void onPackageModifiedInternal(@UserIdInt int userId, @NonNull String packageName) {
@@ -515,34 +322,15 @@
                     association.getPackageName());
         }
 
-        mCompanionAppController.onPackagesChanged(userId);
+        mCompanionAppBinder.onPackagesChanged(userId);
     }
 
     private void onPackageAddedInternal(@UserIdInt int userId, @NonNull String packageName) {
         mBackupRestoreProcessor.restorePendingAssociations(userId, packageName);
     }
 
-    // Revoke associations if the selfManaged companion device does not connect for 3 months.
     void removeInactiveSelfManagedAssociations() {
-        final long currentTime = System.currentTimeMillis();
-        long removalWindow = SystemProperties.getLong(SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW, -1);
-        if (removalWindow <= 0) {
-            // 0 or negative values indicate that the sysprop was never set or should be ignored.
-            removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT;
-        }
-
-        for (AssociationInfo association : mAssociationStore.getAssociations()) {
-            if (!association.isSelfManaged()) continue;
-
-            final boolean isInactive =
-                    currentTime - association.getLastTimeConnectedMs() >= removalWindow;
-            if (!isInactive) continue;
-
-            final int id = association.getId();
-
-            Slog.i(TAG, "Removing inactive self-managed association id=" + id);
-            mDisassociationProcessor.disassociate(id);
-        }
+        mDisassociationProcessor.removeIdleSelfManagedAssociations();
     }
 
     public class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub {
@@ -679,24 +467,15 @@
         @Deprecated
         @Override
         public void legacyDisassociate(String deviceMacAddress, String packageName, int userId) {
-            Log.i(TAG, "legacyDisassociate() pkg=u" + userId + "/" + packageName
-                    + ", macAddress=" + deviceMacAddress);
-
             requireNonNull(deviceMacAddress);
             requireNonNull(packageName);
 
-            final AssociationInfo association =
-                    getAssociationWithCallerChecks(userId, packageName, deviceMacAddress);
-            mDisassociationProcessor.disassociate(association.getId());
+            mDisassociationProcessor.disassociate(userId, packageName, deviceMacAddress);
         }
 
         @Override
         public void disassociate(int associationId) {
-            Slog.i(TAG, "disassociate() associationId=" + associationId);
-
-            final AssociationInfo association =
-                    getAssociationWithCallerChecks(associationId);
-            mDisassociationProcessor.disassociate(association.getId());
+            mDisassociationProcessor.disassociate(associationId);
         }
 
         @Override
@@ -758,21 +537,25 @@
         }
 
         @Override
+        @Deprecated
         @EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE)
-        public void registerDevicePresenceListenerService(String deviceAddress,
-                String callingPackage, int userId) throws RemoteException {
-            registerDevicePresenceListenerService_enforcePermission();
-            // TODO: take the userId into account.
-            registerDevicePresenceListenerActive(callingPackage, deviceAddress, true);
+        public void legacyStartObservingDevicePresence(String deviceAddress, String callingPackage,
+                int userId) throws RemoteException {
+            legacyStartObservingDevicePresence_enforcePermission();
+
+            mDevicePresenceProcessor.startObservingDevicePresence(userId, callingPackage,
+                    deviceAddress);
         }
 
         @Override
+        @Deprecated
         @EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE)
-        public void unregisterDevicePresenceListenerService(String deviceAddress,
-                String callingPackage, int userId) throws RemoteException {
-            unregisterDevicePresenceListenerService_enforcePermission();
-            // TODO: take the userId into account.
-            registerDevicePresenceListenerActive(callingPackage, deviceAddress, false);
+        public void legacyStopObservingDevicePresence(String deviceAddress, String callingPackage,
+                int userId) throws RemoteException {
+            legacyStopObservingDevicePresence_enforcePermission();
+
+            mDevicePresenceProcessor.stopObservingDevicePresence(userId, callingPackage,
+                    deviceAddress);
         }
 
         @Override
@@ -780,7 +563,8 @@
         public void startObservingDevicePresence(ObservingDevicePresenceRequest request,
                 String packageName, int userId) {
             startObservingDevicePresence_enforcePermission();
-            registerDevicePresenceListener(request, packageName, userId, /* active */ true);
+
+            mDevicePresenceProcessor.startObservingDevicePresence(request, packageName, userId);
         }
 
         @Override
@@ -788,80 +572,8 @@
         public void stopObservingDevicePresence(ObservingDevicePresenceRequest request,
                 String packageName, int userId) {
             stopObservingDevicePresence_enforcePermission();
-            registerDevicePresenceListener(request, packageName, userId, /* active */ false);
-        }
 
-        private void registerDevicePresenceListener(ObservingDevicePresenceRequest request,
-                String packageName, int userId, boolean active) {
-            enforceUsesCompanionDeviceFeature(getContext(), userId, packageName);
-            enforceCallerIsSystemOr(userId, packageName);
-
-            final int associationId = request.getAssociationId();
-            final AssociationInfo associationInfo = mAssociationStore.getAssociationById(
-                    associationId);
-            final ParcelUuid uuid = request.getUuid();
-
-            if (uuid != null) {
-                enforceCallerCanObservingDevicePresenceByUuid(getContext());
-                if (active) {
-                    startObservingDevicePresenceByUuid(uuid, packageName, userId);
-                } else {
-                    stopObservingDevicePresenceByUuid(uuid, packageName, userId);
-                }
-            } else if (associationInfo == null) {
-                throw new IllegalArgumentException("App " + packageName
-                        + " is not associated with device " + request.getAssociationId()
-                        + " for user " + userId);
-            } else {
-                processDevicePresenceListener(
-                        associationInfo, userId, packageName, active);
-            }
-        }
-
-        private void startObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName,
-                int userId) {
-            final List<ObservableUuid> observableUuids =
-                    mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
-
-            for (ObservableUuid observableUuid : observableUuids) {
-                if (observableUuid.getUuid().equals(uuid)) {
-                    Slog.i(TAG, "The uuid: " + uuid + " for package:" + packageName
-                            + "has been already scheduled for observing");
-                    return;
-                }
-            }
-
-            final ObservableUuid observableUuid = new ObservableUuid(userId, uuid,
-                    packageName, System.currentTimeMillis());
-
-            mObservableUuidStore.writeObservableUuid(userId, observableUuid);
-        }
-
-        private void stopObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName,
-                int userId) {
-            final List<ObservableUuid> uuidsTobeObserved =
-                    mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
-            boolean isScheduledObserving = false;
-
-            for (ObservableUuid observableUuid : uuidsTobeObserved) {
-                if (observableUuid.getUuid().equals(uuid)) {
-                    isScheduledObserving = true;
-                    break;
-                }
-            }
-
-            if (!isScheduledObserving) {
-                Slog.i(TAG, "The uuid: " + uuid.toString() + " for package:" + packageName
-                        + "has NOT been scheduled for observing yet");
-                return;
-            }
-
-            mObservableUuidStore.removeObservableUuid(userId, uuid, packageName);
-            mDevicePresenceMonitor.removeCurrentConnectedUuidDevice(uuid);
-
-            if (!shouldBindPackage(userId, packageName)) {
-                mCompanionAppController.unbindCompanionApplication(userId, packageName);
-            }
+            mDevicePresenceProcessor.stopObservingDevicePresence(request, packageName, userId);
         }
 
         @Override
@@ -874,8 +586,7 @@
         @Override
         public boolean isPermissionTransferUserConsented(String packageName, int userId,
                 int associationId) {
-            return mSystemDataTransferProcessor.isPermissionTransferUserConsented(packageName,
-                    userId, associationId);
+            return mSystemDataTransferProcessor.isPermissionTransferUserConsented(associationId);
         }
 
         @Override
@@ -891,8 +602,7 @@
                 ParcelFileDescriptor fd) {
             attachSystemDataTransport_enforcePermission();
 
-            getAssociationWithCallerChecks(associationId);
-            mTransportManager.attachSystemDataTransport(packageName, userId, associationId, fd);
+            mTransportManager.attachSystemDataTransport(associationId, fd);
         }
 
         @Override
@@ -900,161 +610,61 @@
         public void detachSystemDataTransport(String packageName, int userId, int associationId) {
             detachSystemDataTransport_enforcePermission();
 
-            getAssociationWithCallerChecks(associationId);
-            mTransportManager.detachSystemDataTransport(packageName, userId, associationId);
-        }
-
-        @Override
-        public void enableSystemDataSync(int associationId, int flags) {
-            getAssociationWithCallerChecks(associationId);
-            mAssociationRequestsProcessor.enableSystemDataSync(associationId, flags);
-        }
-
-        @Override
-        public void disableSystemDataSync(int associationId, int flags) {
-            getAssociationWithCallerChecks(associationId);
-            mAssociationRequestsProcessor.disableSystemDataSync(associationId, flags);
-        }
-
-        @Override
-        public void enablePermissionsSync(int associationId) {
-            getAssociationWithCallerChecks(associationId);
-            mSystemDataTransferProcessor.enablePermissionsSync(associationId);
-        }
-
-        @Override
-        public void disablePermissionsSync(int associationId) {
-            getAssociationWithCallerChecks(associationId);
-            mSystemDataTransferProcessor.disablePermissionsSync(associationId);
-        }
-
-        @Override
-        public PermissionSyncRequest getPermissionSyncRequest(int associationId) {
-            // TODO: temporary fix, will remove soon
-            AssociationInfo association = mAssociationStore.getAssociationById(associationId);
-            if (association == null) {
-                return null;
-            }
-            getAssociationWithCallerChecks(associationId);
-            return mSystemDataTransferProcessor.getPermissionSyncRequest(associationId);
+            mTransportManager.detachSystemDataTransport(associationId);
         }
 
         @Override
         @EnforcePermission(MANAGE_COMPANION_DEVICES)
         public void enableSecureTransport(boolean enabled) {
             enableSecureTransport_enforcePermission();
+
             mTransportManager.enableSecureTransport(enabled);
         }
 
         @Override
-        public void notifyDeviceAppeared(int associationId) {
-            if (DEBUG) Log.i(TAG, "notifyDevice_Appeared() id=" + associationId);
-
-            AssociationInfo association = getAssociationWithCallerChecks(associationId);
-            if (!association.isSelfManaged()) {
-                throw new IllegalArgumentException("Association with ID " + associationId
-                        + " is not self-managed. notifyDeviceAppeared(int) can only be called for"
-                        + " self-managed associations.");
-            }
-            // AssociationInfo class is immutable: create a new AssociationInfo object with updated
-            // timestamp.
-            association = (new AssociationInfo.Builder(association))
-                    .setLastTimeConnected(System.currentTimeMillis())
-                    .build();
-            mAssociationStore.updateAssociation(association);
-
-            mDevicePresenceMonitor.onSelfManagedDeviceConnected(associationId);
-
-            final String deviceProfile = association.getDeviceProfile();
-            if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) {
-                Slog.i(TAG, "Enable hint mode for device device profile: " + deviceProfile);
-                mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, true);
-            }
+        public void enableSystemDataSync(int associationId, int flags) {
+            mAssociationRequestsProcessor.enableSystemDataSync(associationId, flags);
         }
 
         @Override
-        public void notifyDeviceDisappeared(int associationId) {
-            if (DEBUG) Log.i(TAG, "notifyDevice_Disappeared() id=" + associationId);
+        public void disableSystemDataSync(int associationId, int flags) {
+            mAssociationRequestsProcessor.disableSystemDataSync(associationId, flags);
+        }
 
-            final AssociationInfo association = getAssociationWithCallerChecks(associationId);
-            if (!association.isSelfManaged()) {
-                throw new IllegalArgumentException("Association with ID " + associationId
-                        + " is not self-managed. notifyDeviceAppeared(int) can only be called for"
-                        + " self-managed associations.");
-            }
+        @Override
+        public void enablePermissionsSync(int associationId) {
+            mSystemDataTransferProcessor.enablePermissionsSync(associationId);
+        }
 
-            mDevicePresenceMonitor.onSelfManagedDeviceDisconnected(associationId);
+        @Override
+        public void disablePermissionsSync(int associationId) {
+            mSystemDataTransferProcessor.disablePermissionsSync(associationId);
+        }
 
-            final String deviceProfile = association.getDeviceProfile();
-            if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) {
-                Slog.i(TAG, "Disable hint mode for device profile: " + deviceProfile);
-                mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false);
-            }
+        @Override
+        public PermissionSyncRequest getPermissionSyncRequest(int associationId) {
+            return mSystemDataTransferProcessor.getPermissionSyncRequest(associationId);
+        }
+
+        @Override
+        @EnforcePermission(REQUEST_COMPANION_SELF_MANAGED)
+        public void notifySelfManagedDeviceAppeared(int associationId) {
+            notifySelfManagedDeviceAppeared_enforcePermission();
+
+            mDevicePresenceProcessor.notifySelfManagedDevicePresenceEvent(associationId, true);
+        }
+
+        @Override
+        @EnforcePermission(REQUEST_COMPANION_SELF_MANAGED)
+        public void notifySelfManagedDeviceDisappeared(int associationId) {
+            notifySelfManagedDeviceDisappeared_enforcePermission();
+
+            mDevicePresenceProcessor.notifySelfManagedDevicePresenceEvent(associationId, false);
         }
 
         @Override
         public boolean isCompanionApplicationBound(String packageName, int userId) {
-            return mCompanionAppController.isCompanionApplicationBound(userId, packageName);
-        }
-
-        private void registerDevicePresenceListenerActive(String packageName, String deviceAddress,
-                boolean active) throws RemoteException {
-            if (DEBUG) {
-                Log.i(TAG, "registerDevicePresenceListenerActive()"
-                        + " active=" + active
-                        + " deviceAddress=" + deviceAddress);
-            }
-            final int userId = getCallingUserId();
-            enforceCallerIsSystemOr(userId, packageName);
-
-            AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(
-                    userId, packageName, deviceAddress);
-
-            if (association == null) {
-                throw new RemoteException(new DeviceNotAssociatedException("App " + packageName
-                        + " is not associated with device " + deviceAddress
-                        + " for user " + userId));
-            }
-
-            processDevicePresenceListener(association, userId, packageName, active);
-        }
-
-        private void processDevicePresenceListener(AssociationInfo association,
-                int userId, String packageName, boolean active) {
-            // If already at specified state, then no-op.
-            if (active == association.isNotifyOnDeviceNearby()) {
-                if (DEBUG) Log.d(TAG, "Device presence listener is already at desired state.");
-                return;
-            }
-
-            // AssociationInfo class is immutable: create a new AssociationInfo object with updated
-            // flag.
-            association = (new AssociationInfo.Builder(association))
-                    .setNotifyOnDeviceNearby(active)
-                    .build();
-            // Do not need to call {@link BleCompanionDeviceScanner#restartScan()} since it will
-            // trigger {@link BleCompanionDeviceScanner#restartScan(int, AssociationInfo)} when
-            // an application sets/unsets the mNotifyOnDeviceNearby flag.
-            mAssociationStore.updateAssociation(association);
-
-            int associationId = association.getId();
-            // If device is already present, then trigger callback.
-            if (active && mDevicePresenceMonitor.isDevicePresent(associationId)) {
-                Slog.i(TAG, "Device is already present. Triggering callback.");
-                if (mDevicePresenceMonitor.isBlePresent(associationId)
-                        || mDevicePresenceMonitor.isSimulatePresent(associationId)) {
-                    onDeviceAppearedInternal(associationId);
-                    onDevicePresenceEventInternal(associationId, EVENT_BLE_APPEARED);
-                } else if (mDevicePresenceMonitor.isBtConnected(associationId)) {
-                    onDevicePresenceEventInternal(associationId, EVENT_BT_CONNECTED);
-                }
-            }
-
-            // If last listener is unregistered, then unbind application.
-            if (!active && !shouldBindPackage(userId, packageName)) {
-                if (DEBUG) Log.d(TAG, "Last listener unregistered. Unbinding application.");
-                mCompanionAppController.unbindCompanionApplication(userId, packageName);
-            }
+            return mCompanionAppBinder.isCompanionApplicationBound(userId, packageName);
         }
 
         @Override
@@ -1070,7 +680,8 @@
             }
 
             final MacAddress macAddressObj = MacAddress.fromString(macAddress);
-            createNewAssociation(userId, packageName, macAddressObj, null, null, false);
+            mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddressObj,
+                    null, null, null, false, null, null);
         }
 
         private void checkCanCallNotificationApi(String callingPackage, int userId) {
@@ -1099,9 +710,7 @@
 
         @Override
         public void setAssociationTag(int associationId, String tag) {
-            AssociationInfo association = getAssociationWithCallerChecks(associationId);
-            association = (new AssociationInfo.Builder(association)).setTag(tag).build();
-            mAssociationStore.updateAssociation(association);
+            mAssociationRequestsProcessor.setAssociationTag(associationId, tag);
         }
 
         @Override
@@ -1124,7 +733,7 @@
                 @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err,
                 @NonNull String[] args) {
             return new CompanionDeviceShellCommand(CompanionDeviceManagerService.this,
-                    mAssociationStore, mDevicePresenceMonitor, mTransportManager,
+                    mAssociationStore, mDevicePresenceProcessor, mTransportManager,
                     mSystemDataTransferProcessor, mAssociationRequestsProcessor,
                     mBackupRestoreProcessor, mDisassociationProcessor)
                     .exec(this, in.getFileDescriptor(), out.getFileDescriptor(),
@@ -1139,21 +748,13 @@
             }
 
             mAssociationStore.dump(out);
-            mDevicePresenceMonitor.dump(out);
-            mCompanionAppController.dump(out);
+            mDevicePresenceProcessor.dump(out);
+            mCompanionAppBinder.dump(out);
             mTransportManager.dump(out);
             mSystemDataTransferRequestStore.dump(out);
         }
     }
 
-    void createNewAssociation(@UserIdInt int userId, @NonNull String packageName,
-            @Nullable MacAddress macAddress, @Nullable CharSequence displayName,
-            @Nullable String deviceProfile, boolean isSelfManaged) {
-        mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress,
-                displayName, deviceProfile, /* associatedDevice */ null, isSelfManaged,
-                /* callback */ null, /* resultReceiver */ null);
-    }
-
     /**
      * Update special access for the association's package
      */
@@ -1169,8 +770,6 @@
             return;
         }
 
-        Slog.i(TAG, "Updating special access for package=[" + packageInfo.packageName + "]...");
-
         if (containsEither(packageInfo.requestedPermissions,
                 android.Manifest.permission.RUN_IN_BACKGROUND,
                 android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
@@ -1280,29 +879,6 @@
                 }
             };
 
-    private final CompanionDevicePresenceMonitor.Callback mDevicePresenceCallback =
-            new CompanionDevicePresenceMonitor.Callback() {
-                @Override
-                public void onDeviceAppeared(int associationId) {
-                    onDeviceAppearedInternal(associationId);
-                }
-
-                @Override
-                public void onDeviceDisappeared(int associationId) {
-                    onDeviceDisappearedInternal(associationId);
-                }
-
-                @Override
-                public void onDevicePresenceEvent(int associationId, int event) {
-                    onDevicePresenceEventInternal(associationId, event);
-                }
-
-                @Override
-                public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) {
-                    onDevicePresenceEventByUuidInternal(uuid, event);
-                }
-            };
-
     private final PackageMonitor mPackageMonitor = new PackageMonitor() {
         @Override
         public void onPackageRemoved(String packageName, int uid) {
@@ -1315,7 +891,7 @@
         }
 
         @Override
-        public void onPackageModified(String packageName) {
+        public void onPackageModified(@NonNull String packageName) {
             onPackageModifiedInternal(getChangingUserId(), packageName);
         }
 
@@ -1325,25 +901,15 @@
         }
     };
 
-    private static Map<String, Set<Integer>> deepUnmodifiableCopy(Map<String, Set<Integer>> orig) {
-        final Map<String, Set<Integer>> copy = new HashMap<>();
-
-        for (Map.Entry<String, Set<Integer>> entry : orig.entrySet()) {
-            final Set<Integer> valueCopy = new HashSet<>(entry.getValue());
-            copy.put(entry.getKey(), Collections.unmodifiableSet(valueCopy));
-        }
-
-        return Collections.unmodifiableMap(copy);
-    }
-
     private static <T> boolean containsEither(T[] array, T a, T b) {
         return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b);
     }
 
     private class LocalService implements CompanionDeviceManagerServiceInternal {
+
         @Override
         public void removeInactiveSelfManagedAssociations() {
-            CompanionDeviceManagerService.this.removeInactiveSelfManagedAssociations();
+            mDisassociationProcessor.removeIdleSelfManagedAssociations();
         }
 
         @Override
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java
index cdf832f..9d1250d 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java
@@ -27,8 +27,9 @@
  * Companion Device Manager Local System Service Interface.
  */
 public interface CompanionDeviceManagerServiceInternal {
+
     /**
-     * @see CompanionDeviceManagerService#removeInactiveSelfManagedAssociations
+     * Remove idle self-managed associations.
      */
     void removeInactiveSelfManagedAssociations();
 
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
index a7a73cb..a789384 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
@@ -18,8 +18,6 @@
 
 import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_CONTEXT_SYNC;
 
-import static com.android.server.companion.utils.PermissionsUtils.sanitizeWithCallerChecks;
-
 import android.companion.AssociationInfo;
 import android.companion.ContextSyncMessage;
 import android.companion.Flags;
@@ -38,7 +36,7 @@
 import com.android.server.companion.datatransfer.SystemDataTransferProcessor;
 import com.android.server.companion.datatransfer.contextsync.BitmapUtils;
 import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController;
-import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
+import com.android.server.companion.presence.DevicePresenceProcessor;
 import com.android.server.companion.presence.ObservableUuid;
 import com.android.server.companion.transport.CompanionTransportManager;
 
@@ -51,7 +49,7 @@
     private final CompanionDeviceManagerService mService;
     private final DisassociationProcessor mDisassociationProcessor;
     private final AssociationStore mAssociationStore;
-    private final CompanionDevicePresenceMonitor mDevicePresenceMonitor;
+    private final DevicePresenceProcessor mDevicePresenceProcessor;
     private final CompanionTransportManager mTransportManager;
 
     private final SystemDataTransferProcessor mSystemDataTransferProcessor;
@@ -60,7 +58,7 @@
 
     CompanionDeviceShellCommand(CompanionDeviceManagerService service,
             AssociationStore associationStore,
-            CompanionDevicePresenceMonitor devicePresenceMonitor,
+            DevicePresenceProcessor devicePresenceProcessor,
             CompanionTransportManager transportManager,
             SystemDataTransferProcessor systemDataTransferProcessor,
             AssociationRequestsProcessor associationRequestsProcessor,
@@ -68,7 +66,7 @@
             DisassociationProcessor disassociationProcessor) {
         mService = service;
         mAssociationStore = associationStore;
-        mDevicePresenceMonitor = devicePresenceMonitor;
+        mDevicePresenceProcessor = devicePresenceProcessor;
         mTransportManager = transportManager;
         mSystemDataTransferProcessor = systemDataTransferProcessor;
         mAssociationRequestsProcessor = associationRequestsProcessor;
@@ -85,7 +83,7 @@
             if ("simulate-device-event".equals(cmd) && Flags.devicePresence()) {
                 associationId = getNextIntArgRequired();
                 int event = getNextIntArgRequired();
-                mDevicePresenceMonitor.simulateDeviceEvent(associationId, event);
+                mDevicePresenceProcessor.simulateDeviceEvent(associationId, event);
                 return 0;
             }
 
@@ -97,7 +95,7 @@
                 ObservableUuid observableUuid = new ObservableUuid(
                         userId, ParcelUuid.fromString(uuid), packageName,
                         System.currentTimeMillis());
-                mDevicePresenceMonitor.simulateDeviceEventByUuid(observableUuid, event);
+                mDevicePresenceProcessor.simulateDeviceEventByUuid(observableUuid, event);
                 return 0;
             }
 
@@ -124,8 +122,9 @@
                     String address = getNextArgRequired();
                     String deviceProfile = getNextArg();
                     final MacAddress macAddress = MacAddress.fromString(address);
-                    mService.createNewAssociation(userId, packageName, macAddress,
-                            /* displayName= */ deviceProfile, deviceProfile, false);
+                    mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress,
+                            deviceProfile, deviceProfile, /* associatedDevice */ null, false,
+                            /* callback */ null, /* resultReceiver */ null);
                 }
                 break;
 
@@ -134,8 +133,13 @@
                     final String packageName = getNextArgRequired();
                     final String address = getNextArgRequired();
                     final AssociationInfo association =
-                            mService.getAssociationWithCallerChecks(userId, packageName, address);
-                    mDisassociationProcessor.disassociate(association.getId());
+                            mAssociationStore.getFirstAssociationByAddress(userId, packageName,
+                                    address);
+                    if (association == null) {
+                        out.println("Association doesn't exist.");
+                    } else {
+                        mDisassociationProcessor.disassociate(association.getId());
+                    }
                 }
                 break;
 
@@ -144,9 +148,7 @@
                     final List<AssociationInfo> userAssociations =
                             mAssociationStore.getAssociationsByUser(userId);
                     for (AssociationInfo association : userAssociations) {
-                        if (sanitizeWithCallerChecks(mService.getContext(), association) != null) {
-                            mDisassociationProcessor.disassociate(association.getId());
-                        }
+                        mDisassociationProcessor.disassociate(association.getId());
                     }
                 }
                 break;
@@ -157,12 +159,12 @@
 
                 case "simulate-device-appeared":
                     associationId = getNextIntArgRequired();
-                    mDevicePresenceMonitor.simulateDeviceEvent(associationId, /* event */ 0);
+                    mDevicePresenceProcessor.simulateDeviceEvent(associationId, /* event */ 0);
                     break;
 
                 case "simulate-device-disappeared":
                     associationId = getNextIntArgRequired();
-                    mDevicePresenceMonitor.simulateDeviceEvent(associationId, /* event */ 1);
+                    mDevicePresenceProcessor.simulateDeviceEvent(associationId, /* event */ 1);
                     break;
 
                 case "get-backup-payload": {
@@ -410,10 +412,9 @@
         pw.println("      Remove an existing Association.");
         pw.println("  disassociate-all USER_ID");
         pw.println("      Remove all Associations for a user.");
-        pw.println("  clear-association-memory-cache");
+        pw.println("  refresh-cache");
         pw.println("      Clear the in-memory association cache and reload all association ");
-        pw.println("      information from persistent storage. USE FOR DEBUGGING PURPOSES ONLY.");
-        pw.println("      USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
+        pw.println("      information from disk. USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
 
         pw.println("  simulate-device-appeared ASSOCIATION_ID");
         pw.println("      Make CDM act as if the given companion device has appeared.");
diff --git a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
index a02d9f9..a18776e 100644
--- a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
@@ -145,7 +145,8 @@
 
     /**
      * Handle incoming {@link AssociationRequest}s, sent via
-     * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest, IAssociationRequestCallback, String, int)}
+     * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest,
+     * IAssociationRequestCallback, String, int)}
      */
     public void processNewAssociationRequest(@NonNull AssociationRequest request,
             @NonNull String packageName, @UserIdInt int userId,
@@ -212,7 +213,8 @@
         // 2b.4. Send the PendingIntent back to the app.
         try {
             callback.onAssociationPending(pendingIntent);
-        } catch (RemoteException ignore) { }
+        } catch (RemoteException ignore) {
+        }
     }
 
     /**
@@ -252,7 +254,8 @@
             // forward it back to the application via the callback.
             try {
                 callback.onFailure(e.getMessage());
-            } catch (RemoteException ignore) { }
+            } catch (RemoteException ignore) {
+            }
             return;
         }
 
@@ -322,7 +325,8 @@
      * Enable system data sync.
      */
     public void enableSystemDataSync(int associationId, int flags) {
-        AssociationInfo association = mAssociationStore.getAssociationById(associationId);
+        AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
+                associationId);
         AssociationInfo updated = (new AssociationInfo.Builder(association))
                 .setSystemDataSyncFlags(association.getSystemDataSyncFlags() | flags).build();
         mAssociationStore.updateAssociation(updated);
@@ -332,12 +336,23 @@
      * Disable system data sync.
      */
     public void disableSystemDataSync(int associationId, int flags) {
-        AssociationInfo association = mAssociationStore.getAssociationById(associationId);
+        AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
+                associationId);
         AssociationInfo updated = (new AssociationInfo.Builder(association))
                 .setSystemDataSyncFlags(association.getSystemDataSyncFlags() & (~flags)).build();
         mAssociationStore.updateAssociation(updated);
     }
 
+    /**
+     * Set association tag.
+     */
+    public void setAssociationTag(int associationId, String tag) {
+        AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
+                associationId);
+        association = (new AssociationInfo.Builder(association)).setTag(tag).build();
+        mAssociationStore.updateAssociation(association);
+    }
+
     private void sendCallbackAndFinish(@Nullable AssociationInfo association,
             @Nullable IAssociationRequestCallback callback,
             @Nullable ResultReceiver resultReceiver) {
@@ -396,14 +411,14 @@
         // If the application already has a pending association request, that PendingIntent
         // will be cancelled except application wants to cancel the request by the system.
         return Binder.withCleanCallingIdentity(() ->
-            PendingIntent.getActivityAsUser(
-                    mContext, /*requestCode */ packageUid, intent,
-                    FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE,
-                    ActivityOptions.makeBasic()
-                            .setPendingIntentCreatorBackgroundActivityStartMode(
-                                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
-                            .toBundle(),
-                    UserHandle.CURRENT)
+                PendingIntent.getActivityAsUser(
+                        mContext, /*requestCode */ packageUid, intent,
+                        FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE,
+                        ActivityOptions.makeBasic()
+                                .setPendingIntentCreatorBackgroundActivityStartMode(
+                                        ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
+                                .toBundle(),
+                        UserHandle.CURRENT)
         );
     }
 
diff --git a/services/companion/java/com/android/server/companion/association/AssociationStore.java b/services/companion/java/com/android/server/companion/association/AssociationStore.java
index edebb55..ae2b708 100644
--- a/services/companion/java/com/android/server/companion/association/AssociationStore.java
+++ b/services/companion/java/com/android/server/companion/association/AssociationStore.java
@@ -18,6 +18,7 @@
 
 import static com.android.server.companion.utils.MetricUtils.logCreateAssociation;
 import static com.android.server.companion.utils.MetricUtils.logRemoveAssociation;
+import static com.android.server.companion.utils.PermissionsUtils.checkCallerCanManageAssociationsForPackage;
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -26,6 +27,7 @@
 import android.annotation.UserIdInt;
 import android.companion.AssociationInfo;
 import android.companion.IOnAssociationsChangedListener;
+import android.content.Context;
 import android.content.pm.UserInfo;
 import android.net.MacAddress;
 import android.os.Binder;
@@ -57,21 +59,22 @@
 @SuppressLint("LongLogTag")
 public class AssociationStore {
 
-    @IntDef(prefix = { "CHANGE_TYPE_" }, value = {
+    @IntDef(prefix = {"CHANGE_TYPE_"}, value = {
             CHANGE_TYPE_ADDED,
             CHANGE_TYPE_REMOVED,
             CHANGE_TYPE_UPDATED_ADDRESS_CHANGED,
             CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED,
     })
     @Retention(RetentionPolicy.SOURCE)
-    public @interface ChangeType {}
+    public @interface ChangeType {
+    }
 
     public static final int CHANGE_TYPE_ADDED = 0;
     public static final int CHANGE_TYPE_REMOVED = 1;
     public static final int CHANGE_TYPE_UPDATED_ADDRESS_CHANGED = 2;
     public static final int CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED = 3;
 
-    /**  Listener for any changes to associations. */
+    /** Listener for any changes to associations. */
     public interface OnChangeListener {
         /**
          * Called when there are association changes.
@@ -100,25 +103,30 @@
         /**
          * Called when an association is added.
          */
-        default void onAssociationAdded(AssociationInfo association) {}
+        default void onAssociationAdded(AssociationInfo association) {
+        }
 
         /**
          * Called when an association is removed.
          */
-        default void onAssociationRemoved(AssociationInfo association) {}
+        default void onAssociationRemoved(AssociationInfo association) {
+        }
 
         /**
          * Called when an association is updated.
          */
-        default void onAssociationUpdated(AssociationInfo association, boolean addressChanged) {}
+        default void onAssociationUpdated(AssociationInfo association, boolean addressChanged) {
+        }
     }
 
     private static final String TAG = "CDM_AssociationStore";
 
-    private final Object mLock = new Object();
-
+    private final Context mContext;
+    private final UserManager mUserManager;
+    private final AssociationDiskStore mDiskStore;
     private final ExecutorService mExecutor;
 
+    private final Object mLock = new Object();
     @GuardedBy("mLock")
     private boolean mPersisted = false;
     @GuardedBy("mLock")
@@ -132,10 +140,9 @@
     private final RemoteCallbackList<IOnAssociationsChangedListener> mRemoteListeners =
             new RemoteCallbackList<>();
 
-    private final UserManager mUserManager;
-    private final AssociationDiskStore mDiskStore;
-
-    public AssociationStore(UserManager userManager, AssociationDiskStore diskStore) {
+    public AssociationStore(Context context, UserManager userManager,
+            AssociationDiskStore diskStore) {
+        mContext = context;
         mUserManager = userManager;
         mDiskStore = diskStore;
         mExecutor = Executors.newSingleThreadExecutor();
@@ -202,7 +209,7 @@
 
         synchronized (mLock) {
             if (mIdToAssociationMap.containsKey(id)) {
-                Slog.e(TAG, "Association with id=[" + id + "] already exists.");
+                Slog.e(TAG, "Association id=[" + id + "] already exists.");
                 return;
             }
 
@@ -449,6 +456,26 @@
     }
 
     /**
+     * Get association by id with caller checks.
+     */
+    @NonNull
+    public AssociationInfo getAssociationWithCallerChecks(int associationId) {
+        AssociationInfo association = getAssociationById(associationId);
+        if (association == null) {
+            throw new IllegalArgumentException(
+                    "getAssociationWithCallerChecks() Association id=[" + associationId
+                            + "] doesn't exist.");
+        }
+        if (checkCallerCanManageAssociationsForPackage(mContext, association.getUserId(),
+                association.getPackageName())) {
+            return association;
+        }
+
+        throw new IllegalArgumentException(
+                "The caller can't interact with the association id=[" + associationId + "].");
+    }
+
+    /**
      * Register a local listener for association changes.
      */
     public void registerLocalListener(@NonNull OnChangeListener listener) {
diff --git a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java
index ec897791..acf683d 100644
--- a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java
+++ b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java
@@ -22,6 +22,8 @@
 import static com.android.internal.util.CollectionUtils.any;
 import static com.android.server.companion.utils.RolesUtils.removeRoleHolderForAssociation;
 
+import static java.util.concurrent.TimeUnit.DAYS;
+
 import android.annotation.NonNull;
 import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
@@ -30,21 +32,27 @@
 import android.content.Context;
 import android.content.pm.PackageManagerInternal;
 import android.os.Binder;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.util.Slog;
 
-import com.android.server.companion.CompanionApplicationController;
 import com.android.server.companion.datatransfer.SystemDataTransferRequestStore;
-import com.android.server.companion.presence.CompanionDevicePresenceMonitor;
+import com.android.server.companion.presence.CompanionAppBinder;
+import com.android.server.companion.presence.DevicePresenceProcessor;
 import com.android.server.companion.transport.CompanionTransportManager;
 
 /**
- * A class response for Association removal.
+ * This class responsible for disassociation.
  */
 @SuppressLint("LongLogTag")
 public class DisassociationProcessor {
 
     private static final String TAG = "CDM_DisassociationProcessor";
+
+    private static final String SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW =
+            "debug.cdm.cdmservice.removal_time_window";
+    private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90);
+
     @NonNull
     private final Context mContext;
     @NonNull
@@ -52,11 +60,11 @@
     @NonNull
     private final PackageManagerInternal mPackageManagerInternal;
     @NonNull
-    private final CompanionDevicePresenceMonitor mDevicePresenceMonitor;
+    private final DevicePresenceProcessor mDevicePresenceMonitor;
     @NonNull
     private final SystemDataTransferRequestStore mSystemDataTransferRequestStore;
     @NonNull
-    private final CompanionApplicationController mCompanionAppController;
+    private final CompanionAppBinder mCompanionAppController;
     @NonNull
     private final CompanionTransportManager mTransportManager;
     private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener;
@@ -66,8 +74,8 @@
             @NonNull ActivityManager activityManager,
             @NonNull AssociationStore associationStore,
             @NonNull PackageManagerInternal packageManager,
-            @NonNull CompanionDevicePresenceMonitor devicePresenceMonitor,
-            @NonNull CompanionApplicationController applicationController,
+            @NonNull DevicePresenceProcessor devicePresenceMonitor,
+            @NonNull CompanionAppBinder applicationController,
             @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore,
             @NonNull CompanionTransportManager companionTransportManager) {
         mContext = context;
@@ -89,11 +97,7 @@
     public void disassociate(int id) {
         Slog.i(TAG, "Disassociating id=[" + id + "]...");
 
-        final AssociationInfo association = mAssociationStore.getAssociationById(id);
-        if (association == null) {
-            Slog.e(TAG, "Can't disassociate id=[" + id + "]. It doesn't exist.");
-            return;
-        }
+        final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(id);
 
         final int userId = association.getUserId();
         final String packageName = association.getPackageName();
@@ -118,12 +122,12 @@
             return;
         }
 
-        // Association cleanup.
-        mAssociationStore.removeAssociation(association.getId());
-        mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, id);
-
         // Detach transport if exists
-        mTransportManager.detachSystemDataTransport(packageName, userId, id);
+        mTransportManager.detachSystemDataTransport(id);
+
+        // Association cleanup.
+        mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, id);
+        mAssociationStore.removeAssociation(association.getId());
 
         // If role is not in use by other associations, revoke the role.
         // Do not need to remove the system role since it was pre-granted by the system.
@@ -143,10 +147,28 @@
                 it -> it.isNotifyOnDeviceNearby()
                         && mDevicePresenceMonitor.isDevicePresent(it.getId()));
         if (!shouldStayBound) {
-            mCompanionAppController.unbindCompanionApplication(userId, packageName);
+            mCompanionAppController.unbindCompanionApp(userId, packageName);
         }
     }
 
+    /**
+     * @deprecated Use {@link #disassociate(int)} instead.
+     */
+    @Deprecated
+    public void disassociate(int userId, String packageName, String macAddress) {
+        AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId,
+                packageName, macAddress);
+
+        if (association == null) {
+            throw new IllegalArgumentException(
+                    "Association for mac address=[" + macAddress + "] doesn't exist");
+        }
+
+        mAssociationStore.getAssociationWithCallerChecks(association.getId());
+
+        disassociate(association.getId());
+    }
+
     @SuppressLint("MissingPermission")
     private int getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) {
         return Binder.withCleanCallingIdentity(() -> {
@@ -163,7 +185,7 @@
                     () -> mActivityManager.addOnUidImportanceListener(
                             mOnPackageVisibilityChangeListener,
                             ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE));
-        }  catch (IllegalArgumentException e) {
+        } catch (IllegalArgumentException e) {
             Slog.e(TAG, "Failed to start listening to uid importance changes.");
         }
     }
@@ -179,6 +201,34 @@
     }
 
     /**
+     * Remove idle self-managed associations.
+     */
+    public void removeIdleSelfManagedAssociations() {
+        Slog.i(TAG, "Removing idle self-managed associations.");
+
+        final long currentTime = System.currentTimeMillis();
+        long removalWindow = SystemProperties.getLong(SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW, -1);
+        if (removalWindow <= 0) {
+            // 0 or negative values indicate that the sysprop was never set or should be ignored.
+            removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT;
+        }
+
+        for (AssociationInfo association : mAssociationStore.getAssociations()) {
+            if (!association.isSelfManaged()) continue;
+
+            final boolean isInactive =
+                    currentTime - association.getLastTimeConnectedMs() >= removalWindow;
+            if (!isInactive) continue;
+
+            final int id = association.getId();
+
+            Slog.i(TAG, "Removing inactive self-managed association=[" + association.toShortString()
+                    + "].");
+            disassociate(id);
+        }
+    }
+
+    /**
      * An OnUidImportanceListener class which watches the importance of the packages.
      * In this class, we ONLY interested in the importance of the running process is greater than
      * {@link ActivityManager.RunningAppProcessInfo#IMPORTANCE_VISIBLE}.
diff --git a/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java
index f287315..b509e71 100644
--- a/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java
+++ b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java
@@ -30,7 +30,7 @@
 import com.android.server.companion.CompanionDeviceManagerServiceInternal;
 
 /**
- * A Job Service responsible for clean up idle self-managed associations.
+ * A Job Service responsible for clean up self-managed associations if it's idle for 90 days.
  *
  * The job will be executed only if the device is charging and in idle mode due to the application
  * will be killed if association/role are revoked. See {@link DisassociationProcessor}
@@ -45,10 +45,10 @@
     @Override
     public boolean onStartJob(final JobParameters params) {
         Slog.i(TAG, "Execute the Association Removal job");
-        // Special policy for selfManaged that need to revoke associations if the device
-        // does not connect for 90 days.
+
         LocalServices.getService(CompanionDeviceManagerServiceInternal.class)
                 .removeInactiveSelfManagedAssociations();
+
         jobFinished(params, false);
         return true;
     }
diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
index c5ca0bf..9069689 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
@@ -31,7 +31,6 @@
 import android.app.ActivityOptions;
 import android.app.PendingIntent;
 import android.companion.AssociationInfo;
-import android.companion.DeviceNotAssociatedException;
 import android.companion.IOnMessageReceivedListener;
 import android.companion.ISystemDataTransferCallback;
 import android.companion.datatransfer.PermissionSyncRequest;
@@ -56,7 +55,6 @@
 import com.android.server.companion.association.AssociationStore;
 import com.android.server.companion.transport.CompanionTransportManager;
 import com.android.server.companion.utils.PackageUtils;
-import com.android.server.companion.utils.PermissionsUtils;
 
 import java.util.List;
 import java.util.concurrent.ExecutorService;
@@ -120,28 +118,10 @@
     }
 
     /**
-     * Resolve the requested association, throwing if the caller doesn't have
-     * adequate permissions.
-     */
-    @NonNull
-    private AssociationInfo resolveAssociation(String packageName, int userId,
-            int associationId) {
-        AssociationInfo association = mAssociationStore.getAssociationById(associationId);
-        association = PermissionsUtils.sanitizeWithCallerChecks(mContext, association);
-        if (association == null) {
-            throw new DeviceNotAssociatedException("Association "
-                    + associationId + " is not associated with the app " + packageName
-                    + " for user " + userId);
-        }
-        return association;
-    }
-
-    /**
      * Return whether the user has consented to the permission transfer for the association.
      */
-    public boolean isPermissionTransferUserConsented(String packageName, @UserIdInt int userId,
-            int associationId) {
-        resolveAssociation(packageName, userId, associationId);
+    public boolean isPermissionTransferUserConsented(int associationId) {
+        mAssociationStore.getAssociationWithCallerChecks(associationId);
 
         PermissionSyncRequest request = getPermissionSyncRequest(associationId);
         if (request == null) {
@@ -167,7 +147,8 @@
             return null;
         }
 
-        final AssociationInfo association = resolveAssociation(packageName, userId, associationId);
+        final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
+                associationId);
 
         Slog.i(LOG_TAG, "Creating permission sync intent for userId [" + userId
                 + "] associationId [" + associationId + "]");
@@ -207,7 +188,7 @@
         Slog.i(LOG_TAG, "Start system data transfer for package [" + packageName
                 + "] userId [" + userId + "] associationId [" + associationId + "]");
 
-        final AssociationInfo association = resolveAssociation(packageName, userId, associationId);
+        mAssociationStore.getAssociationWithCallerChecks(associationId);
 
         // Check if the request has been consented by the user.
         PermissionSyncRequest request = getPermissionSyncRequest(associationId);
@@ -239,24 +220,20 @@
      * Enable perm sync for the association
      */
     public void enablePermissionsSync(int associationId) {
-        Binder.withCleanCallingIdentity(() -> {
-            int userId = mAssociationStore.getAssociationById(associationId).getUserId();
-            PermissionSyncRequest request = new PermissionSyncRequest(associationId);
-            request.setUserConsented(true);
-            mSystemDataTransferRequestStore.writeRequest(userId, request);
-        });
+        int userId = mAssociationStore.getAssociationWithCallerChecks(associationId).getUserId();
+        PermissionSyncRequest request = new PermissionSyncRequest(associationId);
+        request.setUserConsented(true);
+        mSystemDataTransferRequestStore.writeRequest(userId, request);
     }
 
     /**
      * Disable perm sync for the association
      */
     public void disablePermissionsSync(int associationId) {
-        Binder.withCleanCallingIdentity(() -> {
-            int userId = mAssociationStore.getAssociationById(associationId).getUserId();
-            PermissionSyncRequest request = new PermissionSyncRequest(associationId);
-            request.setUserConsented(false);
-            mSystemDataTransferRequestStore.writeRequest(userId, request);
-        });
+        int userId = mAssociationStore.getAssociationWithCallerChecks(associationId).getUserId();
+        PermissionSyncRequest request = new PermissionSyncRequest(associationId);
+        request.setUserConsented(false);
+        mSystemDataTransferRequestStore.writeRequest(userId, request);
     }
 
     /**
@@ -264,18 +241,17 @@
      */
     @Nullable
     public PermissionSyncRequest getPermissionSyncRequest(int associationId) {
-        return Binder.withCleanCallingIdentity(() -> {
-            int userId = mAssociationStore.getAssociationById(associationId).getUserId();
-            List<SystemDataTransferRequest> requests =
-                    mSystemDataTransferRequestStore.readRequestsByAssociationId(userId,
-                            associationId);
-            for (SystemDataTransferRequest request : requests) {
-                if (request instanceof PermissionSyncRequest) {
-                    return (PermissionSyncRequest) request;
-                }
+        int userId = mAssociationStore.getAssociationWithCallerChecks(associationId)
+                .getUserId();
+        List<SystemDataTransferRequest> requests =
+                mSystemDataTransferRequestStore.readRequestsByAssociationId(userId,
+                        associationId);
+        for (SystemDataTransferRequest request : requests) {
+            if (request instanceof PermissionSyncRequest) {
+                return (PermissionSyncRequest) request;
             }
-            return null;
-        });
+        }
+        return null;
     }
 
     /**
diff --git a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java
index c89ce11..9c37881 100644
--- a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java
+++ b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java
@@ -33,7 +33,7 @@
 import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_MATCH_LOST;
 import static android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_POWER;
 
-import static com.android.server.companion.presence.CompanionDevicePresenceMonitor.DEBUG;
+import static com.android.server.companion.presence.DevicePresenceProcessor.DEBUG;
 import static com.android.server.companion.utils.Utils.btDeviceToString;
 
 import static java.util.Objects.requireNonNull;
diff --git a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
index cb363a7..2d345c4 100644
--- a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
+++ b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
@@ -19,7 +19,7 @@
 import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED;
 import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED;
 
-import static com.android.server.companion.presence.CompanionDevicePresenceMonitor.DEBUG;
+import static com.android.server.companion.presence.DevicePresenceProcessor.DEBUG;
 import static com.android.server.companion.utils.Utils.btDeviceToString;
 
 import android.annotation.NonNull;
diff --git a/services/companion/java/com/android/server/companion/presence/CompanionAppBinder.java b/services/companion/java/com/android/server/companion/presence/CompanionAppBinder.java
new file mode 100644
index 0000000..b6348ea
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/presence/CompanionAppBinder.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.presence;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.UserIdInt;
+import android.companion.AssociationInfo;
+import android.companion.CompanionDeviceService;
+import android.companion.DevicePresenceEvent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Handler;
+import android.util.Pair;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.infra.PerUser;
+import com.android.server.companion.CompanionDeviceManagerService;
+import com.android.server.companion.utils.PackageUtils;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Manages communication with companion applications via
+ * {@link android.companion.ICompanionDeviceService} interface, including "connecting" (binding) to
+ * the services, maintaining the connection (the binding), and invoking callback methods such as
+ * {@link CompanionDeviceService#onDeviceAppeared(AssociationInfo)},
+ * {@link CompanionDeviceService#onDeviceDisappeared(AssociationInfo)} and
+ * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} in the
+ * application process.
+ *
+ * <p>
+ * The following is the list of the APIs provided by {@link CompanionAppBinder} (to be
+ * utilized by {@link CompanionDeviceManagerService}):
+ * <ul>
+ * <li> {@link #bindCompanionApp(int, String, boolean, CompanionServiceConnector.Listener)}
+ * <li> {@link #unbindCompanionApp(int, String)}
+ * <li> {@link #isCompanionApplicationBound(int, String)}
+ * <li> {@link #isRebindingCompanionApplicationScheduled(int, String)}
+ * </ul>
+ *
+ * @see CompanionDeviceService
+ * @see android.companion.ICompanionDeviceService
+ * @see CompanionServiceConnector
+ */
+@SuppressLint("LongLogTag")
+public class CompanionAppBinder {
+    private static final String TAG = "CDM_CompanionAppBinder";
+
+    private static final long REBIND_TIMEOUT = 10 * 1000; // 10 sec
+
+    @NonNull
+    private final Context mContext;
+    @NonNull
+    private final CompanionServicesRegister mCompanionServicesRegister;
+
+    @NonNull
+    @GuardedBy("mBoundCompanionApplications")
+    private final Map<Pair<Integer, String>, List<CompanionServiceConnector>>
+            mBoundCompanionApplications;
+    @NonNull
+    @GuardedBy("mScheduledForRebindingCompanionApplications")
+    private final Set<Pair<Integer, String>> mScheduledForRebindingCompanionApplications;
+
+    public CompanionAppBinder(@NonNull Context context) {
+        mContext = context;
+        mCompanionServicesRegister = new CompanionServicesRegister();
+        mBoundCompanionApplications = new HashMap<>();
+        mScheduledForRebindingCompanionApplications = new HashSet<>();
+    }
+
+    /**
+     * On package changed.
+     */
+    public void onPackagesChanged(@UserIdInt int userId) {
+        mCompanionServicesRegister.invalidate(userId);
+    }
+
+    /**
+     * CDM binds to the companion app.
+     */
+    public void bindCompanionApp(@UserIdInt int userId, @NonNull String packageName,
+            boolean isSelfManaged, CompanionServiceConnector.Listener listener) {
+        Slog.i(TAG, "Binding user=[" + userId + "], package=[" + packageName + "], isSelfManaged=["
+                + isSelfManaged + "]...");
+
+        final List<ComponentName> companionServices =
+                mCompanionServicesRegister.forPackage(userId, packageName);
+        if (companionServices.isEmpty()) {
+            Slog.e(TAG, "Can not bind companion applications u" + userId + "/" + packageName + ": "
+                    + "eligible CompanionDeviceService not found.\n"
+                    + "A CompanionDeviceService should declare an intent-filter for "
+                    + "\"android.companion.CompanionDeviceService\" action and require "
+                    + "\"android.permission.BIND_COMPANION_DEVICE_SERVICE\" permission.");
+            return;
+        }
+
+        final List<CompanionServiceConnector> serviceConnectors = new ArrayList<>();
+        synchronized (mBoundCompanionApplications) {
+            if (mBoundCompanionApplications.containsKey(new Pair<>(userId, packageName))) {
+                Slog.w(TAG, "The package is ALREADY bound.");
+                return;
+            }
+
+            for (int i = 0; i < companionServices.size(); i++) {
+                boolean isPrimary = i == 0;
+                serviceConnectors.add(CompanionServiceConnector.newInstance(mContext, userId,
+                        companionServices.get(i), isSelfManaged, isPrimary));
+            }
+
+            mBoundCompanionApplications.put(new Pair<>(userId, packageName), serviceConnectors);
+        }
+
+        // Set listeners for both Primary and Secondary connectors.
+        for (CompanionServiceConnector serviceConnector : serviceConnectors) {
+            serviceConnector.setListener(listener);
+        }
+
+        // Now "bind" all the connectors: the primary one and the rest of them.
+        for (CompanionServiceConnector serviceConnector : serviceConnectors) {
+            serviceConnector.connect();
+        }
+    }
+
+    /**
+     * CDM unbinds the companion app.
+     */
+    public void unbindCompanionApp(@UserIdInt int userId, @NonNull String packageName) {
+        Slog.i(TAG, "Unbinding user=[" + userId + "], package=[" + packageName + "]...");
+
+        final List<CompanionServiceConnector> serviceConnectors;
+
+        synchronized (mBoundCompanionApplications) {
+            serviceConnectors = mBoundCompanionApplications.remove(new Pair<>(userId, packageName));
+        }
+
+        synchronized (mScheduledForRebindingCompanionApplications) {
+            mScheduledForRebindingCompanionApplications.remove(new Pair<>(userId, packageName));
+        }
+
+        if (serviceConnectors == null) {
+            Slog.e(TAG, "The package is not bound.");
+            return;
+        }
+
+        for (CompanionServiceConnector serviceConnector : serviceConnectors) {
+            serviceConnector.postUnbind();
+        }
+    }
+
+    /**
+     * @return whether the companion application is bound now.
+     */
+    public boolean isCompanionApplicationBound(@UserIdInt int userId, @NonNull String packageName) {
+        synchronized (mBoundCompanionApplications) {
+            return mBoundCompanionApplications.containsKey(new Pair<>(userId, packageName));
+        }
+    }
+
+    /**
+     * Remove bound apps for package.
+     */
+    public void removePackage(int userId, String packageName) {
+        synchronized (mBoundCompanionApplications) {
+            mBoundCompanionApplications.remove(new Pair<>(userId, packageName));
+        }
+
+        synchronized (mScheduledForRebindingCompanionApplications) {
+            mScheduledForRebindingCompanionApplications.remove(new Pair<>(userId, packageName));
+        }
+    }
+
+    /**
+     * Schedule rebinding for the package.
+     */
+    public void scheduleRebinding(@UserIdInt int userId, @NonNull String packageName,
+            CompanionServiceConnector serviceConnector) {
+        Slog.i(TAG, "scheduleRebinding() " + userId + "/" + packageName);
+
+        if (isRebindingCompanionApplicationScheduled(userId, packageName)) {
+            Slog.i(TAG, "CompanionApplication rebinding has been scheduled, skipping "
+                    + serviceConnector.getComponentName());
+            return;
+        }
+
+        if (serviceConnector.isPrimary()) {
+            synchronized (mScheduledForRebindingCompanionApplications) {
+                mScheduledForRebindingCompanionApplications.add(new Pair<>(userId, packageName));
+            }
+        }
+
+        // Rebinding in 10 seconds.
+        Handler.getMain().postDelayed(() ->
+                        onRebindingCompanionApplicationTimeout(userId, packageName,
+                                serviceConnector),
+                REBIND_TIMEOUT);
+    }
+
+    private boolean isRebindingCompanionApplicationScheduled(
+            @UserIdInt int userId, @NonNull String packageName) {
+        synchronized (mScheduledForRebindingCompanionApplications) {
+            return mScheduledForRebindingCompanionApplications.contains(
+                    new Pair<>(userId, packageName));
+        }
+    }
+
+    private void onRebindingCompanionApplicationTimeout(
+            @UserIdInt int userId, @NonNull String packageName,
+            @NonNull CompanionServiceConnector serviceConnector) {
+        // Re-mark the application is bound.
+        if (serviceConnector.isPrimary()) {
+            synchronized (mBoundCompanionApplications) {
+                if (!mBoundCompanionApplications.containsKey(new Pair<>(userId, packageName))) {
+                    List<CompanionServiceConnector> serviceConnectors =
+                            Collections.singletonList(serviceConnector);
+                    mBoundCompanionApplications.put(new Pair<>(userId, packageName),
+                            serviceConnectors);
+                }
+            }
+
+            synchronized (mScheduledForRebindingCompanionApplications) {
+                mScheduledForRebindingCompanionApplications.remove(new Pair<>(userId, packageName));
+            }
+        }
+
+        serviceConnector.connect();
+    }
+
+    /**
+     * Dump bound apps.
+     */
+    public void dump(@NonNull PrintWriter out) {
+        out.append("Companion Device Application Controller: \n");
+
+        synchronized (mBoundCompanionApplications) {
+            out.append("  Bound Companion Applications: ");
+            if (mBoundCompanionApplications.isEmpty()) {
+                out.append("<empty>\n");
+            } else {
+                out.append("\n");
+                for (Map.Entry<Pair<Integer, String>, List<CompanionServiceConnector>> entry :
+                        mBoundCompanionApplications.entrySet()) {
+                    out.append("<u").append(String.valueOf(entry.getKey().first)).append(", ")
+                            .append(entry.getKey().second).append(">");
+                    for (CompanionServiceConnector serviceConnector : entry.getValue()) {
+                        out.append(", isPrimary=").append(
+                                String.valueOf(serviceConnector.isPrimary()));
+                    }
+                }
+            }
+        }
+
+        out.append("  Companion Applications Scheduled For Rebinding: ");
+        synchronized (mScheduledForRebindingCompanionApplications) {
+            if (mScheduledForRebindingCompanionApplications.isEmpty()) {
+                out.append("<empty>\n");
+            } else {
+                out.append("\n");
+                for (Pair<Integer, String> app : mScheduledForRebindingCompanionApplications) {
+                    out.append("<u").append(String.valueOf(app.first)).append(", ")
+                            .append(app.second).append(">");
+                }
+            }
+        }
+    }
+
+    @Nullable
+    CompanionServiceConnector getPrimaryServiceConnector(
+            @UserIdInt int userId, @NonNull String packageName) {
+        final List<CompanionServiceConnector> connectors;
+        synchronized (mBoundCompanionApplications) {
+            connectors = mBoundCompanionApplications.get(new Pair<>(userId, packageName));
+        }
+        return connectors != null ? connectors.get(0) : null;
+    }
+
+    private class CompanionServicesRegister extends PerUser<Map<String, List<ComponentName>>> {
+        @Override
+        public synchronized @NonNull Map<String, List<ComponentName>> forUser(
+                @UserIdInt int userId) {
+            return super.forUser(userId);
+        }
+
+        synchronized @NonNull List<ComponentName> forPackage(
+                @UserIdInt int userId, @NonNull String packageName) {
+            return forUser(userId).getOrDefault(packageName, Collections.emptyList());
+        }
+
+        synchronized void invalidate(@UserIdInt int userId) {
+            remove(userId);
+        }
+
+        @Override
+        protected final @NonNull Map<String, List<ComponentName>> create(@UserIdInt int userId) {
+            return PackageUtils.getCompanionServicesForUser(mContext, userId);
+        }
+    }
+}
diff --git a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
deleted file mode 100644
index 7a1a83f..0000000
--- a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
+++ /dev/null
@@ -1,620 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.companion.presence;
-
-import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED;
-import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED;
-import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED;
-import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED;
-import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED;
-import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED;
-import static android.os.Process.ROOT_UID;
-import static android.os.Process.SHELL_UID;
-
-import android.annotation.NonNull;
-import android.annotation.SuppressLint;
-import android.annotation.TestApi;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.companion.AssociationInfo;
-import android.content.Context;
-import android.os.Binder;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.ParcelUuid;
-import android.os.UserManager;
-import android.util.Log;
-import android.util.Slog;
-import android.util.SparseArray;
-import android.util.SparseBooleanArray;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.server.companion.association.AssociationStore;
-
-import java.io.PrintWriter;
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Class responsible for monitoring companion devices' "presence" status (i.e.
- * connected/disconnected for Bluetooth devices; nearby or not for BLE devices).
- *
- * <p>
- * Should only be used by
- * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService}
- * to which it provides the following API:
- * <ul>
- * <li> {@link #onSelfManagedDeviceConnected(int)}
- * <li> {@link #onSelfManagedDeviceDisconnected(int)}
- * <li> {@link #isDevicePresent(int)}
- * <li> {@link Callback#onDeviceAppeared(int) Callback.onDeviceAppeared(int)}
- * <li> {@link Callback#onDeviceDisappeared(int) Callback.onDeviceDisappeared(int)}
- * <li> {@link Callback#onDevicePresenceEvent(int, int)}}
- * </ul>
- */
-@SuppressLint("LongLogTag")
-public class CompanionDevicePresenceMonitor implements AssociationStore.OnChangeListener,
-        BluetoothCompanionDeviceConnectionListener.Callback, BleCompanionDeviceScanner.Callback {
-    static final boolean DEBUG = false;
-    private static final String TAG = "CDM_CompanionDevicePresenceMonitor";
-
-    /** Callback for notifying about changes to status of companion devices. */
-    public interface Callback {
-        /** Invoked when companion device is found nearby or connects. */
-        void onDeviceAppeared(int associationId);
-
-        /** Invoked when a companion device no longer seen nearby or disconnects. */
-        void onDeviceDisappeared(int associationId);
-
-        /** Invoked when device has corresponding event changes. */
-        void onDevicePresenceEvent(int associationId, int event);
-
-        /** Invoked when device has corresponding event changes base on the UUID */
-        void onDevicePresenceEventByUuid(ObservableUuid uuid, int event);
-    }
-
-    private final @NonNull AssociationStore mAssociationStore;
-    private final @NonNull ObservableUuidStore mObservableUuidStore;
-    private final @NonNull Callback mCallback;
-    private final @NonNull BluetoothCompanionDeviceConnectionListener mBtConnectionListener;
-    private final @NonNull BleCompanionDeviceScanner mBleScanner;
-
-    // NOTE: Same association may appear in more than one of the following sets at the same time.
-    // (E.g. self-managed devices that have MAC addresses, could be reported as present by their
-    // companion applications, while at the same be connected via BT, or detected nearby by BLE
-    // scanner)
-    private final @NonNull Set<Integer> mConnectedBtDevices = new HashSet<>();
-    private final @NonNull Set<Integer> mNearbyBleDevices = new HashSet<>();
-    private final @NonNull Set<Integer> mReportedSelfManagedDevices = new HashSet<>();
-    private final @NonNull Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>();
-    @GuardedBy("mBtDisconnectedDevices")
-    private final @NonNull Set<Integer> mBtDisconnectedDevices = new HashSet<>();
-
-    // A map to track device presence within 10 seconds of Bluetooth disconnection.
-    // The key is the association ID, and the boolean value indicates if the device
-    // was detected again within that time frame.
-    @GuardedBy("mBtDisconnectedDevices")
-    private final @NonNull SparseBooleanArray mBtDisconnectedDevicesBlePresence =
-            new SparseBooleanArray();
-
-    // Tracking "simulated" presence. Used for debugging and testing only.
-    private final @NonNull Set<Integer> mSimulated = new HashSet<>();
-    private final SimulatedDevicePresenceSchedulerHelper mSchedulerHelper =
-            new SimulatedDevicePresenceSchedulerHelper();
-
-    private final BleDeviceDisappearedScheduler mBleDeviceDisappearedScheduler =
-            new BleDeviceDisappearedScheduler();
-
-    public CompanionDevicePresenceMonitor(UserManager userManager,
-            @NonNull AssociationStore associationStore,
-            @NonNull ObservableUuidStore observableUuidStore, @NonNull Callback callback) {
-        mAssociationStore = associationStore;
-        mObservableUuidStore = observableUuidStore;
-        mCallback = callback;
-        mBtConnectionListener = new BluetoothCompanionDeviceConnectionListener(userManager,
-                associationStore, mObservableUuidStore,
-                /* BluetoothCompanionDeviceConnectionListener.Callback */ this);
-        mBleScanner = new BleCompanionDeviceScanner(associationStore,
-                /* BleCompanionDeviceScanner.Callback */ this);
-    }
-
-    /** Initialize {@link CompanionDevicePresenceMonitor} */
-    public void init(Context context) {
-        if (DEBUG) Log.i(TAG, "init()");
-
-        final BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
-        if (btAdapter != null) {
-            mBtConnectionListener.init(btAdapter);
-            mBleScanner.init(context, btAdapter);
-        } else {
-            Log.w(TAG, "BluetoothAdapter is NOT available.");
-        }
-
-        mAssociationStore.registerLocalListener(this);
-    }
-
-    /**
-     * @return current connected UUID devices.
-     */
-    public Set<ParcelUuid> getCurrentConnectedUuidDevices() {
-        return mConnectedUuidDevices;
-    }
-
-    /**
-     * Remove current connected UUID device.
-     */
-    public void removeCurrentConnectedUuidDevice(ParcelUuid uuid) {
-        mConnectedUuidDevices.remove(uuid);
-    }
-
-    /**
-     * @return whether the associated companion devices is present. I.e. device is nearby (for BLE);
-     *         or devices is connected (for Bluetooth); or reported (by the application) to be
-     *         nearby (for "self-managed" associations).
-     */
-    public boolean isDevicePresent(int associationId) {
-        return mReportedSelfManagedDevices.contains(associationId)
-                || mConnectedBtDevices.contains(associationId)
-                || mNearbyBleDevices.contains(associationId)
-                || mSimulated.contains(associationId);
-    }
-
-    /**
-     * @return whether the current uuid to be observed is present.
-     */
-    public boolean isDeviceUuidPresent(ParcelUuid uuid) {
-        return mConnectedUuidDevices.contains(uuid);
-    }
-
-    /**
-     * @return whether the current device is BT connected and had already reported to the app.
-     */
-
-    public boolean isBtConnected(int associationId) {
-        return mConnectedBtDevices.contains(associationId);
-    }
-
-    /**
-     * @return whether the current device in BLE range and had already reported to the app.
-     */
-    public boolean isBlePresent(int associationId) {
-        return mNearbyBleDevices.contains(associationId);
-    }
-
-    /**
-     * @return whether the current device had been already reported by the simulator.
-     */
-    public boolean isSimulatePresent(int associationId) {
-        return mSimulated.contains(associationId);
-    }
-
-    /**
-     * Marks a "self-managed" device as connected.
-     *
-     * <p>
-     * Must ONLY be invoked by the
-     * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService}
-     * when an application invokes
-     * {@link android.companion.CompanionDeviceManager#notifyDeviceAppeared(int) notifyDeviceAppeared()}
-     */
-    public void onSelfManagedDeviceConnected(int associationId) {
-        onDevicePresenceEvent(mReportedSelfManagedDevices,
-                associationId, EVENT_SELF_MANAGED_APPEARED);
-    }
-
-    /**
-     * Marks a "self-managed" device as disconnected.
-     *
-     * <p>
-     * Must ONLY be invoked by the
-     * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService}
-     * when an application invokes
-     * {@link android.companion.CompanionDeviceManager#notifyDeviceDisappeared(int) notifyDeviceDisappeared()}
-     */
-    public void onSelfManagedDeviceDisconnected(int associationId) {
-        onDevicePresenceEvent(mReportedSelfManagedDevices,
-                associationId, EVENT_SELF_MANAGED_DISAPPEARED);
-    }
-
-    /**
-     * Marks a "self-managed" device as disconnected when binderDied.
-     */
-    public void onSelfManagedDeviceReporterBinderDied(int associationId) {
-        onDevicePresenceEvent(mReportedSelfManagedDevices,
-                associationId, EVENT_SELF_MANAGED_DISAPPEARED);
-    }
-
-    @Override
-    public void onBluetoothCompanionDeviceConnected(int associationId) {
-        synchronized (mBtDisconnectedDevices) {
-            // A device is considered reconnected within 10 seconds if a pending BLE lost report is
-            // followed by a detected Bluetooth connection.
-            boolean isReconnected = mBtDisconnectedDevices.contains(associationId);
-            if (isReconnected) {
-                Slog.i(TAG, "Device ( " + associationId + " ) is reconnected within 10s.");
-                mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId);
-            }
-
-            Slog.i(TAG, "onBluetoothCompanionDeviceConnected: "
-                    + "associationId( " + associationId + " )");
-            onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED);
-
-            // Stop the BLE scan if all devices report BT connected status and BLE was present.
-            if (canStopBleScan()) {
-                mBleScanner.stopScanIfNeeded();
-            }
-
-        }
-    }
-
-    @Override
-    public void onBluetoothCompanionDeviceDisconnected(int associationId) {
-        Slog.i(TAG, "onBluetoothCompanionDeviceDisconnected "
-                + "associationId( " + associationId + " )");
-        // Start BLE scanning when the device is disconnected.
-        mBleScanner.startScan();
-
-        onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_DISCONNECTED);
-        // If current device is BLE present but BT is disconnected , means it will be
-        // potentially out of range later. Schedule BLE disappeared callback.
-        if (isBlePresent(associationId)) {
-            synchronized (mBtDisconnectedDevices) {
-                mBtDisconnectedDevices.add(associationId);
-            }
-            mBleDeviceDisappearedScheduler.scheduleBleDeviceDisappeared(associationId);
-        }
-    }
-
-    @Override
-    public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) {
-        final ParcelUuid parcelUuid = uuid.getUuid();
-
-        switch(event) {
-            case EVENT_BT_CONNECTED:
-                boolean added = mConnectedUuidDevices.add(parcelUuid);
-
-                if (!added) {
-                    Slog.w(TAG, "Uuid= " + parcelUuid + "is ALREADY reported as "
-                            + "present by this event=" + event);
-                }
-
-                break;
-            case EVENT_BT_DISCONNECTED:
-                final boolean removed = mConnectedUuidDevices.remove(parcelUuid);
-
-                if (!removed) {
-                    Slog.w(TAG, "UUID= " + parcelUuid + " was NOT reported "
-                            + "as present by this event= " + event);
-
-                    return;
-                }
-
-                break;
-        }
-
-        mCallback.onDevicePresenceEventByUuid(uuid, event);
-    }
-
-
-    @Override
-    public void onBleCompanionDeviceFound(int associationId) {
-        onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED);
-        synchronized (mBtDisconnectedDevices) {
-            final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get(associationId);
-            if (mBtDisconnectedDevices.contains(associationId) && isCurrentPresent) {
-                mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId);
-            }
-        }
-    }
-
-    @Override
-    public void onBleCompanionDeviceLost(int associationId) {
-        onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED);
-    }
-
-    /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
-    @TestApi
-    public void simulateDeviceEvent(int associationId, int event) {
-        // IMPORTANT: this API should only be invoked via the
-        // 'companiondevice simulate-device-appeared' Shell command, so the only uid-s allowed to
-        // make this call are SHELL and ROOT.
-        // No other caller (including SYSTEM!) should be allowed.
-        enforceCallerShellOrRoot();
-        // Make sure the association exists.
-        enforceAssociationExists(associationId);
-
-        switch (event) {
-            case EVENT_BLE_APPEARED:
-                simulateDeviceAppeared(associationId, event);
-                break;
-            case EVENT_BT_CONNECTED:
-                onBluetoothCompanionDeviceConnected(associationId);
-                break;
-            case EVENT_BLE_DISAPPEARED:
-                simulateDeviceDisappeared(associationId, event);
-                break;
-            case EVENT_BT_DISCONNECTED:
-                onBluetoothCompanionDeviceDisconnected(associationId);
-                break;
-            default:
-                throw new IllegalArgumentException("Event: " + event + "is not supported");
-        }
-    }
-
-    /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
-    @TestApi
-    public void simulateDeviceEventByUuid(ObservableUuid uuid, int event) {
-        // IMPORTANT: this API should only be invoked via the
-        // 'companiondevice simulate-device-uuid-events' Shell command, so the only uid-s allowed to
-        // make this call are SHELL and ROOT.
-        // No other caller (including SYSTEM!) should be allowed.
-        enforceCallerShellOrRoot();
-        onDevicePresenceEventByUuid(uuid, event);
-    }
-
-    private void simulateDeviceAppeared(int associationId, int state) {
-        onDevicePresenceEvent(mSimulated, associationId, state);
-        mSchedulerHelper.scheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId);
-    }
-
-    private void simulateDeviceDisappeared(int associationId, int state) {
-        mSchedulerHelper.unscheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId);
-        onDevicePresenceEvent(mSimulated, associationId, state);
-    }
-
-    private void enforceAssociationExists(int associationId) {
-        if (mAssociationStore.getAssociationById(associationId) == null) {
-            throw new IllegalArgumentException(
-                    "Association with id " + associationId + " does not exist.");
-        }
-    }
-
-    private void onDevicePresenceEvent(@NonNull Set<Integer> presentDevicesForSource,
-            int associationId, int event) {
-        Slog.i(TAG, "onDevicePresenceEvent() id=" + associationId + ", event=" + event);
-
-        switch (event) {
-            case EVENT_BLE_APPEARED:
-                synchronized (mBtDisconnectedDevices) {
-                    // If a BLE device is detected within 10 seconds after BT is disconnected,
-                    // flag it as BLE is present.
-                    if (mBtDisconnectedDevices.contains(associationId)) {
-                        Slog.i(TAG, "Device ( " + associationId + " ) is present,"
-                                + " do not need to send the callback with event ( "
-                                + EVENT_BLE_APPEARED + " ).");
-                        mBtDisconnectedDevicesBlePresence.append(associationId, true);
-                    }
-                }
-            case EVENT_BT_CONNECTED:
-            case EVENT_SELF_MANAGED_APPEARED:
-                final boolean added = presentDevicesForSource.add(associationId);
-
-                if (!added) {
-                    Slog.w(TAG, "Association with id "
-                            + associationId + " is ALREADY reported as "
-                            + "present by this source, event=" + event);
-                }
-
-                mCallback.onDeviceAppeared(associationId);
-
-                break;
-            case EVENT_BLE_DISAPPEARED:
-            case EVENT_BT_DISCONNECTED:
-            case EVENT_SELF_MANAGED_DISAPPEARED:
-                final boolean removed = presentDevicesForSource.remove(associationId);
-
-                if (!removed) {
-                    Slog.w(TAG, "Association with id " + associationId + " was NOT reported "
-                            + "as present by this source, event= " + event);
-
-                    return;
-                }
-
-                mCallback.onDeviceDisappeared(associationId);
-
-                break;
-            default:
-                Slog.e(TAG, "Event: " + event + " is not supported");
-                return;
-        }
-
-        mCallback.onDevicePresenceEvent(associationId, event);
-    }
-
-    /**
-     * Implements
-     * {@link AssociationStore.OnChangeListener#onAssociationRemoved(AssociationInfo)}
-     */
-    @Override
-    public void onAssociationRemoved(@NonNull AssociationInfo association) {
-        final int id = association.getId();
-        if (DEBUG) {
-            Log.i(TAG, "onAssociationRemoved() id=" + id);
-            Log.d(TAG, "  > association=" + association);
-        }
-
-        mConnectedBtDevices.remove(id);
-        mNearbyBleDevices.remove(id);
-        mReportedSelfManagedDevices.remove(id);
-        mSimulated.remove(id);
-        mBtDisconnectedDevices.remove(id);
-        mBtDisconnectedDevicesBlePresence.delete(id);
-
-        // Do NOT call mCallback.onDeviceDisappeared()!
-        // CompanionDeviceManagerService will know that the association is removed, and will do
-        // what's needed.
-    }
-
-    /**
-     * Return a set of devices that pending to report connectivity
-     */
-    public SparseArray<Set<BluetoothDevice>> getPendingConnectedDevices() {
-        synchronized (mBtConnectionListener.mPendingConnectedDevices) {
-            return mBtConnectionListener.mPendingConnectedDevices;
-        }
-    }
-
-    private static void enforceCallerShellOrRoot() {
-        final int callingUid = Binder.getCallingUid();
-        if (callingUid == SHELL_UID || callingUid == ROOT_UID) return;
-
-        throw new SecurityException("Caller is neither Shell nor Root");
-    }
-
-    /**
-     * The BLE scan can be only stopped if all the devices have been reported
-     * BT connected and BLE presence and are not pending to report BLE lost.
-     */
-    private boolean canStopBleScan() {
-        for (AssociationInfo ai : mAssociationStore.getActiveAssociations()) {
-            int id = ai.getId();
-            synchronized (mBtDisconnectedDevices) {
-                if (ai.isNotifyOnDeviceNearby() && !(isBtConnected(id)
-                        && isBlePresent(id) && mBtDisconnectedDevices.isEmpty())) {
-                    Slog.i(TAG, "The BLE scan cannot be stopped, "
-                            + "device( " + id + " ) is not yet connected "
-                            + "OR the BLE is not current present Or is pending to report BLE lost");
-                    return false;
-                }
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Dumps system information about devices that are marked as "present".
-     */
-    public void dump(@NonNull PrintWriter out) {
-        out.append("Companion Device Present: ");
-        if (mConnectedBtDevices.isEmpty()
-                && mNearbyBleDevices.isEmpty()
-                && mReportedSelfManagedDevices.isEmpty()) {
-            out.append("<empty>\n");
-            return;
-        } else {
-            out.append("\n");
-        }
-
-        out.append("  Connected Bluetooth Devices: ");
-        if (mConnectedBtDevices.isEmpty()) {
-            out.append("<empty>\n");
-        } else {
-            out.append("\n");
-            for (int associationId : mConnectedBtDevices) {
-                AssociationInfo a = mAssociationStore.getAssociationById(associationId);
-                out.append("    ").append(a.toShortString()).append('\n');
-            }
-        }
-
-        out.append("  Nearby BLE Devices: ");
-        if (mNearbyBleDevices.isEmpty()) {
-            out.append("<empty>\n");
-        } else {
-            out.append("\n");
-            for (int associationId : mNearbyBleDevices) {
-                AssociationInfo a = mAssociationStore.getAssociationById(associationId);
-                out.append("    ").append(a.toShortString()).append('\n');
-            }
-        }
-
-        out.append("  Self-Reported Devices: ");
-        if (mReportedSelfManagedDevices.isEmpty()) {
-            out.append("<empty>\n");
-        } else {
-            out.append("\n");
-            for (int associationId : mReportedSelfManagedDevices) {
-                AssociationInfo a = mAssociationStore.getAssociationById(associationId);
-                out.append("    ").append(a.toShortString()).append('\n');
-            }
-        }
-    }
-
-    private class SimulatedDevicePresenceSchedulerHelper extends Handler {
-        SimulatedDevicePresenceSchedulerHelper() {
-            super(Looper.getMainLooper());
-        }
-
-        void scheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) {
-            // First, unschedule if it was scheduled previously.
-            if (hasMessages(/* what */ associationId)) {
-                removeMessages(/* what */ associationId);
-            }
-
-            sendEmptyMessageDelayed(/* what */ associationId, 60 * 1000 /* 60 seconds */);
-        }
-
-        void unscheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) {
-            removeMessages(/* what */ associationId);
-        }
-
-        @Override
-        public void handleMessage(@NonNull Message msg) {
-            final int associationId = msg.what;
-            if (mSimulated.contains(associationId)) {
-                onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_DISAPPEARED);
-            }
-        }
-    }
-
-    private class BleDeviceDisappearedScheduler extends Handler {
-        BleDeviceDisappearedScheduler() {
-            super(Looper.getMainLooper());
-        }
-
-        void scheduleBleDeviceDisappeared(int associationId) {
-            if (hasMessages(associationId)) {
-                removeMessages(associationId);
-            }
-            Slog.i(TAG, "scheduleBleDeviceDisappeared for Device: ( " + associationId + " ).");
-            sendEmptyMessageDelayed(associationId,  10 * 1000 /* 10 seconds */);
-        }
-
-        void unScheduleDeviceDisappeared(int associationId) {
-            if (hasMessages(associationId)) {
-                Slog.i(TAG, "unScheduleDeviceDisappeared for Device( " + associationId + " )");
-                synchronized (mBtDisconnectedDevices) {
-                    mBtDisconnectedDevices.remove(associationId);
-                    mBtDisconnectedDevicesBlePresence.delete(associationId);
-                }
-
-                removeMessages(associationId);
-            }
-        }
-
-        @Override
-        public void handleMessage(@NonNull Message msg) {
-            final int associationId = msg.what;
-            synchronized (mBtDisconnectedDevices) {
-                final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get(
-                        associationId);
-                // If a device hasn't reported after 10 seconds and is not currently present,
-                // assume BLE is lost and trigger the onDeviceEvent callback with the
-                // EVENT_BLE_DISAPPEARED event.
-                if (mBtDisconnectedDevices.contains(associationId)
-                        && !isCurrentPresent) {
-                    Slog.i(TAG, "Device ( " + associationId + " ) is likely BLE out of range, "
-                            + "sending callback with event ( " + EVENT_BLE_DISAPPEARED + " )");
-                    onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED);
-                }
-
-                mBtDisconnectedDevices.remove(associationId);
-                mBtDisconnectedDevicesBlePresence.delete(associationId);
-            }
-        }
-    }
-}
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java b/services/companion/java/com/android/server/companion/presence/CompanionServiceConnector.java
similarity index 83%
rename from services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java
rename to services/companion/java/com/android/server/companion/presence/CompanionServiceConnector.java
index 5abdb42..c01c319 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java
+++ b/services/companion/java/com/android/server/companion/presence/CompanionServiceConnector.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.companion;
+package com.android.server.companion.presence;
 
 import static android.content.Context.BIND_ALMOST_PERCEPTIBLE;
 import static android.content.Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE;
@@ -33,36 +33,42 @@
 import android.content.Intent;
 import android.os.Handler;
 import android.os.IBinder;
-import android.util.Log;
+import android.util.Slog;
 
 import com.android.internal.infra.ServiceConnector;
 import com.android.server.ServiceThread;
+import com.android.server.companion.CompanionDeviceManagerService;
 
 /**
  * Manages a connection (binding) to an instance of {@link CompanionDeviceService} running in the
  * application process.
  */
 @SuppressLint("LongLogTag")
-class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDeviceService> {
+public class CompanionServiceConnector extends ServiceConnector.Impl<ICompanionDeviceService> {
+
+    /** Listener for changes to the state of the {@link CompanionServiceConnector}  */
+    public interface Listener {
+        /**
+         * Called when service binding is died.
+         */
+        void onBindingDied(@UserIdInt int userId, @NonNull String packageName,
+                @NonNull CompanionServiceConnector serviceConnector);
+    }
+
     private static final String TAG = "CDM_CompanionServiceConnector";
-    private static final boolean DEBUG = false;
 
     /* Unbinding before executing the callbacks can cause problems. Wait 5-seconds before unbind. */
     private static final long UNBIND_POST_DELAY_MS = 5_000;
-
-    /** Listener for changes to the state of the {@link CompanionDeviceServiceConnector}  */
-    interface Listener {
-        void onBindingDied(@UserIdInt int userId, @NonNull String packageName,
-                @NonNull CompanionDeviceServiceConnector serviceConnector);
-    }
-
-    private final @UserIdInt int mUserId;
-    private final @NonNull ComponentName mComponentName;
+    @UserIdInt
+    private final int mUserId;
+    @NonNull
+    private final ComponentName mComponentName;
+    private final boolean mIsPrimary;
     // IMPORTANT: this can (and will!) be null (at the moment, CompanionApplicationController only
     // installs a listener to the primary ServiceConnector), hence we should always null-check the
     // reference before calling on it.
-    private @Nullable Listener mListener;
-    private boolean mIsPrimary;
+    @Nullable
+    private Listener mListener;
 
     /**
      * Create a CompanionDeviceServiceConnector instance.
@@ -79,16 +85,16 @@
      * IMPORTANCE_FOREGROUND_SERVICE = 125. In order to kill the one time permission session, the
      * service importance level should be higher than 125.
      */
-    static CompanionDeviceServiceConnector newInstance(@NonNull Context context,
+    static CompanionServiceConnector newInstance(@NonNull Context context,
             @UserIdInt int userId, @NonNull ComponentName componentName, boolean isSelfManaged,
             boolean isPrimary) {
         final int bindingFlags = isSelfManaged ? BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE
                 : BIND_ALMOST_PERCEPTIBLE;
-        return new CompanionDeviceServiceConnector(
+        return new CompanionServiceConnector(
                 context, userId, componentName, bindingFlags, isPrimary);
     }
 
-    private CompanionDeviceServiceConnector(@NonNull Context context, @UserIdInt int userId,
+    private CompanionServiceConnector(@NonNull Context context, @UserIdInt int userId,
             @NonNull ComponentName componentName, int bindingFlags, boolean isPrimary) {
         super(context, buildIntent(componentName), bindingFlags, userId, null);
         mUserId = userId;
@@ -133,6 +139,7 @@
         return mIsPrimary;
     }
 
+    @NonNull
     ComponentName getComponentName() {
         return mComponentName;
     }
@@ -140,17 +147,15 @@
     @Override
     protected void onServiceConnectionStatusChanged(
             @NonNull ICompanionDeviceService service, boolean isConnected) {
-        if (DEBUG) {
-            Log.d(TAG, "onServiceConnection_StatusChanged() " + mComponentName.toShortString()
-                    + " connected=" + isConnected);
-        }
+        Slog.d(TAG, "onServiceConnectionStatusChanged() " + mComponentName.toShortString()
+                + " connected=" + isConnected);
     }
 
     @Override
     public void binderDied() {
         super.binderDied();
 
-        if (DEBUG) Log.d(TAG, "binderDied() " + mComponentName.toShortString());
+        Slog.d(TAG, "binderDied() " + mComponentName.toShortString());
 
         // Handle primary process being killed
         if (mListener != null) {
@@ -172,7 +177,8 @@
      * within system_server and thus tends to get heavily congested)
      */
     @Override
-    protected @NonNull Handler getJobHandler() {
+    @NonNull
+    protected Handler getJobHandler() {
         return getServiceThread().getThreadHandler();
     }
 
@@ -182,12 +188,14 @@
         return -1;
     }
 
-    private static @NonNull Intent buildIntent(@NonNull ComponentName componentName) {
+    @NonNull
+    private static Intent buildIntent(@NonNull ComponentName componentName) {
         return new Intent(CompanionDeviceService.SERVICE_INTERFACE)
                 .setComponent(componentName);
     }
 
-    private static @NonNull ServiceThread getServiceThread() {
+    @NonNull
+    private static ServiceThread getServiceThread() {
         if (sServiceThread == null) {
             synchronized (CompanionDeviceManagerService.class) {
                 if (sServiceThread == null) {
@@ -206,5 +214,6 @@
      * <p>
      *  Do NOT reference directly, use {@link #getServiceThread()} method instead.
      */
-    private static volatile @Nullable ServiceThread sServiceThread;
+    @Nullable
+    private static volatile ServiceThread sServiceThread;
 }
diff --git a/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java b/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java
new file mode 100644
index 0000000..642460e
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java
@@ -0,0 +1,1042 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.presence;
+
+import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
+import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED;
+import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED;
+import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED;
+import static android.companion.DevicePresenceEvent.NO_ASSOCIATION;
+import static android.os.Process.ROOT_UID;
+import static android.os.Process.SHELL_UID;
+
+import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage;
+import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanObserveDevicePresenceByUuid;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
+import android.annotation.UserIdInt;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.companion.AssociationInfo;
+import android.companion.DeviceNotAssociatedException;
+import android.companion.DevicePresenceEvent;
+import android.companion.ObservingDevicePresenceRequest;
+import android.content.Context;
+import android.hardware.power.Mode;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ParcelUuid;
+import android.os.PowerManagerInternal;
+import android.os.RemoteException;
+import android.os.UserManager;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.companion.association.AssociationStore;
+
+import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Class responsible for monitoring companion devices' "presence" status (i.e.
+ * connected/disconnected for Bluetooth devices; nearby or not for BLE devices).
+ *
+ * <p>
+ * Should only be used by
+ * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService}
+ * to which it provides the following API:
+ * <ul>
+ * <li> {@link #onSelfManagedDeviceConnected(int)}
+ * <li> {@link #onSelfManagedDeviceDisconnected(int)}
+ * <li> {@link #isDevicePresent(int)}
+ * </ul>
+ */
+@SuppressLint("LongLogTag")
+public class DevicePresenceProcessor implements AssociationStore.OnChangeListener,
+        BluetoothCompanionDeviceConnectionListener.Callback, BleCompanionDeviceScanner.Callback {
+    static final boolean DEBUG = false;
+    private static final String TAG = "CDM_DevicePresenceProcessor";
+
+    @NonNull
+    private final Context mContext;
+    @NonNull
+    private final CompanionAppBinder mCompanionAppBinder;
+    @NonNull
+    private final AssociationStore mAssociationStore;
+    @NonNull
+    private final ObservableUuidStore mObservableUuidStore;
+    @NonNull
+    private final BluetoothCompanionDeviceConnectionListener mBtConnectionListener;
+    @NonNull
+    private final BleCompanionDeviceScanner mBleScanner;
+    @NonNull
+    private final PowerManagerInternal mPowerManagerInternal;
+
+    // NOTE: Same association may appear in more than one of the following sets at the same time.
+    // (E.g. self-managed devices that have MAC addresses, could be reported as present by their
+    // companion applications, while at the same be connected via BT, or detected nearby by BLE
+    // scanner)
+    @NonNull
+    private final Set<Integer> mConnectedBtDevices = new HashSet<>();
+    @NonNull
+    private final Set<Integer> mNearbyBleDevices = new HashSet<>();
+    @NonNull
+    private final Set<Integer> mReportedSelfManagedDevices = new HashSet<>();
+    @NonNull
+    private final Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>();
+    @NonNull
+    @GuardedBy("mBtDisconnectedDevices")
+    private final Set<Integer> mBtDisconnectedDevices = new HashSet<>();
+
+    // A map to track device presence within 10 seconds of Bluetooth disconnection.
+    // The key is the association ID, and the boolean value indicates if the device
+    // was detected again within that time frame.
+    @GuardedBy("mBtDisconnectedDevices")
+    private final @NonNull SparseBooleanArray mBtDisconnectedDevicesBlePresence =
+            new SparseBooleanArray();
+
+    // Tracking "simulated" presence. Used for debugging and testing only.
+    private final @NonNull Set<Integer> mSimulated = new HashSet<>();
+    private final SimulatedDevicePresenceSchedulerHelper mSchedulerHelper =
+            new SimulatedDevicePresenceSchedulerHelper();
+
+    private final BleDeviceDisappearedScheduler mBleDeviceDisappearedScheduler =
+            new BleDeviceDisappearedScheduler();
+
+    public DevicePresenceProcessor(@NonNull Context context,
+            @NonNull CompanionAppBinder companionAppBinder,
+            UserManager userManager,
+            @NonNull AssociationStore associationStore,
+            @NonNull ObservableUuidStore observableUuidStore,
+            @NonNull PowerManagerInternal powerManagerInternal) {
+        mContext = context;
+        mCompanionAppBinder = companionAppBinder;
+        mAssociationStore = associationStore;
+        mObservableUuidStore = observableUuidStore;
+        mBtConnectionListener = new BluetoothCompanionDeviceConnectionListener(userManager,
+                associationStore, mObservableUuidStore,
+                /* BluetoothCompanionDeviceConnectionListener.Callback */ this);
+        mBleScanner = new BleCompanionDeviceScanner(associationStore,
+                /* BleCompanionDeviceScanner.Callback */ this);
+        mPowerManagerInternal = powerManagerInternal;
+    }
+
+    /** Initialize {@link DevicePresenceProcessor} */
+    public void init(Context context) {
+        if (DEBUG) Slog.i(TAG, "init()");
+
+        final BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
+        if (btAdapter != null) {
+            mBtConnectionListener.init(btAdapter);
+            mBleScanner.init(context, btAdapter);
+        } else {
+            Slog.w(TAG, "BluetoothAdapter is NOT available.");
+        }
+
+        mAssociationStore.registerLocalListener(this);
+    }
+
+    /**
+     * Process device presence start request.
+     */
+    public void startObservingDevicePresence(ObservingDevicePresenceRequest request,
+            String packageName, int userId) {
+        Slog.i(TAG,
+                "Start observing request=[" + request + "] for userId=[" + userId + "], package=["
+                        + packageName + "]...");
+        final ParcelUuid requestUuid = request.getUuid();
+
+        if (requestUuid != null) {
+            enforceCallerCanObserveDevicePresenceByUuid(mContext);
+
+            // If it's already being observed, then no-op.
+            if (mObservableUuidStore.isUuidBeingObserved(requestUuid, userId, packageName)) {
+                Slog.i(TAG, "UUID=[" + requestUuid + "], package=[" + packageName + "], userId=["
+                        + userId + "] is already being observed.");
+                return;
+            }
+
+            final ObservableUuid observableUuid = new ObservableUuid(userId, requestUuid,
+                    packageName, System.currentTimeMillis());
+            mObservableUuidStore.writeObservableUuid(userId, observableUuid);
+        } else {
+            final int associationId = request.getAssociationId();
+            AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
+                    associationId);
+
+            // If it's already being observed, then no-op.
+            if (association.isNotifyOnDeviceNearby()) {
+                Slog.i(TAG, "Associated device id=[" + association.getId()
+                        + "] is already being observed. No-op.");
+                return;
+            }
+
+            association = (new AssociationInfo.Builder(association)).setNotifyOnDeviceNearby(true)
+                    .build();
+            mAssociationStore.updateAssociation(association);
+
+            // Send callback immediately if the device is present.
+            if (isDevicePresent(associationId)) {
+                Slog.i(TAG, "Device is already present. Triggering callback.");
+                if (isBlePresent(associationId)) {
+                    onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED);
+                } else if (isBtConnected(associationId)) {
+                    onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED);
+                } else if (isSimulatePresent(associationId)) {
+                    onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_APPEARED);
+                }
+            }
+        }
+
+        Slog.i(TAG, "Registered device presence listener.");
+    }
+
+    /**
+     * Process device presence stop request.
+     */
+    public void stopObservingDevicePresence(ObservingDevicePresenceRequest request,
+            String packageName, int userId) {
+        Slog.i(TAG,
+                "Stop observing request=[" + request + "] for userId=[" + userId + "], package=["
+                        + packageName + "]...");
+
+        final ParcelUuid requestUuid = request.getUuid();
+
+        if (requestUuid != null) {
+            enforceCallerCanObserveDevicePresenceByUuid(mContext);
+
+            if (!mObservableUuidStore.isUuidBeingObserved(requestUuid, userId, packageName)) {
+                Slog.i(TAG, "UUID=[" + requestUuid + "], package=[" + packageName + "], userId=["
+                        + userId + "] is already not being observed.");
+                return;
+            }
+
+            mObservableUuidStore.removeObservableUuid(userId, requestUuid, packageName);
+            removeCurrentConnectedUuidDevice(requestUuid);
+        } else {
+            final int associationId = request.getAssociationId();
+            AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
+                    associationId);
+
+            // If it's already being observed, then no-op.
+            if (!association.isNotifyOnDeviceNearby()) {
+                Slog.i(TAG, "Associated device id=[" + association.getId()
+                        + "] is already not being observed. No-op.");
+                return;
+            }
+
+            association = (new AssociationInfo.Builder(association)).setNotifyOnDeviceNearby(false)
+                    .build();
+            mAssociationStore.updateAssociation(association);
+        }
+
+        Slog.i(TAG, "Unregistered device presence listener.");
+
+        // If last listener is unregistered, then unbind application.
+        if (!shouldBindPackage(userId, packageName)) {
+            mCompanionAppBinder.unbindCompanionApp(userId, packageName);
+        }
+    }
+
+    /**
+     * For legacy device presence below Android V.
+     *
+     * @deprecated Use {@link #startObservingDevicePresence(ObservingDevicePresenceRequest, String,
+     *             int)}
+     */
+    @Deprecated
+    public void startObservingDevicePresence(int userId, String packageName, String deviceAddress)
+            throws RemoteException {
+        Slog.i(TAG,
+                "Start observing device=[" + deviceAddress + "] for userId=[" + userId
+                        + "], package=["
+                        + packageName + "]...");
+
+        enforceCallerCanManageAssociationsForPackage(mContext, userId, packageName, null);
+
+        AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId,
+                packageName, deviceAddress);
+
+        if (association == null) {
+            throw new RemoteException(new DeviceNotAssociatedException("App " + packageName
+                    + " is not associated with device " + deviceAddress
+                    + " for user " + userId));
+        }
+
+        startObservingDevicePresence(
+                new ObservingDevicePresenceRequest.Builder().setAssociationId(association.getId())
+                        .build(), packageName, userId);
+    }
+
+    /**
+     * For legacy device presence below Android V.
+     *
+     * @deprecated Use {@link #stopObservingDevicePresence(ObservingDevicePresenceRequest, String,
+     *             int)}
+     */
+    @Deprecated
+    public void stopObservingDevicePresence(int userId, String packageName, String deviceAddress)
+            throws RemoteException {
+        Slog.i(TAG,
+                "Stop observing device=[" + deviceAddress + "] for userId=[" + userId
+                        + "], package=["
+                        + packageName + "]...");
+
+        enforceCallerCanManageAssociationsForPackage(mContext, userId, packageName, null);
+
+        AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId,
+                packageName, deviceAddress);
+
+        if (association == null) {
+            throw new RemoteException(new DeviceNotAssociatedException("App " + packageName
+                    + " is not associated with device " + deviceAddress
+                    + " for user " + userId));
+        }
+
+        stopObservingDevicePresence(
+                new ObservingDevicePresenceRequest.Builder().setAssociationId(association.getId())
+                        .build(), packageName, userId);
+    }
+
+    /**
+     * @return whether the package should be bound (i.e. at least one of the devices associated with
+     * the package is currently present OR the UUID to be observed by this package is
+     * currently present).
+     */
+    private boolean shouldBindPackage(@UserIdInt int userId, @NonNull String packageName) {
+        final List<AssociationInfo> packageAssociations =
+                mAssociationStore.getActiveAssociationsByPackage(userId, packageName);
+        final List<ObservableUuid> observableUuids =
+                mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
+
+        for (AssociationInfo association : packageAssociations) {
+            if (!association.shouldBindWhenPresent()) continue;
+            if (isDevicePresent(association.getId())) return true;
+        }
+
+        for (ObservableUuid uuid : observableUuids) {
+            if (isDeviceUuidPresent(uuid.getUuid())) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Bind the system to the app if it's not bound.
+     *
+     * Set bindImportant to true when the association is self-managed to avoid the target service
+     * being killed.
+     */
+    private void bindApplicationIfNeeded(int userId, String packageName, boolean bindImportant) {
+        if (!mCompanionAppBinder.isCompanionApplicationBound(userId, packageName)) {
+            mCompanionAppBinder.bindCompanionApp(
+                    userId, packageName, bindImportant, this::onBinderDied);
+        } else {
+            Slog.i(TAG,
+                    "UserId=[" + userId + "], packageName=[" + packageName + "] is already bound.");
+        }
+    }
+
+    /**
+     * @return current connected UUID devices.
+     */
+    public Set<ParcelUuid> getCurrentConnectedUuidDevices() {
+        return mConnectedUuidDevices;
+    }
+
+    /**
+     * Remove current connected UUID device.
+     */
+    public void removeCurrentConnectedUuidDevice(ParcelUuid uuid) {
+        mConnectedUuidDevices.remove(uuid);
+    }
+
+    /**
+     * @return whether the associated companion devices is present. I.e. device is nearby (for BLE);
+     * or devices is connected (for Bluetooth); or reported (by the application) to be
+     * nearby (for "self-managed" associations).
+     */
+    public boolean isDevicePresent(int associationId) {
+        return mReportedSelfManagedDevices.contains(associationId)
+                || mConnectedBtDevices.contains(associationId)
+                || mNearbyBleDevices.contains(associationId)
+                || mSimulated.contains(associationId);
+    }
+
+    /**
+     * @return whether the current uuid to be observed is present.
+     */
+    public boolean isDeviceUuidPresent(ParcelUuid uuid) {
+        return mConnectedUuidDevices.contains(uuid);
+    }
+
+    /**
+     * @return whether the current device is BT connected and had already reported to the app.
+     */
+
+    public boolean isBtConnected(int associationId) {
+        return mConnectedBtDevices.contains(associationId);
+    }
+
+    /**
+     * @return whether the current device in BLE range and had already reported to the app.
+     */
+    public boolean isBlePresent(int associationId) {
+        return mNearbyBleDevices.contains(associationId);
+    }
+
+    /**
+     * @return whether the current device had been already reported by the simulator.
+     */
+    public boolean isSimulatePresent(int associationId) {
+        return mSimulated.contains(associationId);
+    }
+
+    /**
+     * Marks a "self-managed" device as connected.
+     *
+     * <p>
+     * Must ONLY be invoked by the
+     * {@link com.android.server.companion.CompanionDeviceManagerService
+     * CompanionDeviceManagerService}
+     * when an application invokes
+     * {@link android.companion.CompanionDeviceManager#notifyDeviceAppeared(int)
+     * notifyDeviceAppeared()}
+     */
+    public void onSelfManagedDeviceConnected(int associationId) {
+        onDevicePresenceEvent(mReportedSelfManagedDevices,
+                associationId, EVENT_SELF_MANAGED_APPEARED);
+    }
+
+    /**
+     * Marks a "self-managed" device as disconnected.
+     *
+     * <p>
+     * Must ONLY be invoked by the
+     * {@link com.android.server.companion.CompanionDeviceManagerService
+     * CompanionDeviceManagerService}
+     * when an application invokes
+     * {@link android.companion.CompanionDeviceManager#notifyDeviceDisappeared(int)
+     * notifyDeviceDisappeared()}
+     */
+    public void onSelfManagedDeviceDisconnected(int associationId) {
+        onDevicePresenceEvent(mReportedSelfManagedDevices,
+                associationId, EVENT_SELF_MANAGED_DISAPPEARED);
+    }
+
+    /**
+     * Marks a "self-managed" device as disconnected when binderDied.
+     */
+    public void onSelfManagedDeviceReporterBinderDied(int associationId) {
+        onDevicePresenceEvent(mReportedSelfManagedDevices,
+                associationId, EVENT_SELF_MANAGED_DISAPPEARED);
+    }
+
+    @Override
+    public void onBluetoothCompanionDeviceConnected(int associationId) {
+        synchronized (mBtDisconnectedDevices) {
+            // A device is considered reconnected within 10 seconds if a pending BLE lost report is
+            // followed by a detected Bluetooth connection.
+            boolean isReconnected = mBtDisconnectedDevices.contains(associationId);
+            if (isReconnected) {
+                Slog.i(TAG, "Device ( " + associationId + " ) is reconnected within 10s.");
+                mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId);
+            }
+
+            Slog.i(TAG, "onBluetoothCompanionDeviceConnected: "
+                    + "associationId( " + associationId + " )");
+            onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED);
+
+            // Stop the BLE scan if all devices report BT connected status and BLE was present.
+            if (canStopBleScan()) {
+                mBleScanner.stopScanIfNeeded();
+            }
+
+        }
+    }
+
+    @Override
+    public void onBluetoothCompanionDeviceDisconnected(int associationId) {
+        Slog.i(TAG, "onBluetoothCompanionDeviceDisconnected "
+                + "associationId( " + associationId + " )");
+        // Start BLE scanning when the device is disconnected.
+        mBleScanner.startScan();
+
+        onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_DISCONNECTED);
+        // If current device is BLE present but BT is disconnected , means it will be
+        // potentially out of range later. Schedule BLE disappeared callback.
+        if (isBlePresent(associationId)) {
+            synchronized (mBtDisconnectedDevices) {
+                mBtDisconnectedDevices.add(associationId);
+            }
+            mBleDeviceDisappearedScheduler.scheduleBleDeviceDisappeared(associationId);
+        }
+    }
+
+
+    @Override
+    public void onBleCompanionDeviceFound(int associationId) {
+        onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED);
+        synchronized (mBtDisconnectedDevices) {
+            final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get(associationId);
+            if (mBtDisconnectedDevices.contains(associationId) && isCurrentPresent) {
+                mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId);
+            }
+        }
+    }
+
+    @Override
+    public void onBleCompanionDeviceLost(int associationId) {
+        onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED);
+    }
+
+    /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
+    @TestApi
+    public void simulateDeviceEvent(int associationId, int event) {
+        // IMPORTANT: this API should only be invoked via the
+        // 'companiondevice simulate-device-appeared' Shell command, so the only uid-s allowed to
+        // make this call are SHELL and ROOT.
+        // No other caller (including SYSTEM!) should be allowed.
+        enforceCallerShellOrRoot();
+        // Make sure the association exists.
+        enforceAssociationExists(associationId);
+
+        switch (event) {
+            case EVENT_BLE_APPEARED:
+                simulateDeviceAppeared(associationId, event);
+                break;
+            case EVENT_BT_CONNECTED:
+                onBluetoothCompanionDeviceConnected(associationId);
+                break;
+            case EVENT_BLE_DISAPPEARED:
+                simulateDeviceDisappeared(associationId, event);
+                break;
+            case EVENT_BT_DISCONNECTED:
+                onBluetoothCompanionDeviceDisconnected(associationId);
+                break;
+            default:
+                throw new IllegalArgumentException("Event: " + event + "is not supported");
+        }
+    }
+
+    /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
+    @TestApi
+    public void simulateDeviceEventByUuid(ObservableUuid uuid, int event) {
+        // IMPORTANT: this API should only be invoked via the
+        // 'companiondevice simulate-device-uuid-events' Shell command, so the only uid-s allowed to
+        // make this call are SHELL and ROOT.
+        // No other caller (including SYSTEM!) should be allowed.
+        enforceCallerShellOrRoot();
+        onDevicePresenceEventByUuid(uuid, event);
+    }
+
+    private void simulateDeviceAppeared(int associationId, int state) {
+        onDevicePresenceEvent(mSimulated, associationId, state);
+        mSchedulerHelper.scheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId);
+    }
+
+    private void simulateDeviceDisappeared(int associationId, int state) {
+        mSchedulerHelper.unscheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId);
+        onDevicePresenceEvent(mSimulated, associationId, state);
+    }
+
+    private void enforceAssociationExists(int associationId) {
+        if (mAssociationStore.getAssociationById(associationId) == null) {
+            throw new IllegalArgumentException(
+                    "Association with id " + associationId + " does not exist.");
+        }
+    }
+
+    private void onDevicePresenceEvent(@NonNull Set<Integer> presentDevicesForSource,
+            int associationId, int eventType) {
+        Slog.i(TAG,
+                "onDevicePresenceEvent() id=[" + associationId + "], event=[" + eventType + "]...");
+
+        AssociationInfo association = mAssociationStore.getAssociationById(associationId);
+        if (association == null) {
+            Slog.e(TAG, "Association doesn't exist.");
+            return;
+        }
+
+        final int userId = association.getUserId();
+        final String packageName = association.getPackageName();
+        final DevicePresenceEvent event = new DevicePresenceEvent(associationId, eventType, null);
+
+        if (eventType == EVENT_BLE_APPEARED) {
+            synchronized (mBtDisconnectedDevices) {
+                // If a BLE device is detected within 10 seconds after BT is disconnected,
+                // flag it as BLE is present.
+                if (mBtDisconnectedDevices.contains(associationId)) {
+                    Slog.i(TAG, "Device ( " + associationId + " ) is present,"
+                            + " do not need to send the callback with event ( "
+                            + EVENT_BLE_APPEARED + " ).");
+                    mBtDisconnectedDevicesBlePresence.append(associationId, true);
+                }
+            }
+        }
+
+        switch (eventType) {
+            case EVENT_BLE_APPEARED:
+            case EVENT_BT_CONNECTED:
+            case EVENT_SELF_MANAGED_APPEARED:
+                final boolean added = presentDevicesForSource.add(associationId);
+                if (!added) {
+                    Slog.w(TAG, "The association is already present.");
+                }
+
+                if (association.shouldBindWhenPresent()) {
+                    bindApplicationIfNeeded(userId, packageName, association.isSelfManaged());
+                } else {
+                    return;
+                }
+
+                if (association.isSelfManaged() || added) {
+                    notifyDevicePresenceEvent(userId, packageName, event);
+                    // Also send the legacy callback.
+                    legacyNotifyDevicePresenceEvent(association, true);
+                }
+                break;
+            case EVENT_BLE_DISAPPEARED:
+            case EVENT_BT_DISCONNECTED:
+            case EVENT_SELF_MANAGED_DISAPPEARED:
+                final boolean removed = presentDevicesForSource.remove(associationId);
+                if (!removed) {
+                    Slog.w(TAG, "The association is already NOT present.");
+                }
+
+                if (!mCompanionAppBinder.isCompanionApplicationBound(userId, packageName)) {
+                    Slog.e(TAG, "Package is not bound");
+                    return;
+                }
+
+                if (association.isSelfManaged() || removed) {
+                    notifyDevicePresenceEvent(userId, packageName, event);
+                    // Also send the legacy callback.
+                    legacyNotifyDevicePresenceEvent(association, false);
+                }
+
+                // Check if there are other devices associated to the app that are present.
+                if (!shouldBindPackage(userId, packageName)) {
+                    mCompanionAppBinder.unbindCompanionApp(userId, packageName);
+                }
+                break;
+            default:
+                Slog.e(TAG, "Event: " + eventType + " is not supported.");
+                break;
+        }
+    }
+
+    @Override
+    public void onDevicePresenceEventByUuid(ObservableUuid uuid, int eventType) {
+        Slog.i(TAG, "onDevicePresenceEventByUuid ObservableUuid=[" + uuid + "], event=[" + eventType
+                + "]...");
+
+        final ParcelUuid parcelUuid = uuid.getUuid();
+        final String packageName = uuid.getPackageName();
+        final int userId = uuid.getUserId();
+        final DevicePresenceEvent event = new DevicePresenceEvent(NO_ASSOCIATION, eventType,
+                parcelUuid);
+
+        switch (eventType) {
+            case EVENT_BT_CONNECTED:
+                boolean added = mConnectedUuidDevices.add(parcelUuid);
+                if (!added) {
+                    Slog.w(TAG, "This device is already connected.");
+                }
+
+                bindApplicationIfNeeded(userId, packageName, false);
+
+                notifyDevicePresenceEvent(userId, packageName, event);
+                break;
+            case EVENT_BT_DISCONNECTED:
+                final boolean removed = mConnectedUuidDevices.remove(parcelUuid);
+                if (!removed) {
+                    Slog.w(TAG, "This device is already disconnected.");
+                    return;
+                }
+
+                if (!mCompanionAppBinder.isCompanionApplicationBound(userId, packageName)) {
+                    Slog.e(TAG, "Package is not bound.");
+                    return;
+                }
+
+                notifyDevicePresenceEvent(userId, packageName, event);
+
+                if (!shouldBindPackage(userId, packageName)) {
+                    mCompanionAppBinder.unbindCompanionApp(userId, packageName);
+                }
+                break;
+            default:
+                Slog.e(TAG, "Event: " + eventType + " is not supported");
+                break;
+        }
+    }
+
+    /**
+     * Notify device presence event to the app.
+     *
+     * @deprecated Use {@link #notifyDevicePresenceEvent(int, String, DevicePresenceEvent)} instead.
+     */
+    @Deprecated
+    private void legacyNotifyDevicePresenceEvent(AssociationInfo association,
+            boolean isAppeared) {
+        Slog.i(TAG, "legacyNotifyDevicePresenceEvent() association=[" + association.toShortString()
+                + "], isAppeared=[" + isAppeared + "]");
+
+        final int userId = association.getUserId();
+        final String packageName = association.getPackageName();
+
+        final CompanionServiceConnector primaryServiceConnector =
+                mCompanionAppBinder.getPrimaryServiceConnector(userId, packageName);
+        if (primaryServiceConnector == null) {
+            Slog.e(TAG, "Package is not bound.");
+            return;
+        }
+
+        if (isAppeared) {
+            primaryServiceConnector.postOnDeviceAppeared(association);
+        } else {
+            primaryServiceConnector.postOnDeviceDisappeared(association);
+        }
+    }
+
+    /**
+     * Notify the device presence event to the app.
+     */
+    private void notifyDevicePresenceEvent(int userId, String packageName,
+            DevicePresenceEvent event) {
+        Slog.i(TAG,
+                "notifyCompanionDevicePresenceEvent userId=[" + userId + "], packageName=["
+                        + packageName + "], event=[" + event + "]...");
+
+        final CompanionServiceConnector primaryServiceConnector =
+                mCompanionAppBinder.getPrimaryServiceConnector(userId, packageName);
+
+        if (primaryServiceConnector == null) {
+            Slog.e(TAG, "Package is NOT bound.");
+            return;
+        }
+
+        primaryServiceConnector.postOnDevicePresenceEvent(event);
+    }
+
+    /**
+     * Notify the self-managed device presence event to the app.
+     */
+    public void notifySelfManagedDevicePresenceEvent(int associationId, boolean isAppeared) {
+        Slog.i(TAG, "notifySelfManagedDeviceAppeared() id=" + associationId);
+
+        AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(
+                associationId);
+        if (!association.isSelfManaged()) {
+            throw new IllegalArgumentException("Association id=[" + associationId
+                    + "] is not self-managed.");
+        }
+        // AssociationInfo class is immutable: create a new AssociationInfo object with updated
+        // timestamp.
+        association = (new AssociationInfo.Builder(association))
+                .setLastTimeConnected(System.currentTimeMillis())
+                .build();
+        mAssociationStore.updateAssociation(association);
+
+        if (isAppeared) {
+            onSelfManagedDeviceConnected(associationId);
+        } else {
+            onSelfManagedDeviceDisconnected(associationId);
+        }
+
+        final String deviceProfile = association.getDeviceProfile();
+        if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) {
+            Slog.i(TAG, "Enable hint mode for device device profile: " + deviceProfile);
+            mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, isAppeared);
+        }
+    }
+
+    private void onBinderDied(@UserIdInt int userId, @NonNull String packageName,
+            @NonNull CompanionServiceConnector serviceConnector) {
+
+        boolean isPrimary = serviceConnector.isPrimary();
+        Slog.i(TAG, "onBinderDied() u" + userId + "/" + packageName + " isPrimary: " + isPrimary);
+
+        // First, disable hint mode for Auto profile and mark not BOUND for primary service ONLY.
+        if (isPrimary) {
+            final List<AssociationInfo> associations =
+                    mAssociationStore.getActiveAssociationsByPackage(userId, packageName);
+
+            for (AssociationInfo association : associations) {
+                final String deviceProfile = association.getDeviceProfile();
+                if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) {
+                    Slog.i(TAG, "Disable hint mode for device profile: " + deviceProfile);
+                    mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false);
+                    break;
+                }
+            }
+
+            mCompanionAppBinder.removePackage(userId, packageName);
+        }
+
+        // Second: schedule rebinding if needed.
+        final boolean shouldScheduleRebind = shouldScheduleRebind(userId, packageName, isPrimary);
+
+        if (shouldScheduleRebind) {
+            mCompanionAppBinder.scheduleRebinding(userId, packageName, serviceConnector);
+        }
+    }
+
+    /**
+     * Check if the system should rebind the self-managed secondary services
+     * OR non-self-managed services.
+     */
+    private boolean shouldScheduleRebind(int userId, String packageName, boolean isPrimary) {
+        // Make sure do not schedule rebind for the case ServiceConnector still gets callback after
+        // app is uninstalled.
+        boolean stillAssociated = false;
+        // Make sure to clean up the state for all the associations
+        // that associate with this package.
+        boolean shouldScheduleRebind = false;
+        boolean shouldScheduleRebindForUuid = false;
+        final List<ObservableUuid> uuids =
+                mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
+
+        for (AssociationInfo ai :
+                mAssociationStore.getActiveAssociationsByPackage(userId, packageName)) {
+            final int associationId = ai.getId();
+            stillAssociated = true;
+            if (ai.isSelfManaged()) {
+                // Do not rebind if primary one is died for selfManaged application.
+                if (isPrimary && isDevicePresent(associationId)) {
+                    onSelfManagedDeviceReporterBinderDied(associationId);
+                    shouldScheduleRebind = false;
+                }
+                // Do not rebind if both primary and secondary services are died for
+                // selfManaged application.
+                shouldScheduleRebind = mCompanionAppBinder.isCompanionApplicationBound(userId,
+                        packageName);
+            } else if (ai.isNotifyOnDeviceNearby()) {
+                // Always rebind for non-selfManaged devices.
+                shouldScheduleRebind = true;
+            }
+        }
+
+        for (ObservableUuid uuid : uuids) {
+            if (isDeviceUuidPresent(uuid.getUuid())) {
+                shouldScheduleRebindForUuid = true;
+                break;
+            }
+        }
+
+        return (stillAssociated && shouldScheduleRebind) || shouldScheduleRebindForUuid;
+    }
+
+    /**
+     * Implements
+     * {@link AssociationStore.OnChangeListener#onAssociationRemoved(AssociationInfo)}
+     */
+    @Override
+    public void onAssociationRemoved(@NonNull AssociationInfo association) {
+        final int id = association.getId();
+        if (DEBUG) {
+            Log.i(TAG, "onAssociationRemoved() id=" + id);
+            Log.d(TAG, "  > association=" + association);
+        }
+
+        mConnectedBtDevices.remove(id);
+        mNearbyBleDevices.remove(id);
+        mReportedSelfManagedDevices.remove(id);
+        mSimulated.remove(id);
+        synchronized (mBtDisconnectedDevices) {
+            mBtDisconnectedDevices.remove(id);
+            mBtDisconnectedDevicesBlePresence.delete(id);
+        }
+
+        // Do NOT call mCallback.onDeviceDisappeared()!
+        // CompanionDeviceManagerService will know that the association is removed, and will do
+        // what's needed.
+    }
+
+    /**
+     * Return a set of devices that pending to report connectivity
+     */
+    public SparseArray<Set<BluetoothDevice>> getPendingConnectedDevices() {
+        synchronized (mBtConnectionListener.mPendingConnectedDevices) {
+            return mBtConnectionListener.mPendingConnectedDevices;
+        }
+    }
+
+    private static void enforceCallerShellOrRoot() {
+        final int callingUid = Binder.getCallingUid();
+        if (callingUid == SHELL_UID || callingUid == ROOT_UID) return;
+
+        throw new SecurityException("Caller is neither Shell nor Root");
+    }
+
+    /**
+     * The BLE scan can be only stopped if all the devices have been reported
+     * BT connected and BLE presence and are not pending to report BLE lost.
+     */
+    private boolean canStopBleScan() {
+        for (AssociationInfo ai : mAssociationStore.getActiveAssociations()) {
+            int id = ai.getId();
+            synchronized (mBtDisconnectedDevices) {
+                if (ai.isNotifyOnDeviceNearby() && !(isBtConnected(id)
+                        && isBlePresent(id) && mBtDisconnectedDevices.isEmpty())) {
+                    Slog.i(TAG, "The BLE scan cannot be stopped, "
+                            + "device( " + id + " ) is not yet connected "
+                            + "OR the BLE is not current present Or is pending to report BLE lost");
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Dumps system information about devices that are marked as "present".
+     */
+    public void dump(@NonNull PrintWriter out) {
+        out.append("Companion Device Present: ");
+        if (mConnectedBtDevices.isEmpty()
+                && mNearbyBleDevices.isEmpty()
+                && mReportedSelfManagedDevices.isEmpty()) {
+            out.append("<empty>\n");
+            return;
+        } else {
+            out.append("\n");
+        }
+
+        out.append("  Connected Bluetooth Devices: ");
+        if (mConnectedBtDevices.isEmpty()) {
+            out.append("<empty>\n");
+        } else {
+            out.append("\n");
+            for (int associationId : mConnectedBtDevices) {
+                AssociationInfo a = mAssociationStore.getAssociationById(associationId);
+                out.append("    ").append(a.toShortString()).append('\n');
+            }
+        }
+
+        out.append("  Nearby BLE Devices: ");
+        if (mNearbyBleDevices.isEmpty()) {
+            out.append("<empty>\n");
+        } else {
+            out.append("\n");
+            for (int associationId : mNearbyBleDevices) {
+                AssociationInfo a = mAssociationStore.getAssociationById(associationId);
+                out.append("    ").append(a.toShortString()).append('\n');
+            }
+        }
+
+        out.append("  Self-Reported Devices: ");
+        if (mReportedSelfManagedDevices.isEmpty()) {
+            out.append("<empty>\n");
+        } else {
+            out.append("\n");
+            for (int associationId : mReportedSelfManagedDevices) {
+                AssociationInfo a = mAssociationStore.getAssociationById(associationId);
+                out.append("    ").append(a.toShortString()).append('\n');
+            }
+        }
+    }
+
+    private class SimulatedDevicePresenceSchedulerHelper extends Handler {
+        SimulatedDevicePresenceSchedulerHelper() {
+            super(Looper.getMainLooper());
+        }
+
+        void scheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) {
+            // First, unschedule if it was scheduled previously.
+            if (hasMessages(/* what */ associationId)) {
+                removeMessages(/* what */ associationId);
+            }
+
+            sendEmptyMessageDelayed(/* what */ associationId, 60 * 1000 /* 60 seconds */);
+        }
+
+        void unscheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) {
+            removeMessages(/* what */ associationId);
+        }
+
+        @Override
+        public void handleMessage(@NonNull Message msg) {
+            final int associationId = msg.what;
+            if (mSimulated.contains(associationId)) {
+                onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_DISAPPEARED);
+            }
+        }
+    }
+
+    private class BleDeviceDisappearedScheduler extends Handler {
+        BleDeviceDisappearedScheduler() {
+            super(Looper.getMainLooper());
+        }
+
+        void scheduleBleDeviceDisappeared(int associationId) {
+            if (hasMessages(associationId)) {
+                removeMessages(associationId);
+            }
+            Slog.i(TAG, "scheduleBleDeviceDisappeared for Device: ( " + associationId + " ).");
+            sendEmptyMessageDelayed(associationId, 10 * 1000 /* 10 seconds */);
+        }
+
+        void unScheduleDeviceDisappeared(int associationId) {
+            if (hasMessages(associationId)) {
+                Slog.i(TAG, "unScheduleDeviceDisappeared for Device( " + associationId + " )");
+                synchronized (mBtDisconnectedDevices) {
+                    mBtDisconnectedDevices.remove(associationId);
+                    mBtDisconnectedDevicesBlePresence.delete(associationId);
+                }
+
+                removeMessages(associationId);
+            }
+        }
+
+        @Override
+        public void handleMessage(@NonNull Message msg) {
+            final int associationId = msg.what;
+            synchronized (mBtDisconnectedDevices) {
+                final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get(
+                        associationId);
+                // If a device hasn't reported after 10 seconds and is not currently present,
+                // assume BLE is lost and trigger the onDeviceEvent callback with the
+                // EVENT_BLE_DISAPPEARED event.
+                if (mBtDisconnectedDevices.contains(associationId)
+                        && !isCurrentPresent) {
+                    Slog.i(TAG, "Device ( " + associationId + " ) is likely BLE out of range, "
+                            + "sending callback with event ( " + EVENT_BLE_DISAPPEARED + " )");
+                    onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED);
+                }
+
+                mBtDisconnectedDevices.remove(associationId);
+                mBtDisconnectedDevicesBlePresence.delete(associationId);
+            }
+        }
+    }
+}
diff --git a/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java b/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java
index db15da29..fa0f6bd 100644
--- a/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java
+++ b/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java
@@ -300,4 +300,18 @@
             return readObservableUuidsFromCache(userId);
         }
     }
+
+    /**
+     * Check if a UUID is being observed by the package.
+     */
+    public boolean isUuidBeingObserved(ParcelUuid uuid, int userId, String packageName) {
+        final List<ObservableUuid> uuidsBeingObserved = getObservableUuidsForPackage(userId,
+                packageName);
+        for (ObservableUuid observableUuid : uuidsBeingObserved) {
+            if (observableUuid.getUuid().equals(uuid)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
index 793fb7f..697ef87 100644
--- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
+++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
@@ -46,7 +46,6 @@
 @SuppressLint("LongLogTag")
 public class CompanionTransportManager {
     private static final String TAG = "CDM_CompanionTransportManager";
-    private static final boolean DEBUG = false;
 
     private boolean mSecureTransportEnabled = true;
 
@@ -137,11 +136,17 @@
         }
     }
 
-    public void attachSystemDataTransport(String packageName, int userId, int associationId,
-            ParcelFileDescriptor fd) {
+    /**
+     * Attach transport.
+     */
+    public void attachSystemDataTransport(int associationId, ParcelFileDescriptor fd) {
+        Slog.i(TAG, "Attaching transport for association id=[" + associationId + "]...");
+
+        mAssociationStore.getAssociationWithCallerChecks(associationId);
+
         synchronized (mTransports) {
             if (mTransports.contains(associationId)) {
-                detachSystemDataTransport(packageName, userId, associationId);
+                detachSystemDataTransport(associationId);
             }
 
             // TODO: Implement new API to pass a PSK
@@ -149,9 +154,18 @@
 
             notifyOnTransportsChanged();
         }
+
+        Slog.i(TAG, "Transport attached.");
     }
 
-    public void detachSystemDataTransport(String packageName, int userId, int associationId) {
+    /**
+     * Detach transport.
+     */
+    public void detachSystemDataTransport(int associationId) {
+        Slog.i(TAG, "Detaching transport for association id=[" + associationId + "]...");
+
+        mAssociationStore.getAssociationWithCallerChecks(associationId);
+
         synchronized (mTransports) {
             final Transport transport = mTransports.removeReturnOld(associationId);
             if (transport == null) {
@@ -161,6 +175,8 @@
             transport.stop();
             notifyOnTransportsChanged();
         }
+
+        Slog.i(TAG, "Transport detached.");
     }
 
     private void notifyOnTransportsChanged() {
@@ -307,8 +323,7 @@
         int associationId = transport.mAssociationId;
         AssociationInfo association = mAssociationStore.getAssociationById(associationId);
         if (association != null) {
-            detachSystemDataTransport(association.getPackageName(),
-                    association.getUserId(),
+            detachSystemDataTransport(
                     association.getId());
         }
     }
diff --git a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java
index 2cf1f46..d7e766e 100644
--- a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java
+++ b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java
@@ -39,7 +39,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
-import android.companion.AssociationInfo;
 import android.companion.AssociationRequest;
 import android.companion.CompanionDeviceManager;
 import android.content.Context;
@@ -208,7 +207,7 @@
     /**
      * Require the caller to hold necessary permission to observe device presence by UUID.
      */
-    public static void enforceCallerCanObservingDevicePresenceByUuid(@NonNull Context context) {
+    public static void enforceCallerCanObserveDevicePresenceByUuid(@NonNull Context context) {
         if (context.checkCallingPermission(REQUEST_OBSERVE_DEVICE_UUID_PRESENCE)
                 != PERMISSION_GRANTED) {
             throw new SecurityException("Caller (uid=" + getCallingUid() + ") does not have "
@@ -235,23 +234,6 @@
         return checkCallerCanManageCompanionDevice(context);
     }
 
-    /**
-     * Check if CDM can trust the context to process the association.
-     */
-    @Nullable
-    public static AssociationInfo sanitizeWithCallerChecks(@NonNull Context context,
-            @Nullable AssociationInfo association) {
-        if (association == null) return null;
-
-        final int userId = association.getUserId();
-        final String packageName = association.getPackageName();
-        if (!checkCallerCanManageAssociationsForPackage(context, userId, packageName)) {
-            return null;
-        }
-
-        return association;
-    }
-
     private static boolean checkPackage(@UserIdInt int uid, @NonNull String packageName) {
         try {
             return getAppOpsService().checkPackage(uid, packageName) == MODE_ALLOWED;
diff --git a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
index b28bc1f..16a9933 100644
--- a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
+++ b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
@@ -29,6 +29,7 @@
 import android.annotation.RequiresPermission;
 import android.app.ActivityOptions;
 import android.app.admin.DevicePolicyManagerInternal;
+import android.app.contextualsearch.CallbackToken;
 import android.app.contextualsearch.ContextualSearchManager;
 import android.app.contextualsearch.ContextualSearchState;
 import android.app.contextualsearch.IContextualSearchCallback;
@@ -164,7 +165,7 @@
         }
     }
 
-    private Intent getContextualSearchIntent(int entrypoint, IBinder mToken) {
+    private Intent getContextualSearchIntent(int entrypoint, CallbackToken mToken) {
         final Intent launchIntent = getResolvedLaunchIntent();
         if (launchIntent == null) {
             return null;
@@ -256,14 +257,14 @@
     }
 
     private class ContextualSearchManagerStub extends IContextualSearchManager.Stub {
-        private @Nullable IBinder mToken;
+        private @Nullable CallbackToken mToken;
 
         @Override
         public void startContextualSearch(int entrypoint) {
             synchronized (this) {
                 if (DEBUG_USER) Log.d(TAG, "startContextualSearch");
                 enforcePermission("startContextualSearch");
-                mToken = new Binder();
+                mToken = new CallbackToken();
                 // We get the launch intent with the system server's identity because the system
                 // server has READ_FRAME_BUFFER permission to get the screenshot and because only
                 // the system server can invoke non-exported activities.
@@ -284,7 +285,7 @@
             if (DEBUG_USER) {
                 Log.i(TAG, "getContextualSearchState token: " + token + ", callback: " + callback);
             }
-            if (mToken == null || !mToken.equals(token)) {
+            if (mToken == null || !mToken.getToken().equals(token)) {
                 if (DEBUG_USER) {
                     Log.e(TAG, "getContextualSearchState: invalid token, returning error");
                 }
diff --git a/services/core/java/com/android/server/am/LmkdStatsReporter.java b/services/core/java/com/android/server/am/LmkdStatsReporter.java
index 595d16d..b55f35d 100644
--- a/services/core/java/com/android/server/am/LmkdStatsReporter.java
+++ b/services/core/java/com/android/server/am/LmkdStatsReporter.java
@@ -44,6 +44,7 @@
     private static final int LOW_MEM_AND_SWAP_UTIL = 6;
     private static final int LOW_FILECACHE_AFTER_THRASHING = 7;
     private static final int LOW_MEM = 8;
+    private static final int DIRECT_RECL_STUCK = 9;
 
     /**
      * Processes the LMK_KILL_OCCURRED packet data
@@ -98,6 +99,8 @@
                 return FrameworkStatsLog.LMK_KILL_OCCURRED__REASON__LOW_FILECACHE_AFTER_THRASHING;
             case LOW_MEM:
                 return FrameworkStatsLog.LMK_KILL_OCCURRED__REASON__LOW_MEM;
+            case DIRECT_RECL_STUCK:
+                return FrameworkStatsLog.LMK_KILL_OCCURRED__REASON__DIRECT_RECL_STUCK;
             default:
                 return FrameworkStatsLog.LMK_KILL_OCCURRED__REASON__UNKNOWN;
         }
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index a917909..c020a9f 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -576,6 +576,8 @@
             }
             // allowDelayedLocking set here as stopping user is done without any explicit request
             // from outside.
+            Slogf.i(TAG, "Too many running users (%d). Attempting to stop user %d",
+                    currentlyRunningLru.size(), userId);
             if (stopUsersLU(userId, /* force= */ false, /* allowDelayedLocking= */ true,
                     /* stopUserCallback= */ null, /* keyEvictedCallback= */ null)
                     == USER_OP_SUCCESS) {
@@ -1540,6 +1542,7 @@
         }
         if (userInfo.isGuest() || userInfo.isEphemeral()) {
             // This is a user to be stopped.
+            Slogf.i(TAG, "Stopping background guest or ephemeral user " + oldUserId);
             synchronized (mLock) {
                 stopUsersLU(oldUserId, /* force= */ true, /* allowDelayedLocking= */ false,
                         null, null);
@@ -2219,6 +2222,10 @@
         mUserSwitchObservers.finishBroadcast();
     }
 
+    /**
+     * Possibly stops the given full user (or its profile) when we switch out of it, if dictated
+     * by policy.
+     */
     private void stopUserOnSwitchIfEnforced(@UserIdInt int oldUserId) {
         // Never stop system user
         if (oldUserId == UserHandle.USER_SYSTEM) {
@@ -2228,20 +2235,27 @@
                 hasUserRestriction(UserManager.DISALLOW_RUN_IN_BACKGROUND, oldUserId);
         synchronized (mLock) {
             // If running in background is disabled or mStopUserOnSwitch mode, stop the user.
-            boolean disallowRunInBg = hasRestriction || shouldStopUserOnSwitch();
-            if (!disallowRunInBg) {
-                if (DEBUG_MU) {
-                    Slogf.i(TAG, "stopUserOnSwitchIfEnforced() NOT stopping %d and related users",
-                            oldUserId);
-                }
+            if (hasRestriction || shouldStopUserOnSwitch()) {
+                Slogf.i(TAG, "Stopping user %d and its profiles on user switch", oldUserId);
+                stopUsersLU(oldUserId, /* force= */ false, /* allowDelayedLocking= */ false,
+                        null, null);
                 return;
             }
-            if (DEBUG_MU) {
-                Slogf.i(TAG, "stopUserOnSwitchIfEnforced() stopping %d and related users",
-                        oldUserId);
+        }
+
+        // We didn't need to stop the parent, but perhaps one of its profiles needs to be stopped.
+        final List<UserInfo> profiles = mInjector.getUserManager().getProfiles(
+                oldUserId, /* enabledOnly= */ false);
+        final int count = profiles.size();
+        for (int i = 0; i < count; i++) {
+            final int profileUserId = profiles.get(i).id;
+            if (hasUserRestriction(UserManager.DISALLOW_RUN_IN_BACKGROUND, profileUserId)) {
+                Slogf.i(TAG, "Stopping profile %d on user switch", profileUserId);
+                synchronized (mLock) {
+                    stopUsersLU(profileUserId,
+                            /* force= */ true, /* allowDelayedLocking= */ false, null, null);
+                }
             }
-            stopUsersLU(oldUserId, /* force= */ false, /* allowDelayedLocking= */ true,
-                    null, null);
         }
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
index 0c3dfa7..eb78fe6 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java
@@ -23,6 +23,7 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.RemoteException;
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -135,6 +136,14 @@
         mCancelWatchdog = () -> {
             if (!isFinished()) {
                 Slog.e(TAG, "[Watchdog Triggered]: " + this);
+                try {
+                    mClientMonitor.getListener().onError(mClientMonitor.getSensorId(),
+                            mClientMonitor.getCookie(), BiometricConstants.BIOMETRIC_ERROR_CANCELED,
+                            0 /* vendorCode */);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Remote exception when trying to send error in cancel "
+                            + "watchdog.");
+                }
                 getWrappedCallback(mOnStartCallback)
                         .onClientFinished(mClientMonitor, false /* success */);
             }
diff --git a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
index b1defe9..65c9f35 100644
--- a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
+++ b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
@@ -21,6 +21,7 @@
 import android.annotation.Nullable;
 import android.hardware.display.DisplayManagerInternal;
 import android.os.Trace;
+import android.view.Display;
 
 /**
  * An implementation of the offload session that keeps track of whether the session is active.
@@ -42,7 +43,7 @@
 
     @Override
     public void setDozeStateOverride(int displayState) {
-        mDisplayPowerController.overrideDozeScreenState(displayState);
+        mDisplayPowerController.overrideDozeScreenState(displayState, Display.STATE_REASON_OFFLOAD);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index d99f712..043ff0e 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -61,6 +61,7 @@
 import android.util.MathUtils;
 import android.util.MutableFloat;
 import android.util.MutableInt;
+import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.Display;
@@ -788,14 +789,14 @@
     }
 
     @Override
-    public void overrideDozeScreenState(int displayState) {
+    public void overrideDozeScreenState(int displayState, @Display.StateReason int reason) {
         mHandler.postAtTime(() -> {
             if (mDisplayOffloadSession == null
                     || !(DisplayOffloadSession.isSupportedOffloadState(displayState)
                     || displayState == Display.STATE_UNKNOWN)) {
                 return;
             }
-            mDisplayStateController.overrideDozeScreenState(displayState);
+            mDisplayStateController.overrideDozeScreenState(displayState, reason);
             sendUpdatePowerState();
         }, mClock.uptimeMillis());
     }
@@ -975,7 +976,7 @@
                 && mAutomaticBrightnessController.isInIdleMode());
         mScreenBrightnessRampAnimator.setListener(mRampAnimatorListener);
 
-        noteScreenState(mPowerState.getScreenState());
+        noteScreenState(mPowerState.getScreenState(), Display.STATE_REASON_DEFAULT_POLICY);
         noteScreenBrightness(mPowerState.getScreenBrightness());
 
         // Initialize all of the brightness tracking state
@@ -1276,8 +1277,10 @@
             displayBrightnessFollowers = mDisplayBrightnessFollowers.clone();
         }
 
-        int state = mDisplayStateController
-                .updateDisplayState(mPowerRequest, mIsEnabled, mIsInTransition);
+        final Pair<Integer, Integer> stateAndReason =
+                mDisplayStateController
+                        .updateDisplayState(mPowerRequest, mIsEnabled, mIsInTransition);
+        int state = stateAndReason.first;
 
         // Initialize things the first time the power state is changed.
         if (mustInitialize) {
@@ -1287,7 +1290,9 @@
         // Animate the screen state change unless already animating.
         // The transition may be deferred, so after this point we will use the
         // actual state instead of the desired one.
-        animateScreenStateChange(state, mDisplayStateController.shouldPerformScreenOffTransition());
+        animateScreenStateChange(
+                state, /* reason= */ stateAndReason.second,
+                mDisplayStateController.shouldPerformScreenOffTransition());
         state = mPowerState.getScreenState();
 
         // Switch to doze auto-brightness mode if needed
@@ -2024,11 +2029,11 @@
                 Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME, 0);
     }
 
-    private boolean setScreenState(int state) {
-        return setScreenState(state, false /*reportOnly*/);
+    private boolean setScreenState(int state, @Display.StateReason int reason) {
+        return setScreenState(state, reason, false /*reportOnly*/);
     }
 
-    private boolean setScreenState(int state, boolean reportOnly) {
+    private boolean setScreenState(int state, @Display.StateReason int reason, boolean reportOnly) {
         final boolean isOff = (state == Display.STATE_OFF);
         final boolean isOn = (state == Display.STATE_ON);
         final boolean changed = mPowerState.getScreenState() != state;
@@ -2075,9 +2080,9 @@
                             + " value=" + propertyValue + " " + e.getMessage());
                 }
 
-                mPowerState.setScreenState(state);
+                mPowerState.setScreenState(state, reason);
                 // Tell battery stats about the transition.
-                noteScreenState(state);
+                noteScreenState(state, reason);
             }
         }
 
@@ -2174,7 +2179,8 @@
         }
     }
 
-    private void animateScreenStateChange(int target, boolean performScreenOffTransition) {
+    private void animateScreenStateChange(
+            int target, @Display.StateReason int reason, boolean performScreenOffTransition) {
         // If there is already an animation in progress, don't interfere with it.
         if (mColorFadeEnabled
                 && (mColorFadeOnAnimator.isStarted() || mColorFadeOffAnimator.isStarted())) {
@@ -2200,14 +2206,14 @@
             // display has turned off so it can prepare the appropriate power on animation, but we
             // don't want to actually transition to the fully off state since that takes
             // significantly longer to transition from.
-            setScreenState(Display.STATE_OFF, target != Display.STATE_OFF /*reportOnly*/);
+            setScreenState(Display.STATE_OFF, reason, target != Display.STATE_OFF /*reportOnly*/);
         }
 
         // If we were in the process of turning off the screen but didn't quite
         // finish.  Then finish up now to prevent a jarring transition back
         // to screen on if we skipped blocking screen on as usual.
         if (mPendingScreenOff && target != Display.STATE_OFF) {
-            setScreenState(Display.STATE_OFF);
+            setScreenState(Display.STATE_OFF, reason);
             mPendingScreenOff = false;
             mPowerState.dismissColorFadeResources();
         }
@@ -2216,7 +2222,7 @@
             // Want screen on.  The contents of the screen may not yet
             // be visible if the color fade has not been dismissed because
             // its last frame of animation is solid black.
-            if (!setScreenState(Display.STATE_ON)) {
+            if (!setScreenState(Display.STATE_ON, reason)) {
                 return; // screen on blocked
             }
             if (USE_COLOR_FADE_ON_ANIMATION && mColorFadeEnabled && mPowerRequest.isBrightOrDim()) {
@@ -2246,7 +2252,7 @@
             }
 
             // Set screen state.
-            if (!setScreenState(Display.STATE_DOZE)) {
+            if (!setScreenState(Display.STATE_DOZE, reason)) {
                 return; // screen on blocked
             }
 
@@ -2265,10 +2271,10 @@
             // If not already suspending, temporarily set the state to doze until the
             // screen on is unblocked, then suspend.
             if (mPowerState.getScreenState() != Display.STATE_DOZE_SUSPEND) {
-                if (!setScreenState(Display.STATE_DOZE)) {
+                if (!setScreenState(Display.STATE_DOZE, reason)) {
                     return; // screen on blocked
                 }
-                setScreenState(Display.STATE_DOZE_SUSPEND); // already on so can't block
+                setScreenState(Display.STATE_DOZE_SUSPEND, reason); // already on so can't block
             }
 
             // Dismiss the black surface without fanfare.
@@ -2286,10 +2292,10 @@
             // If not already suspending, temporarily set the state to on until the
             // screen on is unblocked, then suspend.
             if (mPowerState.getScreenState() != Display.STATE_ON_SUSPEND) {
-                if (!setScreenState(Display.STATE_ON)) {
+                if (!setScreenState(Display.STATE_ON, reason)) {
                     return;
                 }
-                setScreenState(Display.STATE_ON_SUSPEND);
+                setScreenState(Display.STATE_ON_SUSPEND, reason);
             }
 
             // Dismiss the black surface without fanfare.
@@ -2305,7 +2311,7 @@
             if (mPowerState.getColorFadeLevel() == 0.0f) {
                 // Turn the screen off.
                 // A black surface is already hiding the contents of the screen.
-                setScreenState(Display.STATE_OFF);
+                setScreenState(Display.STATE_OFF, reason);
                 mPendingScreenOff = false;
                 mPowerState.dismissColorFadeResources();
             } else if (performScreenOffTransition
@@ -2667,10 +2673,10 @@
     }
 
 
-    private void noteScreenState(int screenState) {
+    private void noteScreenState(int screenState, @Display.StateReason int reason) {
         // Log screen state change with display id
         FrameworkStatsLog.write(FrameworkStatsLog.SCREEN_STATE_CHANGED_V2,
-                screenState, mDisplayStatsId);
+                screenState, mDisplayStatsId, reason);
         if (mBatteryStats != null) {
             try {
                 // TODO(multi-display): make this multi-display
diff --git a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
index 408d610..d28578a 100644
--- a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
+++ b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
@@ -23,6 +23,7 @@
 import android.hardware.display.BrightnessInfo;
 import android.hardware.display.DisplayManagerInternal;
 import android.os.PowerManager;
+import android.view.Display;
 
 import java.io.PrintWriter;
 
@@ -137,7 +138,13 @@
     boolean requestPowerState(DisplayManagerInternal.DisplayPowerRequest request,
             boolean waitForNegativeProximity);
 
-    void overrideDozeScreenState(int displayState);
+    /**
+     * Overrides the current doze screen state.
+     *
+     * @param displayState the new doze display state.
+     * @param reason the reason behind the new doze display state.
+     */
+    void overrideDozeScreenState(int displayState, @Display.StateReason int reason);
 
     void setDisplayOffloadSession(DisplayManagerInternal.DisplayOffloadSession session);
 
diff --git a/services/core/java/com/android/server/display/DisplayPowerState.java b/services/core/java/com/android/server/display/DisplayPowerState.java
index 90bad12..e5efebc 100644
--- a/services/core/java/com/android/server/display/DisplayPowerState.java
+++ b/services/core/java/com/android/server/display/DisplayPowerState.java
@@ -159,12 +159,13 @@
     /**
      * Sets whether the screen is on, off, or dozing.
      */
-    public void setScreenState(int state) {
+    public void setScreenState(int state, @Display.StateReason int reason) {
         if (mScreenState != state) {
             if (DEBUG) {
-                Slog.w(TAG, "setScreenState: state=" + Display.stateToString(state));
+                Slog.w(TAG,
+                        "setScreenState: state=" + Display.stateToString(state)
+                        + "; reason=" + Display.stateReasonToString(reason));
             }
-
             mScreenState = state;
             mScreenReady = false;
             scheduleScreenUpdate();
diff --git a/services/core/java/com/android/server/display/state/DisplayStateController.java b/services/core/java/com/android/server/display/state/DisplayStateController.java
index 5f28934..21bb208 100644
--- a/services/core/java/com/android/server/display/state/DisplayStateController.java
+++ b/services/core/java/com/android/server/display/state/DisplayStateController.java
@@ -18,6 +18,7 @@
 
 import android.hardware.display.DisplayManagerInternal;
 import android.util.IndentingPrintWriter;
+import android.util.Pair;
 import android.view.Display;
 
 import com.android.server.display.DisplayPowerProximityStateController;
@@ -33,6 +34,7 @@
     private DisplayPowerProximityStateController mDisplayPowerProximityStateController;
     private boolean mPerformScreenOffTransition = false;
     private int mDozeStateOverride = Display.STATE_UNKNOWN;
+    private int mDozeStateOverrideReason = Display.STATE_REASON_UNKNOWN;
 
     public DisplayStateController(DisplayPowerProximityStateController
             displayPowerProximityStateController) {
@@ -47,14 +49,19 @@
      * @param isDisplayEnabled      A boolean flag representing if the display is enabled
      * @param isDisplayInTransition A boolean flag representing if the display is undergoing the
      *                              transition phase
+     * @return a {@link Pair} of integers, the first being the updated display state, and the second
+     *                              being the reason behind the new display state.
      */
-    public int updateDisplayState(DisplayManagerInternal.DisplayPowerRequest displayPowerRequest,
-            boolean isDisplayEnabled, boolean isDisplayInTransition) {
+    public Pair<Integer, Integer> updateDisplayState(
+            DisplayManagerInternal.DisplayPowerRequest displayPowerRequest,
+            boolean isDisplayEnabled,
+            boolean isDisplayInTransition) {
         mPerformScreenOffTransition = false;
         // Compute the basic display state using the policy.
         // We might override this below based on other factors.
         // Initialise brightness as invalid.
         int state;
+        int reason = Display.STATE_REASON_DEFAULT_POLICY;
         switch (displayPowerRequest.policy) {
             case DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF:
                 state = Display.STATE_OFF;
@@ -63,8 +70,10 @@
             case DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE:
                 if (mDozeStateOverride != Display.STATE_UNKNOWN) {
                     state = mDozeStateOverride;
+                    reason = mDozeStateOverrideReason;
                 } else if (displayPowerRequest.dozeScreenState != Display.STATE_UNKNOWN) {
                     state = displayPowerRequest.dozeScreenState;
+                    reason = displayPowerRequest.dozeScreenStateReason;
                 } else {
                     state = Display.STATE_DOZE;
                 }
@@ -84,11 +93,13 @@
             state = Display.STATE_OFF;
         }
 
-        return state;
+        return new Pair(state, reason);
     }
 
-    public void overrideDozeScreenState(int displayState) {
+    /** Overrides the doze screen state with a given reason. */
+    public void overrideDozeScreenState(int displayState, @Display.StateReason int reason) {
         mDozeStateOverride = displayState;
+        mDozeStateOverrideReason = reason;
     }
 
     /**
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index d997020..42c9e08 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -512,7 +512,7 @@
     }
 
     private void startDozingInternal(IBinder token, int screenState,
-            int screenBrightness) {
+            @Display.StateReason int reason, int screenBrightness) {
         if (DEBUG) {
             Slog.d(TAG, "Dream requested to start dozing: " + token
                     + ", screenState=" + screenState
@@ -524,7 +524,7 @@
                 mCurrentDream.dozeScreenState = screenState;
                 mCurrentDream.dozeScreenBrightness = screenBrightness;
                 mPowerManagerInternal.setDozeOverrideFromDreamManager(
-                        screenState, screenBrightness);
+                        screenState, reason, screenBrightness);
                 if (!mCurrentDream.isDozing) {
                     mCurrentDream.isDozing = true;
                     mDozeWakeLock.acquire();
@@ -543,7 +543,9 @@
                 mCurrentDream.isDozing = false;
                 mDozeWakeLock.release();
                 mPowerManagerInternal.setDozeOverrideFromDreamManager(
-                        Display.STATE_UNKNOWN, PowerManager.BRIGHTNESS_DEFAULT);
+                        Display.STATE_UNKNOWN,
+                        Display.STATE_REASON_DREAM_MANAGER,
+                        PowerManager.BRIGHTNESS_DEFAULT);
             }
         }
     }
@@ -1046,7 +1048,9 @@
         }
 
         @Override // Binder call
-        public void startDozing(IBinder token, int screenState, int screenBrightness) {
+        public void startDozing(
+                IBinder token, int screenState, @Display.StateReason int reason,
+                int screenBrightness) {
             // Requires no permission, called by Dream from an arbitrary process.
             if (token == null) {
                 throw new IllegalArgumentException("token must not be null");
@@ -1054,7 +1058,7 @@
 
             final long ident = Binder.clearCallingIdentity();
             try {
-                startDozingInternal(token, screenState, screenBrightness);
+                startDozingInternal(token, screenState, reason, screenBrightness);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
diff --git a/services/core/java/com/android/server/media/AudioManagerRouteController.java b/services/core/java/com/android/server/media/AudioManagerRouteController.java
index 2439589..e7f717a 100644
--- a/services/core/java/com/android/server/media/AudioManagerRouteController.java
+++ b/services/core/java/com/android/server/media/AudioManagerRouteController.java
@@ -387,16 +387,20 @@
     private MediaRoute2Info createMediaRoute2InfoFromAudioDeviceInfo(
             AudioDeviceInfo audioDeviceInfo) {
         String address = audioDeviceInfo.getAddress();
+
         // Passing a null route id means we want to get the default id for the route. Generally, we
         // only expect to pass null for non-Bluetooth routes.
-        String routeId =
-                TextUtils.isEmpty(address)
-                        ? null
-                        : mBluetoothRouteController.getRouteIdForBluetoothAddress(address);
+        String routeId = null;
+
         // We use the name from the port instead AudioDeviceInfo#getProductName because the latter
         // replaces empty names with the name of the device (example: Pixel 8). In that case we want
         // to derive a name ourselves from the type instead.
         String deviceName = audioDeviceInfo.getPort().name();
+
+        if (!TextUtils.isEmpty(address)) {
+            routeId = mBluetoothRouteController.getRouteIdForBluetoothAddress(address);
+            deviceName = mBluetoothRouteController.getNameForBluetoothAddress(address);
+        }
         return createMediaRoute2Info(routeId, audioDeviceInfo.getType(), deviceName, address);
     }
 
diff --git a/services/core/java/com/android/server/media/BluetoothDeviceRoutesManager.java b/services/core/java/com/android/server/media/BluetoothDeviceRoutesManager.java
index 8119628..b881ef6 100644
--- a/services/core/java/com/android/server/media/BluetoothDeviceRoutesManager.java
+++ b/services/core/java/com/android/server/media/BluetoothDeviceRoutesManager.java
@@ -119,6 +119,7 @@
                 BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
         deviceStateChangedIntentFilter.addAction(
                 BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
+        deviceStateChangedIntentFilter.addAction(BluetoothDevice.ACTION_ALIAS_CHANGED);
 
         mContext.registerReceiverAsUser(mDeviceStateChangedReceiver, user,
                 deviceStateChangedIntentFilter, null, null);
@@ -133,13 +134,17 @@
     @Nullable
     public synchronized String getRouteIdForBluetoothAddress(@Nullable String address) {
         BluetoothDevice bluetoothDevice = mAddressToBondedDevice.get(address);
-        // TODO: b/305199571 - Optimize the following statement to avoid creating the full
-        // MediaRoute2Info instance. We just need the id.
         return bluetoothDevice != null
-                ? createBluetoothRoute(bluetoothDevice).mRoute.getId()
+                ? getRouteIdForType(bluetoothDevice, getDeviceType(bluetoothDevice))
                 : null;
     }
 
+    @Nullable
+    public synchronized String getNameForBluetoothAddress(@NonNull String address) {
+        BluetoothDevice bluetoothDevice = mAddressToBondedDevice.get(address);
+        return bluetoothDevice != null ? getDeviceName(bluetoothDevice) : null;
+    }
+
     public synchronized void activateBluetoothDeviceWithAddress(String address) {
         BluetoothRouteInfo btRouteInfo = mBluetoothRoutes.get(address);
 
@@ -218,33 +223,12 @@
         BluetoothRouteInfo
                 newBtRoute = new BluetoothRouteInfo();
         newBtRoute.mBtDevice = device;
-        String deviceName =
-                Flags.enableUseOfBluetoothDeviceGetAliasForMr2infoGetName()
-                        ? device.getAlias()
-                        : device.getName();
-        if (TextUtils.isEmpty(deviceName)) {
-            deviceName = mContext.getResources().getText(R.string.unknownName).toString();
-        }
+        String deviceName = getDeviceName(device);
 
-        String routeId = device.getAddress();
-        int type = MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
-        newBtRoute.mConnectedProfiles = new SparseBooleanArray();
-        if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.A2DP, device)) {
-            newBtRoute.mConnectedProfiles.put(BluetoothProfile.A2DP, true);
-        }
-        if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.HEARING_AID, device)) {
-            newBtRoute.mConnectedProfiles.put(BluetoothProfile.HEARING_AID, true);
-            routeId = HEARING_AID_ROUTE_ID_PREFIX
-                    + mBluetoothProfileMonitor.getGroupId(BluetoothProfile.HEARING_AID, device);
-            type = MediaRoute2Info.TYPE_HEARING_AID;
-        }
-        if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.LE_AUDIO, device)) {
-            newBtRoute.mConnectedProfiles.put(BluetoothProfile.LE_AUDIO, true);
-            routeId = LE_AUDIO_ROUTE_ID_PREFIX
-                    + mBluetoothProfileMonitor.getGroupId(BluetoothProfile.LE_AUDIO, device);
-            type = MediaRoute2Info.TYPE_BLE_HEADSET;
-        }
+        int type = getDeviceType(device);
+        String routeId = getRouteIdForType(device, type);
 
+        newBtRoute.mConnectedProfiles = getConnectedProfiles(device);
         // Note that volume is only relevant for active bluetooth routes, and those are managed via
         // AudioManager.
         newBtRoute.mRoute =
@@ -262,6 +246,58 @@
         return newBtRoute;
     }
 
+    private String getDeviceName(BluetoothDevice device) {
+        String deviceName =
+                Flags.enableUseOfBluetoothDeviceGetAliasForMr2infoGetName()
+                        ? device.getAlias()
+                        : device.getName();
+        if (TextUtils.isEmpty(deviceName)) {
+            deviceName = mContext.getResources().getText(R.string.unknownName).toString();
+        }
+        return deviceName;
+    }
+    private SparseBooleanArray getConnectedProfiles(@NonNull BluetoothDevice device) {
+        SparseBooleanArray connectedProfiles = new SparseBooleanArray();
+        if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.A2DP, device)) {
+            connectedProfiles.put(BluetoothProfile.A2DP, true);
+        }
+        if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.HEARING_AID, device)) {
+            connectedProfiles.put(BluetoothProfile.HEARING_AID, true);
+        }
+        if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.LE_AUDIO, device)) {
+            connectedProfiles.put(BluetoothProfile.LE_AUDIO, true);
+        }
+
+        return connectedProfiles;
+    }
+
+    private int getDeviceType(@NonNull BluetoothDevice device) {
+        if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.LE_AUDIO, device)) {
+            return MediaRoute2Info.TYPE_BLE_HEADSET;
+        }
+
+        if (mBluetoothProfileMonitor.isProfileSupported(BluetoothProfile.HEARING_AID, device)) {
+            return MediaRoute2Info.TYPE_HEARING_AID;
+        }
+
+        return MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
+    }
+
+    private String getRouteIdForType(@NonNull BluetoothDevice device, int type) {
+        return switch (type) {
+            case (MediaRoute2Info.TYPE_BLE_HEADSET) ->
+                    LE_AUDIO_ROUTE_ID_PREFIX
+                            + mBluetoothProfileMonitor.getGroupId(
+                                    BluetoothProfile.LE_AUDIO, device);
+            case (MediaRoute2Info.TYPE_HEARING_AID) ->
+                    HEARING_AID_ROUTE_ID_PREFIX
+                            + mBluetoothProfileMonitor.getGroupId(
+                                    BluetoothProfile.HEARING_AID, device);
+            // TYPE_BLUETOOTH_A2DP
+            default -> device.getAddress();
+        };
+    }
+
     private static class BluetoothRouteInfo {
         private BluetoothDevice mBtDevice;
         private MediaRoute2Info mRoute;
@@ -300,6 +336,7 @@
                 case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED:
                 case BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED:
                 case BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED:
+                case BluetoothDevice.ACTION_ALIAS_CHANGED:
                     updateBluetoothRoutes();
                     notifyBluetoothRoutesUpdated();
             }
diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
index 8e37b4f..418eacc 100644
--- a/services/core/java/com/android/server/notification/ZenModeEventLogger.java
+++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java
@@ -436,7 +436,7 @@
          * Only available when {@code MODES_API} is active; otherwise returns an empty list.
          */
         int[] getActiveRuleTypes() {
-            if (!Flags.modesApi() || mNewZenMode == ZEN_MODE_OFF) {
+            if (!Flags.modesApi()) {
                 return new int[0];
             }
 
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index e394482..87b1769 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -747,7 +747,7 @@
                                 onInstallComplete(PackageManager.INSTALL_SUCCEEDED, mContext,
                                         onCompleteSender);
                             }
-                        });
+                        }, pkgSetting.getAppId(), callingUid, pkgSetting.isSystem());
                 restoreAndPostInstall(request);
             }
         } finally {
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index c10196f..4dcee04 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -158,6 +158,8 @@
     @Nullable
     private DomainSet mPreVerifiedDomains;
 
+    private int mInstallerUidForInstallExisting = INVALID_UID;
+
     // New install
     InstallRequest(InstallingSession params) {
         mUserId = params.getUser().getIdentifier();
@@ -180,7 +182,7 @@
 
     // Install existing package as user
     InstallRequest(int userId, int returnCode, AndroidPackage pkg, int[] newUsers,
-            Runnable runnable) {
+            Runnable runnable, int appId, int installerUid, boolean isSystem) {
         mUserId = userId;
         mInstallArgs = null;
         mReturnCode = returnCode;
@@ -191,6 +193,9 @@
         mIsInstallForUsers = true;
         mSessionId = -1;
         mRequireUserAction = USER_ACTION_UNSPECIFIED;
+        mAppId = appId;
+        mInstallerUidForInstallExisting = installerUid;
+        mSystem = isSystem;
     }
 
     // addForInit
@@ -381,7 +386,8 @@
 
     public int getInstallerPackageUid() {
         return (mInstallArgs != null && mInstallArgs.mInstallSource != null)
-                ? mInstallArgs.mInstallSource.mInstallerPackageUid : INVALID_UID;
+                ? mInstallArgs.mInstallSource.mInstallerPackageUid
+                : mInstallerUidForInstallExisting;
     }
 
     public int getDataLoaderType() {
diff --git a/services/core/java/com/android/server/pm/PackageMetrics.java b/services/core/java/com/android/server/pm/PackageMetrics.java
index 24d2a26..a0b6897 100644
--- a/services/core/java/com/android/server/pm/PackageMetrics.java
+++ b/services/core/java/com/android/server/pm/PackageMetrics.java
@@ -29,6 +29,7 @@
 
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.LocalServices;
+import com.android.server.pm.pkg.AndroidPackage;
 
 import java.io.File;
 import java.io.IOException;
@@ -111,17 +112,20 @@
 
         long versionCode = 0, apksSize = 0;
         if (success) {
-            // TODO: Remove temp try-catch to avoid IllegalStateException. The reason is because
-            //  the scan result is null for installExistingPackageAsUser(). Because it's installing
-            //  a package that's already existing, there's no scanning or parsing involved
-            try {
+            if (mInstallRequest.isInstallForUsers()) {
+                // In case of installExistingPackageAsUser, there's no scanned PackageSetting
+                // in the request but the pkg object should be readily available
+                AndroidPackage pkg = mInstallRequest.getPkg();
+                if (pkg != null) {
+                    versionCode = pkg.getLongVersionCode();
+                    apksSize = getApksSize(new File(pkg.getPath()));
+                }
+            } else {
                 final PackageSetting ps = mInstallRequest.getScannedPackageSetting();
                 if (ps != null) {
                     versionCode = ps.getVersionCode();
                     apksSize = getApksSize(ps.getPath());
                 }
-            } catch (IllegalStateException | NullPointerException e) {
-                // no-op
             }
         }
 
diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
index 73e7485..324637c 100644
--- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
+++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
@@ -717,8 +717,9 @@
                     break;
                 case UserManager.DISALLOW_RUN_IN_BACKGROUND:
                     if (newValue) {
-                        int currentUser = ActivityManager.getCurrentUser();
-                        if (currentUser != userId && userId != UserHandle.USER_SYSTEM) {
+                        final ActivityManager am = context.getSystemService(ActivityManager.class);
+                        if (!am.isProfileForeground(UserHandle.of(userId))
+                                && userId != UserHandle.USER_SYSTEM) {
                             try {
                                 ActivityManager.getService().stopUser(userId, false, null);
                             } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/power/PowerGroup.java b/services/core/java/com/android/server/power/PowerGroup.java
index 6a0fe90..9a9c4f2 100644
--- a/services/core/java/com/android/server/power/PowerGroup.java
+++ b/services/core/java/com/android/server/power/PowerGroup.java
@@ -431,8 +431,10 @@
     }
 
     boolean updateLocked(float screenBrightnessOverride, boolean useProximitySensor,
-            boolean boostScreenBrightness, int dozeScreenState, float dozeScreenBrightness,
-            boolean overrideDrawWakeLock, PowerSaveState powerSaverState, boolean quiescent,
+            boolean boostScreenBrightness, int dozeScreenState,
+            @Display.StateReason int dozeScreenStateReason,
+            float dozeScreenBrightness, boolean overrideDrawWakeLock,
+            PowerSaveState powerSaverState, boolean quiescent,
             boolean dozeAfterScreenOff, boolean bootCompleted,
             boolean screenBrightnessBoostInProgress, boolean waitForNegativeProximity,
             boolean brightWhenDozing) {
@@ -444,18 +446,28 @@
 
         if (mDisplayPowerRequest.policy == DisplayPowerRequest.POLICY_DOZE) {
             mDisplayPowerRequest.dozeScreenState = dozeScreenState;
+            mDisplayPowerRequest.dozeScreenStateReason = dozeScreenStateReason;
             if ((getWakeLockSummaryLocked() & WAKE_LOCK_DRAW) != 0 && !overrideDrawWakeLock) {
                 if (mDisplayPowerRequest.dozeScreenState == Display.STATE_DOZE_SUSPEND) {
                     mDisplayPowerRequest.dozeScreenState = Display.STATE_DOZE;
+                    mDisplayPowerRequest.dozeScreenStateReason =
+                            Display.STATE_REASON_DRAW_WAKE_LOCK;
                 }
                 if (mDisplayPowerRequest.dozeScreenState == Display.STATE_ON_SUSPEND) {
                     mDisplayPowerRequest.dozeScreenState = Display.STATE_ON;
+                    mDisplayPowerRequest.dozeScreenStateReason =
+                            Display.STATE_REASON_DRAW_WAKE_LOCK;
                 }
+            } else {
+                mDisplayPowerRequest.dozeScreenStateReason =
+                        Display.STATE_REASON_DEFAULT_POLICY;
             }
             mDisplayPowerRequest.dozeScreenBrightness = dozeScreenBrightness;
         } else {
             mDisplayPowerRequest.dozeScreenState = Display.STATE_UNKNOWN;
             mDisplayPowerRequest.dozeScreenBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
+            mDisplayPowerRequest.dozeScreenStateReason =
+                    Display.STATE_REASON_DEFAULT_POLICY;
         }
         mDisplayPowerRequest.lowPowerMode = powerSaverState.batterySaverEnabled;
         mDisplayPowerRequest.screenLowPowerBrightnessFactor = powerSaverState.brightnessFactor;
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index ea9ab30..3dec02e 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -640,6 +640,8 @@
     // The screen state to use while dozing.
     private int mDozeScreenStateOverrideFromDreamManager = Display.STATE_UNKNOWN;
 
+    private int mDozeScreenStateOverrideReasonFromDreamManager = Display.STATE_REASON_UNKNOWN;
+
     // The screen brightness to use while dozing.
     private int mDozeScreenBrightnessOverrideFromDreamManager = PowerManager.BRIGHTNESS_DEFAULT;
 
@@ -3574,6 +3576,7 @@
                 boolean ready = powerGroup.updateLocked(screenBrightnessOverride,
                         shouldUseProximitySensorLocked(), shouldBoostScreenBrightness(),
                         mDozeScreenStateOverrideFromDreamManager,
+                        mDozeScreenStateOverrideReasonFromDreamManager,
                         mDozeScreenBrightnessOverrideFromDreamManagerFloat,
                         mDrawWakeLockOverrideFromSidekick,
                         mBatterySaverSupported
@@ -4383,11 +4386,12 @@
     }
 
     private void setDozeOverrideFromDreamManagerInternal(
-            int screenState, int screenBrightness) {
+            int screenState, @Display.StateReason int reason, int screenBrightness) {
         synchronized (mLock) {
             if (mDozeScreenStateOverrideFromDreamManager != screenState
                     || mDozeScreenBrightnessOverrideFromDreamManager != screenBrightness) {
                 mDozeScreenStateOverrideFromDreamManager = screenState;
+                mDozeScreenStateOverrideReasonFromDreamManager = reason;
                 mDozeScreenBrightnessOverrideFromDreamManager = screenBrightness;
                 mDozeScreenBrightnessOverrideFromDreamManagerFloat =
                         BrightnessSynchronizer.brightnessIntToFloat(mDozeScreenBrightnessOverrideFromDreamManager);
@@ -6985,7 +6989,8 @@
         }
 
         @Override
-        public void setDozeOverrideFromDreamManager(int screenState, int screenBrightness) {
+        public void setDozeOverrideFromDreamManager(
+                int screenState, int reason, int screenBrightness) {
             switch (screenState) {
                 case Display.STATE_UNKNOWN:
                 case Display.STATE_OFF:
@@ -7002,7 +7007,7 @@
                     || screenBrightness > PowerManager.BRIGHTNESS_ON) {
                 screenBrightness = PowerManager.BRIGHTNESS_DEFAULT;
             }
-            setDozeOverrideFromDreamManagerInternal(screenState, screenBrightness);
+            setDozeOverrideFromDreamManagerInternal(screenState, reason, screenBrightness);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 1da9f25..6f16d2c 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -463,7 +463,7 @@
     }
 
     void drawMagnifiedRegionBorderIfNeeded(int displayId) {
-        if (Flags.magnificationAlwaysDrawFullscreenBorder()) {
+        if (Flags.alwaysDrawMagnificationFullscreenBorder()) {
             return;
         }
 
@@ -653,7 +653,7 @@
             mDisplayContent = displayContent;
             mDisplay = display;
             mHandler = new MyHandler(mService.mH.getLooper());
-            mMagnifiedViewport = Flags.magnificationAlwaysDrawFullscreenBorder()
+            mMagnifiedViewport = Flags.alwaysDrawMagnificationFullscreenBorder()
                     ? null : new MagnifiedViewport();
             mAccessibilityTracing =
                     AccessibilityController.getAccessibilityControllerInternal(mService);
@@ -697,7 +697,7 @@
                 mMagnificationSpec.clear();
             }
 
-            if (!Flags.magnificationAlwaysDrawFullscreenBorder()) {
+            if (!Flags.alwaysDrawMagnificationFullscreenBorder()) {
                 mMagnifiedViewport.setShowMagnifiedBorderIfNeeded();
             }
         }
@@ -708,7 +708,7 @@
                         FLAGS_MAGNIFICATION_CALLBACK, "activated=" + activated);
             }
             mIsFullscreenMagnificationActivated = activated;
-            if (!Flags.magnificationAlwaysDrawFullscreenBorder()) {
+            if (!Flags.alwaysDrawMagnificationFullscreenBorder()) {
                 mMagnifiedViewport.setMagnifiedRegionBorderShown(activated, true);
                 mMagnifiedViewport.showMagnificationBoundsIfNeeded();
             }
@@ -746,7 +746,7 @@
             }
 
             recomputeBounds();
-            if (!Flags.magnificationAlwaysDrawFullscreenBorder()) {
+            if (!Flags.alwaysDrawMagnificationFullscreenBorder()) {
                 mMagnifiedViewport.onDisplaySizeChanged();
             }
             mHandler.sendEmptyMessage(MyHandler.MESSAGE_NOTIFY_DISPLAY_SIZE_CHANGED);
@@ -908,7 +908,7 @@
                 mAccessibilityTracing.logTrace(LOG_TAG + ".destroy", FLAGS_MAGNIFICATION_CALLBACK);
             }
 
-            if (!Flags.magnificationAlwaysDrawFullscreenBorder()) {
+            if (!Flags.alwaysDrawMagnificationFullscreenBorder()) {
                 mMagnifiedViewport.destroyWindow();
             }
         }
@@ -919,7 +919,7 @@
                         FLAGS_MAGNIFICATION_CALLBACK);
             }
 
-            if (!Flags.magnificationAlwaysDrawFullscreenBorder()) {
+            if (!Flags.alwaysDrawMagnificationFullscreenBorder()) {
                 mMagnifiedViewport.drawWindowIfNeeded();
             }
         }
@@ -1039,14 +1039,14 @@
             }
             visibleWindows.clear();
 
-            if (!Flags.magnificationAlwaysDrawFullscreenBorder()) {
+            if (!Flags.alwaysDrawMagnificationFullscreenBorder()) {
                 mMagnifiedViewport.intersectWithDrawBorderInset(screenWidth, screenHeight);
             }
 
             final boolean magnifiedChanged =
                     !mOldMagnificationRegion.equals(mMagnificationRegion);
             if (magnifiedChanged) {
-                if (!Flags.magnificationAlwaysDrawFullscreenBorder()) {
+                if (!Flags.alwaysDrawMagnificationFullscreenBorder()) {
                     mMagnifiedViewport.updateBorderDrawingStatus(screenWidth, screenHeight);
                 }
                 mOldMagnificationRegion.set(mMagnificationRegion);
@@ -1129,7 +1129,7 @@
         }
 
         void dump(PrintWriter pw, String prefix) {
-            if (!Flags.magnificationAlwaysDrawFullscreenBorder()) {
+            if (!Flags.alwaysDrawMagnificationFullscreenBorder()) {
                 mMagnifiedViewport.dump(pw, prefix);
             }
         }
@@ -1235,7 +1235,7 @@
             }
 
             // TODO(291891390): Remove this class when we clean up the flag
-            //  magnificationAlwaysDrawFullscreenBorder
+            //  alwaysDrawMagnificationFullscreenBorder
             private final class ViewportWindow implements Runnable {
                 private static final String SURFACE_TITLE = "Magnification Overlay";
 
@@ -1540,7 +1540,7 @@
             public static final int MESSAGE_NOTIFY_DISPLAY_SIZE_CHANGED = 4;
 
             // TODO(291891390): Remove this field when we clean up the flag
-            //  magnificationAlwaysDrawFullscreenBorder
+            //  alwaysDrawMagnificationFullscreenBorder
             public static final int MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED = 5;
             public static final int MESSAGE_NOTIFY_IME_WINDOW_VISIBILITY_CHANGED = 6;
 
@@ -1569,7 +1569,7 @@
                     case MESSAGE_SHOW_MAGNIFIED_REGION_BOUNDS_IF_NEEDED : {
                         synchronized (mService.mGlobalLock) {
                             if (isFullscreenMagnificationActivated()) {
-                                if (!Flags.magnificationAlwaysDrawFullscreenBorder()) {
+                                if (!Flags.alwaysDrawMagnificationFullscreenBorder()) {
                                     mMagnifiedViewport.setMagnifiedRegionBorderShown(true, true);
                                 }
                                 mService.scheduleAnimationLocked();
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index a28259d..db2a1f4 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -265,7 +265,8 @@
         advanceTime(1);
 
         // The display should have been turned off
-        verify(mHolder.displayPowerState).setScreenState(Display.STATE_OFF);
+        verify(mHolder.displayPowerState)
+                .setScreenState(Display.STATE_OFF, Display.STATE_REASON_DEFAULT_POLICY);
 
         clearInvocations(mHolder.displayPowerState);
         when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_OFF);
@@ -276,13 +277,15 @@
         advanceTime(1);
 
         // The prox sensor is debounced so the display should not have been turned back on yet
-        verify(mHolder.displayPowerState, never()).setScreenState(Display.STATE_ON);
+        verify(mHolder.displayPowerState, never())
+                .setScreenState(Display.STATE_ON, Display.STATE_REASON_DEFAULT_POLICY);
 
         // Advance time by more than PROXIMITY_SENSOR_NEGATIVE_DEBOUNCE_DELAY
         advanceTime(1000);
 
         // The display should have been turned back on
-        verify(mHolder.displayPowerState).setScreenState(Display.STATE_ON);
+        verify(mHolder.displayPowerState)
+                .setScreenState(Display.STATE_ON, Display.STATE_REASON_DEFAULT_POLICY);
     }
 
     @Test
@@ -305,7 +308,8 @@
         advanceTime(1);
 
         // The display should have been turned off
-        verify(mHolder.displayPowerState).setScreenState(Display.STATE_OFF);
+        verify(mHolder.displayPowerState)
+                .setScreenState(Display.STATE_OFF, Display.STATE_REASON_DEFAULT_POLICY);
 
         when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_OFF);
         // The display device changes and we no longer have a prox sensor
@@ -318,7 +322,8 @@
 
         // The display should have been turned back on and the listener should have been
         // unregistered
-        verify(mHolder.displayPowerState).setScreenState(Display.STATE_ON);
+        verify(mHolder.displayPowerState)
+                .setScreenState(Display.STATE_ON, Display.STATE_REASON_DEFAULT_POLICY);
         verify(mSensorManagerMock).unregisterListener(listener);
     }
 
@@ -814,17 +819,17 @@
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
-        verify(mHolder.displayPowerState).setScreenState(anyInt());
+        verify(mHolder.displayPowerState).setScreenState(anyInt(), anyInt());
 
         mHolder = createDisplayPowerController(42, UNIQUE_ID);
 
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
-        verify(mHolder.displayPowerState, never()).setScreenState(anyInt());
+        verify(mHolder.displayPowerState, never()).setScreenState(anyInt(), anyInt());
 
         mHolder.dpc.onBootCompleted();
         advanceTime(1); // Run updatePowerState
-        verify(mHolder.displayPowerState).setScreenState(anyInt());
+        verify(mHolder.displayPowerState).setScreenState(anyInt(), anyInt());
     }
 
     @Test
@@ -1469,7 +1474,7 @@
         doAnswer(invocation -> {
             when(mHolder.displayPowerState.getScreenState()).thenReturn(invocation.getArgument(0));
             return null;
-        }).when(mHolder.displayPowerState).setScreenState(anyInt());
+        }).when(mHolder.displayPowerState).setScreenState(anyInt(), anyInt());
         mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
 
         // start with DOZE.
@@ -1479,10 +1484,12 @@
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        mHolder.dpc.overrideDozeScreenState(supportedTargetState);
+        mHolder.dpc.overrideDozeScreenState(
+                supportedTargetState, Display.STATE_REASON_DEFAULT_POLICY);
         advanceTime(1); // Run updatePowerState
 
-        verify(mHolder.displayPowerState).setScreenState(supportedTargetState);
+        verify(mHolder.displayPowerState)
+                .setScreenState(supportedTargetState, Display.STATE_REASON_DEFAULT_POLICY);
     }
 
     @Test
@@ -1495,7 +1502,7 @@
         doAnswer(invocation -> {
             when(mHolder.displayPowerState.getScreenState()).thenReturn(invocation.getArgument(0));
             return null;
-        }).when(mHolder.displayPowerState).setScreenState(anyInt());
+        }).when(mHolder.displayPowerState).setScreenState(anyInt(), anyInt());
         mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
 
         // start with DOZE.
@@ -1505,10 +1512,12 @@
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        mHolder.dpc.overrideDozeScreenState(unSupportedTargetState);
+        mHolder.dpc.overrideDozeScreenState(
+                unSupportedTargetState, Display.STATE_REASON_DEFAULT_POLICY);
         advanceTime(1); // Run updatePowerState
 
-        verify(mHolder.displayPowerState, never()).setScreenState(anyInt());
+        verify(mHolder.displayPowerState, never())
+                .setScreenState(anyInt(), eq(Display.STATE_REASON_DEFAULT_POLICY));
     }
 
     @Test
@@ -1520,7 +1529,7 @@
         doAnswer(invocation -> {
             when(mHolder.displayPowerState.getScreenState()).thenReturn(invocation.getArgument(0));
             return null;
-        }).when(mHolder.displayPowerState).setScreenState(anyInt());
+        }).when(mHolder.displayPowerState).setScreenState(anyInt(), anyInt());
         mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
 
         // start with OFF.
@@ -1530,10 +1539,11 @@
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        mHolder.dpc.overrideDozeScreenState(supportedTargetState);
+        mHolder.dpc.overrideDozeScreenState(
+                supportedTargetState, Display.STATE_REASON_DEFAULT_POLICY);
         advanceTime(1); // Run updatePowerState
 
-        verify(mHolder.displayPowerState, never()).setScreenState(anyInt());
+        verify(mHolder.displayPowerState, never()).setScreenState(anyInt(), anyInt());
     }
 
     @Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java
index f5c6bb2..de53266 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java
@@ -18,11 +18,13 @@
 
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.hardware.display.DisplayManagerInternal;
+import android.util.Pair;
 import android.view.Display;
 
 import androidx.test.filters.SmallTest;
@@ -61,9 +63,11 @@
                 DisplayManagerInternal.DisplayPowerRequest.class);
 
         displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF;
-        int state = mDisplayStateController.updateDisplayState(displayPowerRequest, DISPLAY_ENABLED,
-                !DISPLAY_IN_TRANSITION);
-        assertEquals(Display.STATE_OFF, state);
+        Pair<Integer, Integer> stateAndReason =
+                mDisplayStateController.updateDisplayState(
+                        displayPowerRequest, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION);
+        assertTrue(Display.STATE_OFF == stateAndReason.first);
+        assertTrue(Display.STATE_REASON_DEFAULT_POLICY == stateAndReason.second);
         verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest,
                 Display.STATE_OFF);
         assertEquals(true, mDisplayStateController.shouldPerformScreenOffTransition());
@@ -101,9 +105,11 @@
                 DisplayManagerInternal.DisplayPowerRequest.class);
 
         displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
-        int state = mDisplayStateController.updateDisplayState(displayPowerRequest,
-                !DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION);
-        assertEquals(Display.STATE_OFF, state);
+        Pair<Integer, Integer> stateAndReason =
+                mDisplayStateController.updateDisplayState(
+                        displayPowerRequest, !DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION);
+        assertTrue(Display.STATE_OFF == stateAndReason.first);
+        assertTrue(Display.STATE_REASON_DEFAULT_POLICY == stateAndReason.second);
         verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest,
                 Display.STATE_ON);
         assertEquals(false, mDisplayStateController.shouldPerformScreenOffTransition());
@@ -117,9 +123,11 @@
                 DisplayManagerInternal.DisplayPowerRequest.class);
 
         displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
-        int state = mDisplayStateController.updateDisplayState(displayPowerRequest, DISPLAY_ENABLED,
-                DISPLAY_IN_TRANSITION);
-        assertEquals(Display.STATE_OFF, state);
+        Pair<Integer, Integer> stateAndReason =
+                mDisplayStateController.updateDisplayState(
+                        displayPowerRequest, DISPLAY_ENABLED, DISPLAY_IN_TRANSITION);
+        assertTrue(Display.STATE_OFF == stateAndReason.first);
+        assertTrue(Display.STATE_REASON_DEFAULT_POLICY == stateAndReason.second);
         verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest,
                 Display.STATE_ON);
         assertEquals(false, mDisplayStateController.shouldPerformScreenOffTransition());
@@ -133,9 +141,11 @@
                 DisplayManagerInternal.DisplayPowerRequest.class);
 
         displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT;
-        int state = mDisplayStateController.updateDisplayState(displayPowerRequest, DISPLAY_ENABLED,
-                !DISPLAY_IN_TRANSITION);
-        assertEquals(Display.STATE_OFF, state);
+        Pair<Integer, Integer> stateAndReason =
+                mDisplayStateController.updateDisplayState(
+                        displayPowerRequest, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION);
+        assertTrue(Display.STATE_OFF == stateAndReason.first);
+        assertTrue(Display.STATE_REASON_DEFAULT_POLICY == stateAndReason.second);
         verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest,
                 Display.STATE_ON);
         assertEquals(false, mDisplayStateController.shouldPerformScreenOffTransition());
@@ -146,12 +156,15 @@
         DisplayManagerInternal.DisplayPowerRequest displayPowerRequest =
                 new DisplayManagerInternal.DisplayPowerRequest();
         displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
-        mDisplayStateController.overrideDozeScreenState(Display.STATE_DOZE_SUSPEND);
+        mDisplayStateController.overrideDozeScreenState(
+                Display.STATE_DOZE_SUSPEND, Display.STATE_REASON_OFFLOAD);
 
-        int state = mDisplayStateController.updateDisplayState(displayPowerRequest, DISPLAY_ENABLED,
-                !DISPLAY_IN_TRANSITION);
+        Pair<Integer, Integer> stateAndReason =
+                mDisplayStateController.updateDisplayState(
+                        displayPowerRequest, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION);
 
-        assertEquals(state, Display.STATE_DOZE_SUSPEND);
+        assertTrue(Display.STATE_DOZE_SUSPEND == stateAndReason.first);
+        assertTrue(Display.STATE_REASON_OFFLOAD == stateAndReason.second);
     }
 
     @Test
@@ -159,12 +172,15 @@
         DisplayManagerInternal.DisplayPowerRequest displayPowerRequest =
                 new DisplayManagerInternal.DisplayPowerRequest();
         displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF;
-        mDisplayStateController.overrideDozeScreenState(Display.STATE_DOZE_SUSPEND);
+        mDisplayStateController.overrideDozeScreenState(
+                Display.STATE_DOZE_SUSPEND, Display.STATE_REASON_DEFAULT_POLICY);
 
-        int state = mDisplayStateController.updateDisplayState(displayPowerRequest, DISPLAY_ENABLED,
-                !DISPLAY_IN_TRANSITION);
+        Pair<Integer, Integer> stateAndReason =
+                mDisplayStateController.updateDisplayState(
+                        displayPowerRequest, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION);
 
-        assertEquals(state, Display.STATE_OFF);
+        assertTrue(Display.STATE_OFF == stateAndReason.first);
+        assertTrue(Display.STATE_REASON_DEFAULT_POLICY == stateAndReason.second);
     }
 
     private void validDisplayState(int policy, int displayState, boolean isEnabled,
@@ -172,9 +188,10 @@
         DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock(
                 DisplayManagerInternal.DisplayPowerRequest.class);
         displayPowerRequest.policy = policy;
-        int state = mDisplayStateController.updateDisplayState(displayPowerRequest, isEnabled,
-                isInTransition);
-        assertEquals(displayState, state);
+        Pair<Integer, Integer> stateAndReason =
+                mDisplayStateController.updateDisplayState(
+                        displayPowerRequest, isEnabled, isInTransition);
+        assertTrue(displayState == stateAndReason.first);
         verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest,
                 displayState);
         assertEquals(false, mDisplayStateController.shouldPerformScreenOffTransition());
diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerGroupTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerGroupTest.java
index a776eec..94b8d68 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/PowerGroupTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/PowerGroupTest.java
@@ -260,6 +260,7 @@
                 /* useProximitySensor= */ false,
                 /* boostScreenBrightness= */ false,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
+                /* dozeScreenStateReason= */ Display.STATE_REASON_DEFAULT_POLICY,
                 /* dozeScreenBrightness= */ BRIGHTNESS_DOZE,
                 /* overrideDrawWakeLock= */ false,
                 powerSaveState,
@@ -299,6 +300,7 @@
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
+                /* dozeScreenStateReason= */ Display.STATE_REASON_DEFAULT_POLICY,
                 /* dozeScreenBrightness= */ BRIGHTNESS_DOZE,
                 /* overrideDrawWakeLock= */ false,
                 powerSaveState,
@@ -337,6 +339,7 @@
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
+                /* dozeScreenStateReason= */ Display.STATE_REASON_DEFAULT_POLICY,
                 /* dozeScreenBrightness= */ BRIGHTNESS_DOZE,
                 /* overrideDrawWakeLock= */ false,
                 powerSaveState,
@@ -374,6 +377,7 @@
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
+                /* dozeScreenStateReason= */ Display.STATE_REASON_DEFAULT_POLICY,
                 /* dozeScreenBrightness= */ BRIGHTNESS_DOZE,
                 /* overrideDrawWakeLock= */ false,
                 powerSaveState,
@@ -411,6 +415,7 @@
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
+                /* dozeScreenStateReason= */ Display.STATE_REASON_DEFAULT_POLICY,
                 /* dozeScreenBrightness= */ BRIGHTNESS_DOZE,
                 /* overrideDrawWakeLock= */ false,
                 powerSaveState,
@@ -449,6 +454,7 @@
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
+                /* dozeScreenStateReason= */ Display.STATE_REASON_DEFAULT_POLICY,
                 /* dozeScreenBrightness= */ BRIGHTNESS_DOZE,
                 /* overrideDrawWakeLock= */ false,
                 powerSaveState,
@@ -485,6 +491,7 @@
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
+                /* dozeScreenStateReason= */ Display.STATE_REASON_DEFAULT_POLICY,
                 /* dozeScreenBrightness= */ BRIGHTNESS_DOZE,
                 /* overrideDrawWakeLock= */ false,
                 powerSaveState,
@@ -522,6 +529,7 @@
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
+                /* dozeScreenStateReason= */ Display.STATE_REASON_DEFAULT_POLICY,
                 /* dozeScreenBrightness= */ BRIGHTNESS_DOZE,
                 /* overrideDrawWakeLock= */ false,
                 powerSaveState,
@@ -558,6 +566,7 @@
                 /* useProximitySensor= */ true,
                 /* boostScreenBrightness= */ true,
                 /* dozeScreenStateOverride= */ Display.STATE_ON,
+                /* dozeScreenStateReason= */ Display.STATE_REASON_DEFAULT_POLICY,
                 /* dozeScreenBrightness= */ BRIGHTNESS_DOZE,
                 /* overrideDrawWakeLock= */ false,
                 powerSaveState,
diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
index f86ff14..52f28e9 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -1293,7 +1293,10 @@
 
         // Override the display state by DreamManager and verify is reacquires the blocker.
         mService.getLocalServiceInstance()
-                .setDozeOverrideFromDreamManager(Display.STATE_ON, PowerManager.BRIGHTNESS_DEFAULT);
+                .setDozeOverrideFromDreamManager(
+                        Display.STATE_ON,
+                        Display.STATE_REASON_DEFAULT_POLICY,
+                        PowerManager.BRIGHTNESS_DEFAULT);
         assertTrue(isAcquired[0]);
     }
 
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 65986ea..94a71be 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -270,3 +270,89 @@
         "done && " +
         "$(location soong_zip) -o $(out) -C $(genDir)/res -D $(genDir)/res",
 }
+
+FLAKY_AND_IGNORED = [
+    "androidx.test.filters.FlakyTest",
+    "org.junit.Ignore",
+]
+// Used by content protection TEST_MAPPING
+test_module_config {
+    name: "FrameworksServicesTests_contentprotection",
+    base: "FrameworksServicesTests",
+    test_suites: ["general-tests"],
+    include_filters: ["com.android.server.contentprotection"],
+    exclude_annotations: FLAKY_AND_IGNORED,
+}
+
+test_module_config {
+    name: "FrameworksServicesTests_om",
+    base: "FrameworksServicesTests",
+    test_suites: ["general-tests"],
+    include_filters: ["com.android.server.om."],
+    exclude_annotations: FLAKY_AND_IGNORED,
+}
+
+// Used by contexthub TEST_MAPPING
+test_module_config {
+    name: "FrameworksServicesTests_contexthub_presubmit",
+    base: "FrameworksServicesTests",
+    test_suites: ["general-tests"],
+    include_filters: ["com.android.server.location.contexthub."],
+    // TODO(ron): are these right, does it run anything?
+    include_annotations: ["android.platform.test.annotations.Presubmit"],
+    exclude_annotations: FLAKY_AND_IGNORED,
+}
+
+test_module_config {
+    name: "FrameworksServicesTests_contexthub_postsubmit",
+    base: "FrameworksServicesTests",
+    test_suites: ["general-tests"],
+    include_filters: ["com.android.server.location.contexthub."],
+    // TODO(ron): are these right, does it run anything?
+    include_annotations: ["android.platform.test.annotations.Postsubmit"],
+    exclude_annotations: FLAKY_AND_IGNORED,
+}
+
+// Used by contentcapture
+test_module_config {
+    name: "FrameworksServicesTests_contentcapture",
+    base: "FrameworksServicesTests",
+    test_suites: ["general-tests"],
+    include_filters: ["com.android.server.contentcapture"],
+    exclude_annotations: FLAKY_AND_IGNORED,
+}
+
+test_module_config {
+    name: "FrameworksServicesTests_recoverysystem",
+    base: "FrameworksServicesTests",
+    test_suites: ["general-tests"],
+    include_filters: ["com.android.server.recoverysystem."],
+    exclude_annotations: ["androidx.test.filters.FlakyTest"],
+}
+
+// server pm TEST_MAPPING
+test_module_config {
+    name: "FrameworksServicesTests_pm_presubmit",
+    base: "FrameworksServicesTests",
+    test_suites: ["general-tests"],
+    include_annotations: ["android.platform.test.annotations.Presubmit"],
+    include_filters: ["com.android.server.pm."],
+    exclude_annotations: FLAKY_AND_IGNORED,
+}
+
+test_module_config {
+    name: "FrameworksServicesTests_pm_postsubmit",
+    base: "FrameworksServicesTests",
+    test_suites: ["general-tests"],
+    include_annotations: ["android.platform.test.annotations.Postsubmit"],
+    include_filters: ["com.android.server.pm."],
+    exclude_annotations: FLAKY_AND_IGNORED,
+}
+
+// server os TEST_MAPPING
+test_module_config {
+    name: "FrameworksServicesTests_os",
+    base: "FrameworksServicesTests",
+    test_suites: ["general-tests"],
+    include_filters: ["com.android.server.os."],
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
index 1a51c45..58567ca 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java
@@ -1272,7 +1272,7 @@
     }
 
     @Test
-    @RequiresFlagsEnabled(Flags.FLAG_MAGNIFICATION_ALWAYS_DRAW_FULLSCREEN_BORDER)
+    @RequiresFlagsEnabled(Flags.FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER)
     public void onFullscreenMagnificationActivationState_systemUiBorderFlagOn_notifyConnection() {
         mMagnificationController.onFullScreenMagnificationActivationState(
                 TEST_DISPLAY, /* activated= */ true);
@@ -1282,7 +1282,7 @@
     }
 
     @Test
-    @RequiresFlagsDisabled(Flags.FLAG_MAGNIFICATION_ALWAYS_DRAW_FULLSCREEN_BORDER)
+    @RequiresFlagsDisabled(Flags.FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER)
     public void
             onFullscreenMagnificationActivationState_systemUiBorderFlagOff_neverNotifyConnection() {
         mMagnificationController.onFullScreenMagnificationActivationState(
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
index 527bc5b..f6da411 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java
@@ -20,6 +20,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.eq;
@@ -29,7 +30,9 @@
 import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertThrows;
 
+import android.hardware.biometrics.BiometricConstants;
 import android.os.Handler;
+import android.os.RemoteException;
 import android.platform.test.annotations.Presubmit;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -72,6 +75,8 @@
     @Mock
     private BaseClientMonitor mNonInterruptableClientMonitor;
     @Mock
+    private ClientMonitorCallbackConverter mListener;
+    @Mock
     private ClientMonitorCallback mClientCallback;
     @Mock
     private ClientMonitorCallback mOnStartCallback;
@@ -435,17 +440,18 @@
     }
 
     @Test
-    public void cancelWatchdogWhenStarted() {
+    public void cancelWatchdogWhenStarted() throws RemoteException {
         cancelWatchdog(true);
     }
 
     @Test
-    public void cancelWatchdogWithoutStarting() {
+    public void cancelWatchdogWithoutStarting() throws RemoteException {
         cancelWatchdog(false);
     }
 
-    private void cancelWatchdog(boolean start) {
+    private void cancelWatchdog(boolean start) throws RemoteException {
         when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal);
+        when(mInterruptableClientMonitor.getListener()).thenReturn(mListener);
 
         mInterruptableOperation.start(mOnStartCallback);
         if (start) {
@@ -461,6 +467,8 @@
 
         assertThat(mInterruptableOperation.isFinished()).isTrue();
         assertThat(mInterruptableOperation.isCanceling()).isFalse();
+        verify(mInterruptableClientMonitor.getListener()).onError(anyInt(), anyInt(), eq(
+                BiometricConstants.BIOMETRIC_ERROR_CANCELED), eq(0));
         verify(mOnStartCallback).onClientFinished(eq(mInterruptableClientMonitor), eq(false));
         verify(mInterruptableClientMonitor).destroy();
     }
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
index 981eba5..971323a 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
@@ -135,6 +135,8 @@
     private ISession mSession;
     @Mock
     private IFingerprint mFingerprint;
+    @Mock
+    private ClientMonitorCallbackConverter mListener;
 
     @Before
     public void setUp() {
@@ -206,7 +208,7 @@
         // Pretend the scheduler is busy so the first operation doesn't start right away. We want
         // to pretend like there are two operations in the queue before kicking things off
         mScheduler.mCurrentOperation = new BiometricSchedulerOperation(
-                mock(BaseClientMonitor.class), mock(ClientMonitorCallback.class));
+                createBaseClientMonitor(), mock(ClientMonitorCallback.class));
 
         mScheduler.scheduleClientMonitor(client1, callback1);
         assertEquals(1, mScheduler.mPendingOperations.size());
@@ -244,7 +246,7 @@
         // Pretend the scheduler is busy so the first operation doesn't start right away. We want
         // to pretend like there are two operations in the queue before kicking things off
         mScheduler.mCurrentOperation = new BiometricSchedulerOperation(
-                mock(BaseClientMonitor.class), mock(ClientMonitorCallback.class));
+                createBaseClientMonitor(), mock(ClientMonitorCallback.class));
 
         mScheduler.scheduleClientMonitor(client1, callback1);
         assertEquals(1, mScheduler.mPendingOperations.size());
@@ -612,10 +614,10 @@
 
     @Test
     public void testInterruptPrecedingClients_whenExpected() {
-        final BaseClientMonitor interruptableMonitor = mock(BaseClientMonitor.class);
+        final BaseClientMonitor interruptableMonitor = createBaseClientMonitor();
         when(interruptableMonitor.isInterruptable()).thenReturn(true);
 
-        final BaseClientMonitor interrupter = mock(BaseClientMonitor.class);
+        final BaseClientMonitor interrupter = createBaseClientMonitor();
         when(interrupter.interruptsPrecedingClients()).thenReturn(true);
 
         mScheduler.scheduleClientMonitor(interruptableMonitor);
@@ -628,10 +630,10 @@
 
     @Test
     public void testDoesNotInterruptPrecedingClients_whenNotExpected() {
-        final BaseClientMonitor interruptableMonitor = mock(BaseClientMonitor.class);
+        final BaseClientMonitor interruptableMonitor = createBaseClientMonitor();
         when(interruptableMonitor.isInterruptable()).thenReturn(true);
 
-        final BaseClientMonitor interrupter = mock(BaseClientMonitor.class);
+        final BaseClientMonitor interrupter = createBaseClientMonitor();
         when(interrupter.interruptsPrecedingClients()).thenReturn(false);
 
         mScheduler.scheduleClientMonitor(interruptableMonitor);
@@ -741,7 +743,7 @@
         //Start watchdog
         mScheduler.startWatchdog();
         waitForIdle();
-        mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class),
+        mScheduler.scheduleClientMonitor(createBaseClientMonitor(),
                 mock(ClientMonitorCallback.class));
         waitForIdle();
 
@@ -775,9 +777,9 @@
         //Start watchdog
         mScheduler.startWatchdog();
         waitForIdle();
-        mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class),
+        mScheduler.scheduleClientMonitor(createBaseClientMonitor(),
                 mock(ClientMonitorCallback.class));
-        mScheduler.scheduleClientMonitor(mock(BaseClientMonitor.class),
+        mScheduler.scheduleClientMonitor(createBaseClientMonitor(),
                 mock(ClientMonitorCallback.class));
         waitForIdle();
 
@@ -857,7 +859,7 @@
     public void testScheduleOperation_whenNoUser() {
         mCurrentUserId = UserHandle.USER_NULL;
 
-        final BaseClientMonitor nextClient = mock(BaseClientMonitor.class);
+        final BaseClientMonitor nextClient = createBaseClientMonitor();
         when(nextClient.getTargetUserId()).thenReturn(0);
 
         mScheduler.scheduleClientMonitor(nextClient);
@@ -875,9 +877,9 @@
         mStartOperationsFinish = false;
 
         final BaseClientMonitor[] nextClients = new BaseClientMonitor[]{
-                mock(BaseClientMonitor.class),
-                mock(BaseClientMonitor.class),
-                mock(BaseClientMonitor.class)
+                createBaseClientMonitor(),
+                createBaseClientMonitor(),
+                createBaseClientMonitor()
         };
         for (BaseClientMonitor client : nextClients) {
             when(client.getTargetUserId()).thenReturn(5);
@@ -899,7 +901,7 @@
         mCurrentUserId = UserHandle.USER_NULL;
         mStartOperationsFinish = false;
 
-        final BaseClientMonitor client = mock(BaseClientMonitor.class);
+        final BaseClientMonitor client = createBaseClientMonitor();
 
         when(client.getTargetUserId()).thenReturn(5);
 
@@ -913,7 +915,7 @@
         assertThat(mScheduler.mCurrentOperation).isNull();
 
         final BiometricSchedulerOperation fakeOperation = new BiometricSchedulerOperation(
-                mock(BaseClientMonitor.class), new ClientMonitorCallback() {});
+                createBaseClientMonitor(), new ClientMonitorCallback() {});
         mScheduler.mCurrentOperation = fakeOperation;
         startUserClient.mCallback.onClientFinished(startUserClient, true);
 
@@ -925,7 +927,7 @@
     public void testScheduleOperation_whenSameUser() {
         mCurrentUserId = 10;
 
-        BaseClientMonitor nextClient = mock(BaseClientMonitor.class);
+        BaseClientMonitor nextClient = createBaseClientMonitor();
         when(nextClient.getTargetUserId()).thenReturn(mCurrentUserId);
 
         mScheduler.scheduleClientMonitor(nextClient);
@@ -943,7 +945,7 @@
         mCurrentUserId = 10;
 
         final int nextUserId = 11;
-        BaseClientMonitor nextClient = mock(BaseClientMonitor.class);
+        BaseClientMonitor nextClient = createBaseClientMonitor();
         when(nextClient.getTargetUserId()).thenReturn(nextUserId);
 
         mScheduler.scheduleClientMonitor(nextClient);
@@ -963,7 +965,7 @@
     public void testStartUser_alwaysStartsNextOperation() {
         mCurrentUserId = UserHandle.USER_NULL;
 
-        BaseClientMonitor nextClient = mock(BaseClientMonitor.class);
+        BaseClientMonitor nextClient = createBaseClientMonitor();
         when(nextClient.getTargetUserId()).thenReturn(10);
 
         mScheduler.scheduleClientMonitor(nextClient);
@@ -977,7 +979,7 @@
 
         // schedule second operation but swap out the current operation
         // before it runs so that it's not current when it's completion callback runs
-        nextClient = mock(BaseClientMonitor.class);
+        nextClient = createBaseClientMonitor();
         when(nextClient.getTargetUserId()).thenReturn(11);
         mScheduler.scheduleClientMonitor(nextClient);
 
@@ -994,7 +996,7 @@
 
         // When a stop user client fails, check that mStopUserClient
         // is set to null to prevent the scheduler from getting stuck.
-        BaseClientMonitor nextClient = mock(BaseClientMonitor.class);
+        BaseClientMonitor nextClient = createBaseClientMonitor();
         when(nextClient.getTargetUserId()).thenReturn(10);
 
         mScheduler.scheduleClientMonitor(nextClient);
@@ -1008,7 +1010,7 @@
 
         // schedule second operation but swap out the current operation
         // before it runs so that it's not current when it's completion callback runs
-        nextClient = mock(BaseClientMonitor.class);
+        nextClient = createBaseClientMonitor();
         when(nextClient.getTargetUserId()).thenReturn(11);
         mShouldFailStopUser = true;
         mScheduler.scheduleClientMonitor(nextClient);
@@ -1023,6 +1025,13 @@
         return BiometricSchedulerProto.parseFrom(mScheduler.dumpProtoState(clearSchedulerBuffer));
     }
 
+    private BaseClientMonitor createBaseClientMonitor() {
+        BaseClientMonitor client = mock(BaseClientMonitor.class);
+        when(client.getListener()).thenReturn(mListener);
+
+        return client;
+    }
+
     private void waitForIdle() {
         TestableLooper.get(this).processAllMessages();
     }
diff --git a/services/tests/servicestests/src/com/android/server/contentcapture/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/contentcapture/TEST_MAPPING
index 0ffa891..dae8f93 100644
--- a/services/tests/servicestests/src/com/android/server/contentcapture/TEST_MAPPING
+++ b/services/tests/servicestests/src/com/android/server/contentcapture/TEST_MAPPING
@@ -14,5 +14,11 @@
         }
       ]
     }
+  ],
+  "postsubmit": [
+    {
+      // b/331020193, Move to presubmit early april 2024
+      "name": "FrameworksServicesTests_contentcapture"
+    }
   ]
 }
diff --git a/services/tests/servicestests/src/com/android/server/contentprotection/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/contentprotection/TEST_MAPPING
index 419508c..32729a8 100644
--- a/services/tests/servicestests/src/com/android/server/contentprotection/TEST_MAPPING
+++ b/services/tests/servicestests/src/com/android/server/contentprotection/TEST_MAPPING
@@ -14,5 +14,11 @@
         }
       ]
     }
+  ],
+  "postsubmit": [
+    {
+      // b/331020193, Move to presubmit early april 2024
+      "name": "FrameworksServicesTests_contentprotection"
+    }
   ]
 }
diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/location/contexthub/TEST_MAPPING
index 6035250..dc8f934 100644
--- a/services/tests/servicestests/src/com/android/server/location/contexthub/TEST_MAPPING
+++ b/services/tests/servicestests/src/com/android/server/location/contexthub/TEST_MAPPING
@@ -20,12 +20,18 @@
   ],
   "postsubmit": [
     {
+      // b/331020193, Move to presubmit early april 2024
+      "name": "FrameworksServicesTests_contexthub_presubmit"
+    },
+    {
       "name": "FrameworksServicesTests",
       "options": [
         {
           "include-filter": "com.android.server.location.contexthub."
         },
         {
+          // I believe this include annotation is preventing tests from being run
+          // as there are no matching tests with the Postsubmit annotation.
           "include-annotation": "android.platform.test.annotations.Postsubmit"
         },
         {
diff --git a/services/tests/servicestests/src/com/android/server/om/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/om/TEST_MAPPING
index 558e259..41c4383 100644
--- a/services/tests/servicestests/src/com/android/server/om/TEST_MAPPING
+++ b/services/tests/servicestests/src/com/android/server/om/TEST_MAPPING
@@ -16,5 +16,11 @@
         }
       ]
     }
+  ],
+  "postsubmit": [
+    {
+      // b/331020193, Move to presubmit early april 2024
+      "name": "FrameworksServicesTests_om"
+    }
   ]
 }
diff --git a/services/tests/servicestests/src/com/android/server/os/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/os/TEST_MAPPING
index 5a46f8c4..06e7002 100644
--- a/services/tests/servicestests/src/com/android/server/os/TEST_MAPPING
+++ b/services/tests/servicestests/src/com/android/server/os/TEST_MAPPING
@@ -8,5 +8,11 @@
         }
       ]
     }
+  ],
+  "postsubmit": [
+    {
+      // b/331020193, Move to presubmit early april 2024
+      "name": "FrameworksServicesTests_os"
+    }
   ]
 }
diff --git a/services/tests/servicestests/src/com/android/server/pm/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/pm/TEST_MAPPING
index 85a73bb..f4e724f 100644
--- a/services/tests/servicestests/src/com/android/server/pm/TEST_MAPPING
+++ b/services/tests/servicestests/src/com/android/server/pm/TEST_MAPPING
@@ -20,21 +20,13 @@
   ],
   "postsubmit": [
     {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.pm."
-        },
-        {
-          "include-annotation": "android.platform.test.annotations.Postsubmit"
-        },
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
-        },
-        {
-          "exclude-annotation": "org.junit.Ignore"
-        }
-      ]
+      // Presubmit is intentional here while testing with SLO checker.
+      // b/331020193, Move to presubmit early april 2024
+      "name": "FrameworksServicesTests_pm_presubmit"
+    },
+    {
+      // Leave postsubmit here when migrating
+      "name": "FrameworksServicesTests_pm_postsubmit"
     }
   ]
 }
diff --git a/services/tests/servicestests/src/com/android/server/recoverysystem/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/recoverysystem/TEST_MAPPING
index e9d8b2e..7e7393c 100644
--- a/services/tests/servicestests/src/com/android/server/recoverysystem/TEST_MAPPING
+++ b/services/tests/servicestests/src/com/android/server/recoverysystem/TEST_MAPPING
@@ -11,5 +11,11 @@
                 }
             ]
         }
+    ],
+    "postsubmit": [
+      {
+        // b/331020193, Move to presubmit early april 2024
+        "name": "FrameworksServicesTests_recoverysystem"
+      }
     ]
 }
\ No newline at end of file
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index c4d2460..6f07472 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -3634,12 +3634,11 @@
         mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
         setupZenConfig();
 
-        // Event 1: turn on manual zen mode. Manual rule will have ACTIVE_RULE_TYPE_MANUAL
-        mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
-                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null, Process.SYSTEM_UID);
-
         // Create bedtime rule
+        // This one has INTERRUPTION_FILTER_ALL to make sure active rules still count when zen mode
+        // (in the notification filtering sense) is not on
         AutomaticZenRule bedtime = new AutomaticZenRule.Builder("Bedtime Mode (TM)", CONDITION_ID)
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALL)
                 .setType(TYPE_BEDTIME)
                 .build();
         String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule(mPkg, bedtime, UPDATE_ORIGIN_APP,
@@ -3652,17 +3651,21 @@
         String immersiveId = mZenModeHelper.addAutomaticZenRule(mPkg, immersive, UPDATE_ORIGIN_APP,
                 "reason", CUSTOM_PKG_UID);
 
-        // Event 2: Activate bedtime rule
+        // Event 1: Activate bedtime rule. This doesn't turn on notification filtering
         mZenModeHelper.setAutomaticZenRuleState(bedtimeRuleId,
                 new Condition(bedtime.getConditionId(), "", STATE_TRUE, SOURCE_SCHEDULE),
                 UPDATE_ORIGIN_APP, CUSTOM_PKG_UID);
 
+        // Event 2: turn on manual zen mode. Manual rule will have ACTIVE_RULE_TYPE_MANUAL
+        mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null,
+                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "", null, Process.SYSTEM_UID);
+
         // Event 3: Turn immersive on
         mZenModeHelper.setAutomaticZenRuleState(immersiveId,
                 new Condition(immersive.getConditionId(), "", STATE_TRUE, SOURCE_SCHEDULE),
                 UPDATE_ORIGIN_APP, CUSTOM_PKG_UID);
 
-        // Event 4: Turn off bedtime mode, leaving just unknown + immersive
+        // Event 4: Turn off bedtime mode, leaving just manual + immersive
         mZenModeHelper.setAutomaticZenRuleState(bedtimeRuleId,
                 new Condition(bedtime.getConditionId(), "", STATE_FALSE, SOURCE_SCHEDULE),
                 UPDATE_ORIGIN_APP, CUSTOM_PKG_UID);
@@ -3670,19 +3673,21 @@
         // Total of 4 events
         assertEquals(4, mZenModeEventLogger.numLoggedChanges());
 
-        // First event: DND_TURNED_ON; active rules: 1; type is ACTIVE_RULE_TYPE_MANUAL
+        // First event: active rules changed; active rules: 1; type is ACTIVE_RULE_TYPE_MANUAL
         assertThat(mZenModeEventLogger.getEventId(0)).isEqualTo(
-                ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId());
+                ZenModeEventLogger.ZenStateChangedEvent.DND_ACTIVE_RULES_CHANGED.getId());
         assertThat(mZenModeEventLogger.getChangedRuleType(0)).isEqualTo(
-                DNDProtoEnums.MANUAL_RULE);
+                DNDProtoEnums.AUTOMATIC_RULE);
         assertThat(mZenModeEventLogger.getNumRulesActive(0)).isEqualTo(1);
         int[] ruleTypes0 = mZenModeEventLogger.getActiveRuleTypes(0);
         assertThat(ruleTypes0.length).isEqualTo(1);
-        assertThat(ruleTypes0[0]).isEqualTo(ACTIVE_RULE_TYPE_MANUAL);
+        assertThat(ruleTypes0[0]).isEqualTo(TYPE_BEDTIME);
 
         // Second event: active rules: 2; types are TYPE_MANUAL and TYPE_BEDTIME
+        assertThat(mZenModeEventLogger.getEventId(1)).isEqualTo(
+                ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId());
         assertThat(mZenModeEventLogger.getChangedRuleType(1)).isEqualTo(
-                DNDProtoEnums.AUTOMATIC_RULE);
+                DNDProtoEnums.MANUAL_RULE);
         assertThat(mZenModeEventLogger.getNumRulesActive(1)).isEqualTo(2);
         int[] ruleTypes1 = mZenModeEventLogger.getActiveRuleTypes(1);
         assertThat(ruleTypes1.length).isEqualTo(2);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index 48b12f7..e37da20 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -1115,7 +1115,7 @@
                 argThat(h -> (h.inputConfig & InputConfig.SPY) == InputConfig.SPY));
     }
 
-    @RequiresFlagsDisabled(Flags.FLAG_MAGNIFICATION_ALWAYS_DRAW_FULLSCREEN_BORDER)
+    @RequiresFlagsDisabled(Flags.FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER)
     @Test
     public void testDrawMagnifiedViewport() {
         final int displayId = mDisplayContent.mDisplayId;
diff --git a/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java b/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java
index d9cbea9..ed89190 100644
--- a/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java
+++ b/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java
@@ -181,7 +181,7 @@
                 // We only need to look at the broadcast events that occurred before
                 // this notification related event.
                 while (dispatchTimestampsMs.size() > 0
-                        && dispatchTimestampsMs.peekFirst() < timestampMs) {
+                        && dispatchTimestampsMs.peekFirst() <= timestampMs) {
                     final long dispatchTimestampMs = dispatchTimestampsMs.peekFirst();
                     final long elapsedDurationMs = timestampMs - dispatchTimestampMs;
                     // Only increment the counts if the broadcast was sent not too long ago, as
diff --git a/test-legacy/Android.bp b/test-legacy/Android.bp
new file mode 100644
index 0000000..236d704
--- /dev/null
+++ b/test-legacy/Android.bp
@@ -0,0 +1,38 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: [
+        "Android-Apache-2.0",
+    ],
+}
+
+java_library {
+    name: "android.test.legacy",
+    sdk_version: "current",
+    libs: [
+        "android.test.mock.stubs",
+        "junit",
+    ],
+    static_libs: [
+        "android.test.base-minus-junit",
+        "android.test.runner-minus-junit",
+    ],
+    dist: {
+        targets: [
+            "sdk",
+        ],
+    },
+}
diff --git a/test-legacy/Android.mk b/test-legacy/Android.mk
deleted file mode 100644
index da9dc25..0000000
--- a/test-legacy/Android.mk
+++ /dev/null
@@ -1,50 +0,0 @@
-#
-# Copyright (C) 2018 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.
-#
-
-LOCAL_PATH:= $(call my-dir)
-
-# For unbundled build we'll use the prebuilt jar from prebuilts/sdk.
-ifeq (,$(TARGET_BUILD_APPS)$(filter true,$(TARGET_BUILD_PDK)))
-
-# Build the android.test.legacy library
-# =====================================
-# Built against the SDK so that it can be statically included in APKs
-# without breaking link type checks.
-#
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := android.test.legacy
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 SPDX-license-identifier-MIT SPDX-license-identifier-Unicode-DFS
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../NOTICE
-
-LOCAL_SDK_VERSION := current
-
-LOCAL_JAVA_LIBRARIES := junit android.test.mock.stubs
-LOCAL_STATIC_JAVA_LIBRARIES := \
-    android.test.base-minus-junit \
-    android.test.runner-minus-junit \
-
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-$(call declare-license-metadata,$(full_classes_jar),\
-    SPDX-license-identifier-Apache-2.0 SPDX-license-identifier-MIT SPDX-license-identifier-Unicode-DFS,\
-    notice,$(LOCAL_PATH)/../NOTICE,Android,frameworks/base)
-
-# Archive a copy of the classes.jar in SDK build.
-$(call dist-for-goals,sdk,$(full_classes_jar):android.test.legacy.jar)
-
-endif  # not TARGET_BUILD_APPS not TARGET_BUILD_PDK=true